SecretManager interface, allowing direct process.env reads to be banned via ESLint everywhere else.
SecretManager Interface
The coreSecretManager interface is intentionally minimal — no bulk access or enumeration of values.
Source:
packages/core/src/security/secret-manager.ts — immutable interface with 4 methods.OAuth credentials use a separate port. API keys and other read-only secrets live in
SecretStorePort (this page). OAuth tokens — which need refresh-on-write semantics with per-profile-id locking — live in OAuthCredentialStorePort. The two are intentionally separate: the secret store is read-only by contract; the OAuth store is mutable with refresh, expiry, and lock-coordinated writes. See OAuth concepts for the full picture.createSecretManager
SecretManager from a record of environment variables. Design principles:
- Defensive copy: Takes a snapshot of defined values into an internal
Map. Subsequent mutations to theenvparameter have no effect on the returned manager. - No enumeration: No
.envproperty or.getAll()method is exposed. The only way to list keys is viakeys(), which returns a defensive copy. - Diagnostic errors:
require()includes the missing key name in the error message for troubleshooting. - Immutable: The returned object has only the 4 documented methods.
envSubset
process.env to child_process.spawn(), use this to select only the keys the subprocess needs. Returns a plain object with only the allowed keys that exist in the manager.
SecretsCrypto
The encryption engine uses AES-256-GCM with HKDF-SHA256 key derivation. Pure cryptographic primitives — has zero knowledge of storage.Source:
packages/core/src/security/secret-crypto.ts — all operations return Result type, never throw (except createSecretsCrypto which validates the master key at creation time).Algorithm Details
| Component | Specification |
|---|---|
| Cipher | AES-256-GCM |
| Key Derivation | HKDF-SHA256 |
| HKDF info string | Versioned for future crypto upgrades |
| Master key minimum | 32 bytes |
| Salt | 32 bytes random per encryption |
| IV (nonce) | 12 bytes random (AES-GCM standard) |
| Auth tag | 16 bytes (GCM authentication tag) |
EncryptedSecret Interface
createSecretsCrypto
SecretsCrypto engine with the given master key.
- Master key must be at least 32 bytes (fails fast at creation, not per-call)
- Only the first 32 bytes are used
- Returns an object with
encrypt(plaintext)anddecrypt(encrypted)methods - Both methods return
Result<T, Error>— never throw
Encryption Flow
- Generate 32-byte random salt
- Derive encryption key via HKDF-SHA256:
hkdfSync("sha256", masterKey, salt, infoString, 32) - Generate 12-byte random IV
- Encrypt with
aes-256-gcmusing derived key and IV - Return
{ ciphertext, iv, authTag, salt }
parseMasterKey
| Format | Minimum Length | Bytes Produced |
|---|---|---|
| Hex | 64 characters | 32+ bytes |
| Base64 | 44 characters | 32+ bytes |
Error if neither encoding produces at least 32 bytes.
ScopedSecretManager
A per-agentSecretManager decorator that restricts access to secrets matching glob patterns and emits audit events.
Source:
createScopedSecretManager() in packages/core/src/security/secret-manager.ts (same module as SecretManager) — decorator pattern, callers cannot distinguish from a plain SecretManager.createScopedSecretManager
| Field | Type | Description |
|---|---|---|
agentId | string | Agent this scoped manager belongs to (included in audit events) |
allowPatterns | string[] | Glob patterns that grant access |
eventBus | TypedEventBus? | Optional event bus for audit event emission |
Access Control
- Glob pattern matching: Case-insensitive, supports
*wildcard. Patternopenai_*matchesOPENAI_API_KEY. - Empty
allowPatterns= unrestricted access: This is intentional for backward compatibility — existing agents with nosecrets.allowconfig get Zod default of[], which means “no restrictions” (not “deny all”). - Deny behavior: When access is denied,
get()returnsundefined,has()returnsfalse,require()throws with a descriptive error.
Audit Events
Every access attempt (viaget, has, or require) emits a secret:accessed event through the optional eventBus:
keys() filters the returned list but does NOT emit events (listing operation, not access).
Unrestricted Access Warning
The first time an agent accesses a secret without explicitsecrets.allow configuration (empty allowPatterns), a one-time security:warn event is emitted:
ScopedSecretManager instance.
Credential Broker as a SecretManager Consumer
The credential broker is a per-request consumer of the platform (unscoped)SecretManager. On each proxied HTTPS request, the broker calls SecretManager.get(secretRef) to resolve the binding’s secret. The key is never cached to disk — it lives only in the daemon’s encrypted store and in memory for the duration of the injection.
Resolution emits a secret:accessed audit event with the following fields: secretName, agentId, outcome (success/not_found), timestamp. A not_found outcome causes the broker to return 502 and refuse to forward the request — there is no fallback path that sends a request upstream without a valid credential.
Scoping note. The broker uses the platform (unscoped) SecretManager for executor-level bindings (executor.broker.bindings[*].secretRef). Per-agent scoped secrets in exec tool invocations (secretRefs in exec tool config) use a ScopedSecretManager — these are two distinct injection paths and are intentionally kept separate.
Source:
packages/infra/src/credential-broker/mitm-broker.ts:501–506 — secret:accessed emission on both success and not_found paths. See Credential Broker for the complete broker deep-dive.SecretRef Resolution
Secret references allow YAML config files to reference secrets from multiple sources without storing plaintext values.Source:
packages/core/src/security/secret-ref-resolver.ts — Three resolution providers.Providers
| Source | Syntax | Description |
|---|---|---|
env | env:provider/KEY_NAME | Reads from SecretManager (environment variables or encrypted store) |
file | file:provider/path | Reads from an absolute file path. Supports JSON Pointer extraction via provider#/json/pointer syntax |
exec | exec:provider/id | Invokes a credential helper binary via JSON RPC protocol with timeout |
File Provider Details
- Path must be absolute (rejects relative paths)
- Rejects paths containing
..segments - Validates file exists and is a regular file
- Handles symlinks: resolves with
realpathSyncand re-validates - Maximum file size: 1 MB (default
fileMaxBytes) - JSON Pointer extraction:
provider#/json/pointersyntax for extracting specific fields from JSON files
Exec Provider Details
- Sends JSON RPC request:
{ protocolVersion: 1, provider, ids: [id] } - Default timeout: 10 seconds (
execTimeoutMs) - Maximum output buffer: 1 MB
- Validates response shape:
{ protocolVersion: 1, values: { [id]: string } }
Config Deep Walk
resolveConfigSecretRefs() recursively walks a config object and resolves all SecretRef values to strings. Operates on a structuredClone — never mutates the original. Fails fast on the first resolution error.
Secrets Audit
The secrets audit scanner detects plaintext secrets in config files and known provider environment variables.Source:
packages/core/src/security/secrets-audit.ts — Structured findings for CLI and JSON output.scanConfigForSecrets
Walks raw parsed config recursively. For each leaf string value whose field name matchesSECRET_FIELD_PATTERN, emits a PLAINTEXT_SECRET finding unless the value is a SecretRef object or empty.
scanEnvForSecrets
Scans env records for known provider patterns. Detects API keys and secrets from 11 named providers (Anthropic, OpenAI, Telegram, Discord, Slack_BOT_TOKEN and _SIGNING_SECRET, Groq, Deepgram, ElevenLabs, Brave, Google, Comis SECRETS_MASTER_KEY) plus 4 generic suffix patterns (_API_KEY, _SECRET, _TOKEN, _PASSWORD).
Skips operational variables: COMIS_*, NODE_*, PATH, HOME, SHELL, USER, TERM, LANG, TZ, EDITOR, VISUAL.
Finding Types
| Code | Severity | Source | Description |
|---|---|---|---|
PLAINTEXT_SECRET | error | Config files | Non-empty string in a secret field name |
KNOWN_PROVIDER_ENV | warn | .env files | Known provider secret in environment |
HIGH_ENTROPY_VALUE | info | Various | Suspicious high-entropy string |
Configuration
Secrets storage (Global)
| Field | Type | Default | Description |
|---|---|---|---|
security.storage | "encrypted" | "file" | "env" | "encrypted" | Credential storage backend: encrypted (AES-256-GCM SQLite), file (plaintext JSON at 0600), or env (read-only, reads .env/process.env). The database path is fixed at <dataDir>/secrets.db and is not configurable. |
Source:
packages/core/src/config/schema-security.ts — SecurityConfigSchema Zod schema.AgentSecretsConfigSchema (Per-Agent)
| Field | Type | Default | Description |
|---|---|---|---|
allow | string[] | [] | Glob patterns for allowed secret names. Empty = unrestricted access. |
Secret Access Utilities
matchesSecretPattern
* as wildcard. Regex special characters in the pattern are treated as literals.
Source:
packages/core/src/security/secret-access.ts — custom 15-line implementation instead of picomatch/minimatch (sufficient for * wildcards).isSecretAccessible
true if the secret name matches any of the allow patterns, or if allowPatterns is empty (unrestricted access for backward compatibility).
Pattern Examples
| Pattern | Matches | Does Not Match |
|---|---|---|
openai_* | OPENAI_API_KEY, OPENAI_ORG_ID | ANTHROPIC_API_KEY |
*_api_key | OPENAI_API_KEY, BRAVE_API_KEY | DISCORD_BOT_TOKEN |
my_secret | MY_SECRET (case-insensitive) | MY_SECRET_2 |
* | Everything | — |
Security Model
Defense-in-depth security architecture
Node Permissions
Node.js permission model integration
OAuth
Subscription-based authentication storage and refresh
