> ## Documentation Index
> Fetch the complete documentation index at: https://comis-feature-matrix-channel.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Secret Manager

> Secret management system technical reference covering SecretManager interface, encryption, scoping, and audit

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.

<Info>
  **Source:** `packages/core/src/security/secret-manager.ts` -- immutable interface with 4 methods.
</Info>

<Info>
  **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](/security/oauth) for the full picture.
</Info>

```typescript theme={null}
interface SecretManager {
  /** Get a secret value by key, or undefined if not set. */
  get(key: string): string | undefined;

  /** Check if a secret key is available (value is defined). */
  has(key: string): boolean;

  /**
   * Get a secret value by key, throwing if not set.
   * Error message includes the key name for diagnostics.
   */
  require(key: string): string;

  /** Get the list of available key names (defensive copy). */
  keys(): string[];
}
```

### createSecretManager

```typescript theme={null}
function createSecretManager(
  env: Record<string, string | undefined>
): SecretManager
```

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

```typescript theme={null}
function envSubset(
  manager: SecretManager,
  allowedKeys: readonly string[]
): Record<string, string>
```

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.

<Info>
  **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).
</Info>

### 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

```typescript theme={null}
interface EncryptedSecret {
  readonly ciphertext: Buffer;  // AES-256-GCM ciphertext
  readonly iv: Buffer;          // 12-byte initialization vector
  readonly authTag: Buffer;     // 16-byte GCM authentication tag
  readonly salt: Buffer;        // 32-byte random salt for HKDF
}
```

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

### createSecretsCrypto

```typescript theme={null}
function createSecretsCrypto(masterKey: Buffer): SecretsCrypto
```

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

```typescript theme={null}
function parseMasterKey(raw: string): Buffer
```

Parses a master key from a hex or base64 encoded string.

| Format | Minimum Length | Bytes Produced |
| ------ | -------------- | -------------- |
| Hex    | 64 characters  | 32+ bytes      |
| Base64 | 44 characters  | 32+ bytes      |

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.

<Info>
  **Source:** `createScopedSecretManager()` in `packages/core/src/security/secret-manager.ts` (same module as `SecretManager`) -- decorator pattern, callers cannot distinguish from a plain `SecretManager`.
</Info>

### createScopedSecretManager

```typescript theme={null}
function createScopedSecretManager(
  base: SecretManager,
  options: ScopedSecretManagerOptions
): SecretManager
```

Options:

| 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. 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`:

```typescript theme={null}
{
  secretName: string;
  agentId: string;
  outcome: "success" | "denied" | "not_found";
  timestamp: number;
}
```

**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:

```typescript theme={null}
{
  category: "secret_access",
  agentId: string,
  message: "Agent ... accessed secret ... without explicit secrets.allow configuration.",
  timestamp: number
}
```

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.

<Info>
  Source: `packages/infra/src/credential-broker/mitm-broker.ts:501–506` — secret:accessed emission on both success and not\_found paths. See [Credential Broker](/security/credential-broker) for the complete broker deep-dive.
</Info>

## SecretRef Resolution

Secret references allow YAML config files to reference secrets from multiple sources without storing plaintext values.

<Info>
  **Source:** `packages/core/src/security/secret-ref-resolver.ts` -- Three resolution providers.
</Info>

### 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 `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.

<Info>
  **Source:** `packages/core/src/security/secrets-audit.ts` -- Structured findings for CLI and JSON output.
</Info>

### 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

| 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. |

<Info>
  **Source:** `packages/core/src/config/schema-security.ts` -- `SecurityConfigSchema` Zod schema.
</Info>

### AgentSecretsConfigSchema (Per-Agent)

| Field   | Type       | Default | Description                                                          |
| ------- | ---------- | ------- | -------------------------------------------------------------------- |
| `allow` | `string[]` | `[]`    | Glob patterns for allowed secret names. Empty = unrestricted access. |

```yaml theme={null}
# Global secrets configuration
security:
  storage: encrypted   # encrypted (default) | file | env; DB path is fixed at <dataDir>/secrets.db

# Per-agent secrets access
agents:
  my-agent:
    secrets:
      allow:
        - "OPENAI_*"
        - "ANTHROPIC_*"
        - "MY_CUSTOM_KEY"
```

## Secret Access Utilities

### matchesSecretPattern

```typescript theme={null}
function matchesSecretPattern(secretName: string, pattern: string): boolean
```

Case-insensitive glob matching for secret name filtering. Supports `*` as wildcard. Regex special characters in the pattern are treated as literals.

<Info>
  **Source:** `packages/core/src/security/secret-access.ts` -- custom 15-line implementation instead of picomatch/minimatch (sufficient for `*` wildcards).
</Info>

### isSecretAccessible

```typescript theme={null}
function isSecretAccessible(
  secretName: string,
  allowPatterns: string[]
): boolean
```

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

| 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                        | --                  |

For user-facing setup guide, see [Secrets](/security/secrets).

<CardGroup cols={2}>
  <Card title="Security Model" icon="shield" href="/security">
    Defense-in-depth security architecture
  </Card>

  <Card title="Node Permissions" icon="lock" href="/reference/node-permissions">
    Node.js permission model integration
  </Card>

  <Card title="OAuth" icon="shield-check" href="/security/oauth">
    Subscription-based authentication storage and refresh
  </Card>
</CardGroup>
