ADR-0006: No long-lived credential reachable from inside a container
- Status: Proposed
- Date: 2026-08-06
- Implementation:
partial— machinery shipped (src/engine/credentials.ts:200catalog,:264resolve;src/engine/git-scrub.ts:91), adoption pending.../credential-boundary.mdrecordsCLOUDFLARE_API_TOKENandNPM_TOKENas “machinery done, adoption pending” and operator secrets as open.
Context
Section titled “Context”flare-dispatch injects secret values into the command env with best-effort substring redaction
(6 of ~19 catalog runs take a secrets input; wrangler deploy needs CLOUDFLARE_API_TOKEN), and
embeds GitHub installation tokens in clone URLs that survive in .git/config. fractalbot’s ADR-0005
designed credential injection but never built it. Meanwhile the platform caught up: Cloudflare
Sandboxes’ outbound handlers can inject credentials Worker-side so “the secret lives in the Worker’s
environment and is never passed into the sandbox” — the machinery exists first-party; the substrate’s job
is deciding which credential attaches under which grant. flare-dispatch’s writeback path already
embodies the target: the Worker, never the container, performs the authenticated write.
Decision
Section titled “Decision”No long-lived credential is reachable from inside a container — env, argv, or filesystem. Writes leave by two sanctioned shapes:
- Worker-side writeback (preferred): the sandbox produces an artifact; the substrate or the consumer’s Worker performs the authenticated write.
- Handler-injected credentials for in-sandbox writes that cannot writeback (
wrangler deploy→api.cloudflare.comis the acceptance case): credentials are per-host descriptors{secretName, host, headerTemplate}in the run/recipe definition; the substrate resolves the value and injects it in the egress handler on requests that pass the grant, strips container-authored auth headers, and hands the container nothing (or an inert placeholder for tools that refuse to start without one).
Diagram source
flowchart TB accTitle: Where a credential attaches subgraph box["Container — no long-lived credential"] cmd["**command**"] art["**artifact**<br/>written under `/artifacts`"] end subgraph worker["Worker side"] secrets[("Worker secrets")] handler["**egress handler**<br/>drops container auth headers<br/>injects the host's `headerTemplate`"] wb["**writeback**<br/>substrate or consumer Worker"] end cmd -->|request that passes the grant| handler cmd --> art art --> wb secrets --> handler secrets --> wb handler --> api["credentialed host<br/>e.g. `api.cloudflare.com`"] wb --> write["authenticated write"] class secrets accentThe one sanctioned in-container credential is the per-execution model-proxy token: execution-scoped, budget-capped, header-only transport (query-param rejected), revoked by a DO alarm at the run’s max wall-clock — revocation does not depend on finalize running — and never logged.
Consequences
Section titled “Consequences”- The rule takes effect per credential class as its shape lands; a migration table (GitHub App
token,
CLOUDFLARE_API_TOKEN, npm token, operator secrets) tracks each. Thesecrets/secretPrefixrun inputs are deprecated at adoption and removed at stage-2 exit. - Clone URLs are scrubbed from git remotes immediately post-clone;
.gitconfig is redacted at the artifact/checkpoint capture chokepoint. - The substrate’s never-store/never-log list: consumer bot tokens (never reach the substrate), installation tokens, secret values, capability-token values, raw prompts beyond metering metadata.