Skip to main content
The secret management system provides centralized credential access, AES-256-GCM encryption, per-agent access scoping, and secret reference resolution. All credential access in the system goes through the SecretManager interface, allowing direct process.env reads to be banned via ESLint everywhere else.

SecretManager Interface

The core SecretManager 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

Creates a SecretManager from a record of environment variables. Design principles:
  • Defensive copy: Takes a snapshot of defined values into an internal Map. Subsequent mutations to the env parameter have no effect on the returned manager.
  • No enumeration: No .env property or .getAll() method is exposed. The only way to list keys is via keys(), 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

Creates a restricted environment record for subprocess spawning. Instead of passing the full 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

All fields are independent Buffer copies with no shared memory references.

createSecretsCrypto

Creates a 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) and decrypt(encrypted) methods
  • Both methods return Result<T, Error> — never throw

Encryption Flow

  1. Generate 32-byte random salt
  2. Derive encryption key via HKDF-SHA256: hkdfSync("sha256", masterKey, salt, infoString, 32)
  3. Generate 12-byte random IV
  4. Encrypt with aes-256-gcm using derived key and IV
  5. Return { ciphertext, iv, authTag, salt }

parseMasterKey

Parses a master key from a hex or base64 encoded string. 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-agent SecretManager 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

Options:

Access Control

  • Glob pattern matching: Case-insensitive, supports * wildcard. Pattern openai_* matches OPENAI_API_KEY.
  • Empty allowPatterns = unrestricted access: This is intentional — an agent with no secrets.allow config 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() returns undefined, has() returns false, require() throws with a descriptive error.

Audit Events

Every access attempt (via get, has, or require) emits a secret:accessed event through the optional eventBus:
Important: 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 explicit secrets.allow configuration (empty allowPatterns), a one-time security:warn event is emitted:
This warning fires only once per 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 realpathSync and re-validates
  • Maximum file size: 1 MB (default fileMaxBytes)
  • JSON Pointer extraction: provider#/json/pointer syntax 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 matches SECRET_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.tsSecurityConfigSchema Zod schema.

AgentSecretsConfigSchema (Per-Agent)

Secret Access Utilities

matchesSecretPattern

Case-insensitive glob matching for secret name filtering. Supports * 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

Returns 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