Skip to main content

The Problem

Exec tools are powerful — and that power cuts both ways. A process running inside the exec sandbox can read environment variables, iterate /proc/self/environ, or cat any file its user can reach. Handing a live API key to an exec process — even inside a filesystem sandbox — means the key is accessible to any code that runs in that process, including prompt-injected commands. The credential broker solves this by keeping the key out of the sandbox entirely. The daemon holds the key in its encrypted secret store; the agent process only ever sees a placeholder. At the network boundary, the broker intercepts the TLS connection, terminates it with its own CA, and swaps the placeholder for the real credential before forwarding the request upstream. From inside the sandbox: cat /proc/self/environ shows ANTHROPIC_API_KEY=broker-placeholder. The real key is never a file, an environment variable, or a /proc entry inside the namespace.

Architecture

The broker works as an in-process MITM proxy running inside the daemon. Here is the full flow for a driven-CLI spawn:
  1. Store the key once. The operator runs comis secrets set ANTHROPIC_EXECUTOR_KEY — the key is encrypted and stored in the daemon’s SecretManager.
  2. Configure the binding. An executor.broker.bindings entry maps api.anthropic.com to the anthropic preset and the ANTHROPIC_EXECUTOR_KEY secret reference.
  3. Spawn the driven CLI. When the broker-enabled sandbox spawns the CLI process, the following environment variables are injected:
    • HTTPS_PROXY / HTTP_PROXY — points to the broker’s unix socket
    • NODE_EXTRA_CA_CERTS — path to the broker’s CA certificate
    • ANTHROPIC_API_KEY=broker-placeholder — a non-secret placeholder value
  4. Broker intercepts the request. The CLI sends its HTTPS request through the proxy. The broker terminates the TLS connection using its own CA (which the CLI trusts via NODE_EXTRA_CA_CERTS), then inspects the target host.
  5. Credential injection. For api.anthropic.com, the broker matches the anthropic binding, resolves ANTHROPIC_EXECUTOR_KEY from SecretManager per-request, strips the Authorization header, and injects the real key as x-api-key. The modified request is forwarded to the upstream API over a fresh TLS connection.
  6. Audit trail. Every inject, deny, and blocked-egress event carries agentId and traceId — providing full observability without ever logging the credential value.

Broker vs exec secretRefs

Two credential injection paths exist in Comis: Use secretRefs for simple key-as-env-var cases. Use the credential broker when you need the key to be unreachable inside the sandbox.
Source: packages/skills/src/tools/builtin/exec-tool/exec-shared.ts — broker spawn env injected last via brokerSpawnEnv merge (guard: only driven-CLI spawns receive it; general exec spawns receive none of these vars)

Configuration

The executor.broker config is wired into the daemon (AppConfigSchemasetupBroker): adding an executor: block to config.yaml starts the broker at boot — a TCP listener plus a 0600 unix socket at ~/.comis/broker.sock. The examples below are the active config contract.
~/.comis/config.yaml

Binding fields

Source: packages/core/src/config/schema-broker.ts — BrokerBindingConfigSchema (z.strictObject; unknown keys are rejected)

Built-in presets

Two built-in presets ship today. Anthropic and Finnhub today; more presets are on the catalog roadmap.
Source: packages/core/src/security/provider-catalog/presets.ts — PRESETS array, 2 entries

Custom bindings

The broker is provider-agnostic. Any host can be covered by a custom hostRules entry — no curated preset is required. A binding with secretRef only and no explicit inject array defaults to Authorization: Bearer injection.

Host pattern kinds

Injection rule vocabulary

Source: packages/core/src/config/schema-broker.ts — InjectionRuleSchema discriminated union

Fail-Closed Guarantees

The broker is fail-closed: no binding, no proxy token, no path access — the request is refused. A missing secret never forwards the request. The 502 case deserves emphasis: if the secret is unavailable, the broker returns 502 rather than forwarding with a missing or placeholder credential. There is no fallback path that leaks a request upstream without a valid secret.
Source: packages/infra/src/credential-broker/mitm-broker.ts — fail-closed gates at lines 250, 269, 364, 405, 419, 450, 490, 522, 571

Observability

broker:* events

Every stage of the broker pipeline emits a typed event on the Comis event bus. The event taxonomy covers seven events: The broker:egress_blocked event deliberately carries targetHostHash rather than the plaintext hostname. The SHA-256 hash is computed inside the emit helper — there is no call path that emits the raw host value into the event payload (redaction-by-construction).

secret:accessed event

A secret:accessed event is emitted separately from the broker:* taxonomy on the success, denied, and not_found paths. Fields: secretName, agentId, outcome (success/denied/not_found), timestamp. The durable audit trail persists success reads (a secret was accessed) and denied reads (a blocked access); a routine not_found read (an optional-key probe resolving to nothing — no access, re-emitted every turn) is not persisted, keeping the trail free of per-turn probe noise.

Structured logging

