Category: Security / Enhancement (relates to #11829, #11202, #9627)
Summary
TL;DR - API keys leak because the Gateway process both resolves credentials and runs the agent. No amount of field stripping (#11202) or output redaction (#10659) fixes this — the agent shares a trust boundary with every secret. This proposes a CredentialBroker that resolves and injects auth internally, so the agent execution path never sees raw credentials.
#11829 covers the leak vectors well. This issue names the structural cause: credential co-location.
The Gateway process is simultaneously:
- The thing that authenticates to APIs (needs real credentials)
- The thing that runs agent logic (processes untrusted input)
Current flow:
openclaw.json / env vars / credentials/
→ model-auth.ts resolves apiKey → plain string
→ models-config.ts writes to models.json
→ system prompt serializes it (#11202)
→ agent sees all provider API keys every turn
Same pattern for channels — channels.telegram.botToken etc. are plain strings in Gateway memory, readable by any tool the agent invokes.
This means fixing #11202 (strip from prompt context) and #10659 (output redaction) are necessary but insufficient. A prompt-injected agent can still:
- Read
~/.openclaw/credentials/ via exec tool
- Enumerate env vars (
env | grep KEY)
- Access config via the filesystem
- Read Gateway process memory (plugins run in-process)
Proposed solution
One new module: src/agents/credential-broker.ts
Agent Execution (untrusted input)
│
│ "call anthropic/claude-opus-4-6 with this prompt"
│ (no apiKey, no token, no secret)
│
▼
Credential Broker (closure boundary)
│
│ resolves auth from profiles/env/files
│ injects headers into outbound request
│ strips secrets from model catalog
│
▼
Authenticated HTTP request → api.anthropic.com
The broker is a function boundary, not a process boundary. The existing model-auth.ts resolution logic stays — the broker wraps it so that callers get authenticated requests back instead of raw credential strings.
export interface CredentialBroker {
// Inject auth into outbound request. Caller never sees the raw key.
injectAuth(request: OutboundRequest): Promise;
// Model catalog for prompt context — credentials stripped.
getSafeModelCatalog(): SafeModelInfo[];
}
What Changes
Phase 1 (~50 LOC, days): Strip apiKey from model catalog before prompt serialization. This is the #11829 Layer 1 fix, included as the first commit.
Phase 2 (~300-500 LOC, weeks): The broker itself.
model-auth.ts becomes an internal detail of the broker
agent-scope.ts calls broker.injectAuth() instead of resolving auth directly
models-config.ts uses broker.getSafeModelCatalog()
Phase 3 (future PR): Extend to channel adapters and plugins.
Config Extension: apiKeyFile
For deployments managing secrets externally (agenix, Docker secrets, Vault, SOPS), add optional file-path alternatives:
{
"models": {
"providers": {
"anthropic": {
"apiKeyFile": "/run/secrets/anthropic-api-key"
}
}
}
}
Broker reads at runtime, warns if permissions aren't 0600. This also fixes #9627 (config writes resolving env vars to plaintext) — apiKeyFile references survive config serialization.
Same pattern as systemd EnvironmentFile, PostgreSQL password_file, Docker secrets.
What this doesn't cover
- Network egress control (agent can still make outbound HTTP)
- Prompt injection prevention (broker reduces blast radius, doesn't prevent injection)
- Skill supply chain (malicious skills in-process still dangerous)
Additional context
@steipete, @theonejvo
Category: Security / Enhancement (relates to #11829, #11202, #9627)
Summary
TL;DR - API keys leak because the Gateway process both resolves credentials and runs the agent. No amount of field stripping (#11202) or output redaction (#10659) fixes this — the agent shares a trust boundary with every secret. This proposes a
CredentialBrokerthat resolves and injects auth internally, so the agent execution path never sees raw credentials.#11829 covers the leak vectors well. This issue names the structural cause: credential co-location.
The Gateway process is simultaneously:
Current flow:
Same pattern for channels —
channels.telegram.botTokenetc. are plain strings in Gateway memory, readable by any tool the agent invokes.This means fixing #11202 (strip from prompt context) and #10659 (output redaction) are necessary but insufficient. A prompt-injected agent can still:
~/.openclaw/credentials/via exec toolenv | grep KEY)Proposed solution
One new module:
src/agents/credential-broker.tsThe broker is a function boundary, not a process boundary. The existing
model-auth.tsresolution logic stays — the broker wraps it so that callers get authenticated requests back instead of raw credential strings.What Changes
Phase 1 (~50 LOC, days): Strip
apiKeyfrom model catalog before prompt serialization. This is the #11829 Layer 1 fix, included as the first commit.Phase 2 (~300-500 LOC, weeks): The broker itself.
model-auth.tsbecomes an internal detail of the brokeragent-scope.tscallsbroker.injectAuth()instead of resolving auth directlymodels-config.tsusesbroker.getSafeModelCatalog()Phase 3 (future PR): Extend to channel adapters and plugins.
Config Extension:
apiKeyFileFor deployments managing secrets externally (agenix, Docker secrets, Vault, SOPS), add optional file-path alternatives:
{ "models": { "providers": { "anthropic": { "apiKeyFile": "/run/secrets/anthropic-api-key" } } } }Broker reads at runtime, warns if permissions aren't
0600. This also fixes #9627 (config writes resolving env vars to plaintext) —apiKeyFilereferences survive config serialization.Same pattern as systemd
EnvironmentFile, PostgreSQLpassword_file, Docker secrets.What this doesn't cover
Additional context
{{secret:VAR}}placeholders — broker implements the runtime substitution this proposesfetch()apiKeyFilefixes this@steipete, @theonejvo