PRD 0010: Credential proxy for agent-bound API tokens #14
Reference in New Issue
Block a user
Delete Branch "cred-proxy"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Draft PRD for a per-bottle reverse proxy that holds API tokens (Anthropic OAuth, GitHub PAT, Gitea PAT, npm) in a root-owned process inside the agent container. The agent (
node, UID 1000) keeps only URLs in its environ; the proxy injects the rightAuthorizationheader and forwards over TLS. The kernel'sptrace_may_accesscheck is the boundary — same property the git-gate already relies on.AWS / SigV4 explicitly out of scope: per-request signing doesn't fit a bearer-injection proxy. Held for a future PRD.
Companion build path to the landscape note at
docs/research/agent-credential-proxy-landscape.md, which surveys nono / Infisical Agent Vault / LLM gateways and recommends build over adopt.PRD-only; no implementation in this PR.
fe9d05664cto9fa9717135Lifts bottle.tokens into a per-route CredProxyUpstream table, renders a mode-600 routes.json that carries no token values or host env-var names, and derives the {token_env: TokenRef} map the launch step will use to forward host env values into the sidecar's environ. Shape mirrors GitGate/PipelockProxy: abstract base does the host-side prepare; start/stop is backend-specific. No backend wiring yet.Replace bottle.tokens (with Kind enum and hardcoded per-kind route/auth tables) with bottle.cred_proxy.routes — each route declares its own path, upstream, auth_scheme, token_ref, and optional role[]. The manifest is now the source of truth for the proxy's runtime route table; adding an upstream is a manifest edit, not a code change. Agent-side rewrites move from per-kind dispatch to per-role tags on routes: anthropic-base-url -> set ANTHROPIC_BASE_URL=<proxy><path> npm-registry -> write ~/.npmrc registry= git-insteadof -> write ~/.gitconfig [url] insteadOf, keyed off route.upstream (suppressed when bottle.git brokers the same host) tea-login -> add a ~/.config/tea/config.yml login Roles are a list (string accepted as sugar). A gitea route typically carries ["git-insteadof", "tea-login"]. Singleton roles (anthropic-base-url, npm-registry) appear on at most one route. token_env slots are assigned per distinct TokenRef in declaration order — two routes sharing a token_ref (e.g. github API + git endpoints) share a slot. Drops: TOKEN_KINDS, _KIND_ROUTES, _KIND_AUTH_SCHEME, _TOKEN_DEFAULT_HOST, cred_proxy_route_path_for_gitea, the kind field on CredProxyUpstream, and the kind-based hardcoding in pipelock_token_hosts (now derives from route.UpstreamHost). Legacy bottle.tokens manifests now die with a hint pointing at bottle.cred_proxy.routes + this PRD. Tests rewritten end-to-end. Docs + example.json + the dev ~/claude-bottle.json updated to match.Two failure-clarity paper cuts from the cred-proxy debugging: 1. Every docker create / start / network-connect call on the three sidecars (pipelock, git-gate, cred-proxy) was piping stderr to DEVNULL. A stuck orphan from a previous run produced "failed to create pipelock sidecar claude-bottle-pipelock-demo" with no pointer at the real cause ("Conflict. The container name ... is already in use ..."). Switch each call to capture_output=True and include the stripped stderr in the die() message. 2. The agent container had a container_exists() probe in resolve_plan that fails fast with a hint, but the sidecars (whose names are deterministic from the slug) didn't. So an orphan caused launch() to bail deep inside docker create. Add a probe in resolve_plan for each sidecar this launch will actually try to create: pipelock always; git-gate when bottle.git is non-empty; cred-proxy when bottle.cred_proxy.routes is non-empty. Die with a "./cli.py cleanup" pointer. Smoke-tested with an orphaned pipelock-<slug> container — the new probe fires with the expected hint before any sidecar build/start work begins.claude-code's chat bodies legitimately trip pipelock's BIP-39 seed- phrase detector — any 12+ English words that pass the BIP-39 checksum match. The direct path to api.anthropic.com already sits on tls_interception.passthrough_domains so no body scan runs there, but the cred-proxy hop is plain HTTP through pipelock and the body scanner fires. Add an anthropic-route-specific suppress entry: suppress: - rule: "BIP-39 Seed Phrase" path: "/anthropic/**" Just this one detector, only on this one path. Every other DLP pattern (AKIA, gh*_, sk-ant-, etc.) keeps firing — those are unambiguous credential shapes with no legitimate reason to appear in a chat completion. Other detectors that fire on natural language can be added to the suppress list when/if they surface. Wiring: pipelock_effective_suppress(bottle) computes the entries from bottle.cred_proxy.routes; pipelock_build_config accepts them and emits a `suppress:` block; pipelock_render_yaml renders it. Probed schema with `pipelock check --config` to confirm the {rule, path} shape; full yaml validates clean.The previous attempt added a `suppress: [{rule, path}]` entry. The yaml validated and the entry showed up in the live pipelock's config, but the BIP-39 detector kept firing — `suppress` only silences alerts, not enforcement. Reproduced the failure in isolation, probed three knobs against a real pipelock with a canonical BIP-39 body (`abandon abandon ... about`): suppress: [{rule: "BIP-39 Seed Phrase", path: "/anthropic/**"}] -> still 403 rules.disabled: ["dlp:BIP-39 Seed Phrase"] -> still 403 seed_phrase_detection: { enabled: false } -> 200 (forwarded) Only the global toggle actually stops the block. Pipelock 2.3.0 has no per-path / per-host knob for this detector, so the trade-off is: when the bottle declares an `anthropic-base-url` route, BIP-39 detection comes off globally for that bottle. Every other DLP pattern (gh*_, sk-ant-, AKIA, etc.) keeps firing — the ones that actually map to claude-bottle's threat model. Drops the `suppress:` emitter from pipelock_build_config / pipelock_render_yaml; replaces with a `seed_phrase_detection: { enabled: false }` block driven by `pipelock_seed_phrase_detection_enabled(bottle)`. Tests flip from suppress-shape to seed_phrase shape. End-to-end probe through the real pipelock image confirms BIP-39 bodies forward.