feat(secrets): egress-time credential injection with process-local sentinels#102009
Conversation
…ess-local sentinels SecretRef-managed model-provider credentials now travel as opaque oc-sent-v1 sentinels through auth storage, stream options, and SDK config; the guarded model fetch injects real values into headers and URLs immediately before the SSRF-guarded send and fails closed on unknown sentinels. packages/ai adapters converge on the host guarded fetch where the SDK supports custom fetch and unwrap at construction where it does not. Resolved values (and their percent-encoded forms) register for exact-value log redaction. Kill switch: OPENCLAW_SECRET_SENTINELS=off. Also fixes a pre-existing unhandled rejection race in capNonOkResponseBodyLazily (pipeThrough writer leak).
|
Codex review: found issues before merge. Reviewed July 8, 2026, 4:07 AM ET / 08:07 UTC. Summary PR surface: Source +1101, Tests +1597, Docs +15. Total +2713 across 60 files. Reproducibility: yes. Source inspection shows PR-head sentinel headers are swapped into Review metrics: 3 noteworthy metrics.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review findings
Review detailsBest possible solution: Keep this PR as the canonical candidate, but make local-service health probes sentinel-safe before merge and require maintainer security review of the cross-provider credential boundary. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows PR-head sentinel headers are swapped into Is this the best way to solve the issue? No, not yet. The process-local sentinel design is a plausible core fix, but this patch unwraps credentials early enough for local-service health probes to bypass the intended guarded egress boundary. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 4ae8d735bf54. Label changesLabel justifications:
Evidence reviewedPR surface: Source +1101, Tests +1597, Docs +15. Total +2713 across 60 files. View PR surface stats
Security concerns:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (3 earlier review cycles)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 84b522cb8d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| function redactCaptureText(value: string): string { | ||
| return redactRegisteredSecretValues(value, () => REDACTED_CAPTURE_HEADER_VALUE); | ||
| } |
There was a problem hiding this comment.
Redact registered secrets from WebSocket captures too
With debug proxy capture enabled, the new exact-value redaction is only wired into the HTTP capture path; the WebSocket path (captureWsEvent) still persists params.url, params.payload, and params.errorText directly. That means any registered SecretRef value that reaches a WS URL/frame/error in no-fetch realtime/WebSocket provider paths can still be stored in capture_events or payload blobs in plaintext while equivalent HTTP captures are masked. Please apply the same redactCaptureUrl/redactCaptureText handling before recording WS events.
Useful? React with 👍 / 👎.
|
Merged via squash.
|
…ntinels (openclaw#102009) * feat(secrets): resolve SecretRef model credentials at egress via process-local sentinels SecretRef-managed model-provider credentials now travel as opaque oc-sent-v1 sentinels through auth storage, stream options, and SDK config; the guarded model fetch injects real values into headers and URLs immediately before the SSRF-guarded send and fails closed on unknown sentinels. packages/ai adapters converge on the host guarded fetch where the SDK supports custom fetch and unwrap at construction where it does not. Resolved values (and their percent-encoded forms) register for exact-value log redaction. Kill switch: OPENCLAW_SECRET_SENTINELS=off. Also fixes a pre-existing unhandled rejection race in capNonOkResponseBodyLazily (pipeThrough writer leak). * test(plugin-sdk): update public surface budget
…ntinels (openclaw#102009) * feat(secrets): resolve SecretRef model credentials at egress via process-local sentinels SecretRef-managed model-provider credentials now travel as opaque oc-sent-v1 sentinels through auth storage, stream options, and SDK config; the guarded model fetch injects real values into headers and URLs immediately before the SSRF-guarded send and fails closed on unknown sentinels. packages/ai adapters converge on the host guarded fetch where the SDK supports custom fetch and unwrap at construction where it does not. Resolved values (and their percent-encoded forms) register for exact-value log redaction. Kill switch: OPENCLAW_SECRET_SENTINELS=off. Also fixes a pre-existing unhandled rejection race in capNonOkResponseBodyLazily (pipeThrough writer leak). * test(plugin-sdk): update public surface budget
What Problem This Solves
SecretRefs stop provider credentials from living in
openclaw.json, but the resolved plaintext still travels through the whole model-call chain:resolveApiKeyForProvider→ runtime auth storage → stream options → SDK clientapiKey. Any log line, error dump, proxy-capture file, or introspection surface along that chain can leak the real key. Twopackages/aiadapters (Anthropic, OpenAI responses) also constructed SDK clients without the host guarded fetch, so there was no in-repo interception point at all for those requests. The secrets docs punt the remaining risk to "an external credential proxy".Fixes #102008.
Why This Change Was Made
Egress-time injection closes the gap inside the runtime instead of requiring an external broker:
oc-sent-v1-<hex>) at model-auth resolution. Auth storage, stream options, SDK config, logs, and error objects only ever hold sentinels. Provider auth exchanges unwrap for the exchange and re-sentinel the result.packages/aiprovider now uses the latest injection point its SDK supports — custom fetch (Anthropic, OpenAI responses/completions, Azure, Mistral) or construction-time unwrap via a new inert host port (@google/genaiexposes no fetch hook; evidence in adapter comments). Key-shape auth routing (e.g.sk-ant-oatdetection) resolves through the host port without exposing values.redactSensitiveText/redactSecrets/proxy-capture, even when pattern redaction is off — defense in depth if plaintext ever reaches a log path.capNonOkResponseBodyLazily(pipeThroughwriter leak on cancellation/cap races) that the new tests exposed deterministically.Plugin SDK auth surfaces (
getApiKeyForModel/resolveApiKeyForProviderruntime methods) keep returning plaintext — shipped contract; plugins run their own HTTP clients. Internal call sites opt in viasecretSentinels: true. Flipping the default once the plugin SDK grows a sentinel-aware helper is a named follow-up.User Impact
models.providers.*.apiKeypaths) are protected automatically.OPENCLAW_SECRET_SENTINELS=off|0|falsekill switch for incident response; exact-value redaction stays on.docs/gateway/secrets.mdwith the exact threat model (this is not process isolation; same-process memory still holds the value at the final adapter boundary).Evidence
src/secrets/sentinel.test.ts(10),src/logging/redact.test.ts(115),src/agents/provider-transport-fetch.test.ts(88, 3 consecutive runs, zero unhandled errors), integration test proving a local HTTP server receives the real header while the caller held only the sentinel and logs redact the exact value.corepack pnpm build, andpnpm check:changedgreen on Blacksmith Testbox during development.OPENCLAW_HOME/OPENCLAW_STATE_DIR,models.providers.anthropic.apiKeyas afileSecretRef,openclaw agent --local -m "Reply with exactly the single word: pineapple" --model anthropic/claude-haiku-4-5→ reply"pineapple". Leak sweep: raw key present only in the staged key file; zero occurrences in the debug log, generatedmodels.json, session/trajectory files, or any state artifact.Adapter decision table (SDK fetch support verified against installed sources):
fetch/HTTPClient.fetcher)@google/genaihas no fetch hook)cf-aig-authorizationheader covered by substring swap + testNon-goals in this PR: channel tokens, MCP env/headers, skills env, sandbox egress proxying for tool traffic (tracked as follow-ups in the issue).