Every pipeline stage carries traceId, agentId, and step; every failure log carries err, errorKind, and a non-empty hint. Hosts where credentials are injected via query param (setParam) never emit a full URL in logs — only the path prefix is logged, never the query string.
Source: packages/infra/src/credential-broker/broker-events.ts — typed emit helpers; emitEgressBlocked enforces the hash-only invariant structurally

Secure Credential Home

When secureCredentialHome: true is set on the sandbox options, the following bind mounts are removed for the credentialed sandbox:
  • ~/.claude (read-write bind)
  • ~/.claude.json (read-only bind)
  • ~/.local/share/claude (read-write bind)
This means cat ~/.claude/.credentials.json inside the sandbox returns “no such file or directory”. The Claude Code CLI cannot read its own credential cache from inside the sandbox.
Source: packages/skills/src/tools/builtin/sandbox/bwrap-provider.ts:196–218 — credential home bind removal for secure profile

Network Isolation (Linux)

On Linux, the credentialed sandbox runs with --unshare-net; the broker unix socket is the only bind-mounted network path — so the broker is the only reachable egress destination for driven-CLI traffic. This kernel-enforced isolation is validated on the Linux production host class (Ubuntu 24.04 LTS, kernel 6.8, rootless bubblewrap, run as the unprivileged service user): a direct connection to any non-broker host from inside the namespace fails with “network is unreachable”, while the bound broker socket stays reachable and carries the injected-credential request end-to-end. Network modes (SandboxOptions.network): macOS note: The broker-only network mode requires bubblewrap and is Linux-only. The broker still runs on macOS (TLS termination, injection, events) but without network namespace enforcement. On macOS, sandbox-exec is used for filesystem isolation and does not support the broker-only network mode.
Source: packages/skills/src/tools/builtin/sandbox/bwrap-provider.ts — broker-only branch at line 234; SandboxOptions.network union defined in packages/skills/src/tools/builtin/sandbox/types.ts

Troubleshooting

Broker request fails with 407 (bad_token):
Broker returns 403 (no_binding): The requested host has no matching binding. Add a hostRules entry or use a preset. Verify the host matches exactly (including any subdomain). Broker returns 502 (credential_unavailable): SecretManager returned undefined for the secretRef. Verify the key exists: comis secrets list. Check for typos between the secretRef value in config and the key name in the secrets store. Broker returns 501 (ws_upgrade_not_supported): WebSocket credential injection is not supported in this release; it is a future capability. The broker returns 501 with the ws_upgrade_not_supported reason code so the error is actionable rather than a silent connection hang. Trace a full request by traceId: Every broker log entry carries a step field. To trace a complete request pipeline:
Log a specific agent’s broker activity:

Known Limitations

  • One broker-issued exec per agent assembly: The session token is issued once per assembleToolsForAgent call, not per exec invocation. The broker token is single-use: the first exec call in a turn consumes it, so a second exec call in the same agent turn will receive a 407 Proxy Auth Required from the broker rather than a clean execution error. Workaround: Limit each agent turn to a single exec call when executor.broker is enabled, or structure multi-step work so successive execs are in separate turns. Resolution: Per-command token issuance (threading sessionManager into ExecToolDeps so issueToken() is called inside execute()) is planned as follow-on work.

Capability lease (jailed script surface)

Separate from the egress broker above, the daemon mints a capability lease to authenticate the jailed script surface to the loopback capability endpoint. The lease is the authentication boundary — it carries the agent’s resolved orchestration capabilities, not a powerful credential. It is minted per root-run, injected as COMIS_CAP_LEASE (the bearer) + COMIS_ORCH_SOCKET (the 0600 socket path) into the jailed spawn environment, and registered with the OutputGuard at mint so the bearer is never logged. The lease record: The lease is multi-use (the jailed child dispatches many calls per run), revocable (a revoked lease is denied at both validate and renew), and audience-bound (RFC 8707): each use is checked against the requested method’s required capability, so a captured lease cannot be replayed at a foreign method. The lease lives in memory (run-scoped) — there is no on-disk lease table, only the ephemeral cap.sock socket. The operator-facing revoke RPC + cascade and the endpoint rate-limit are a later release; this release ships the lease, the validate path that respects revocation/expiry/audience, and the fail-closed deny matrix.

Non-Goals (This Release)

  • OAuth / subscription auth — future
  • AWS SigV4 signing execution — the awsSigV4 finalizer interface ships as a tested no-op pass-through (deferred)
  • Full WebSocket credential injection — future; the fail-closed guard (501) is shipped
  • External vault / on-demand secret fetch — future; SecretManager is the only resolution path today
  • Operator-facing lease revoke RPC + cascade — a later release; the revoked field + the revocation-respecting validate/renew paths ship now

Exec Sandbox

Filesystem isolation for the system.exec tool — the sandbox the broker protects

Secret Manager

Encrypted credential store that the broker resolves per-request

Audit Log

broker:* events in the full event taxonomy

Defense in Depth

How the credential broker fits into the 22 categorical security layers