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
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
Tries hex decoding first (even length, 64+ chars). Falls back to base64 if hex fails. Throws
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
Access Control
- Glob pattern matching: Case-insensitive, supports
*wildcard. Patternopenai_*matchesOPENAI_API_KEY. - Empty
allowPatterns= unrestricted access: This is intentional — an agent with nosecrets.allowconfig gets the Zod default of[], which means “no restrictions” (not “deny all”), so leaving scoping unconfigured never locks an agent out of every credential. - 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
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
Configuration
Secrets storage (Global)
Source:
packages/core/src/config/schema-security.ts — SecurityConfigSchema Zod schema.AgentSecretsConfigSchema (Per-Agent)
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 (an empty list means unrestricted access, not deny-all).
Pattern Examples
For user-facing setup guide, see Secrets.
Security Model
Defense-in-depth security architecture
Node Permissions
Node.js permission model integration
OAuth
Subscription-based authentication storage and refresh
