OpenCode custody: static-key slots served from the vault through an in-process auth proxy
Implements the plugin design from #17, static-key slice: OpenCode type:"api" providers (deepseek, synthetic, minimax-coding-plan, …) get their keys moved into the vault, the auth.json entry becomes a non-secret tombstone, and a plugin serves the real key per request from a capability handle.
How it works (source-verified on stock OpenCode 1.18.25; the spike in scripts/spikes/ is the executed proof):
- The plugin registers through the
confighook by injecting{ apiKey: sentinel, fetch }intocfg.provider[id].options. Config provider options are re-applied after everyauth.loader(provider.ts:1643-1650), so the injectedfetchalso replaces a shipped plugin's — no fork, no per-provider knowledge. - The
fetchis an in-process auth proxy: it rewrites every header value and URL query value equal to the sentinel with the vault-served material, forwards, and observes the response. 401 →report_auth_failurewith the record_version that was actually served, then the next account; 429/402 → cooldown, next; 403/5xx → returned as-is (not a credential verdict). - Ownership is a conjunction: tombstone in
auth.json(absence of a local credential) ANDserve: "opencode-claustrum"in the handle file (who owns the slot). The seven cells are explicit; a real key sitting behind an owned slot gets a refusing fetch, never either copy. - Multiple keys per provider:
apikey:<provider>:<label>, handle-file order is failover order. - Redirects are followed manually and same-origin only; cross-origin redirects are refused so a substituted
x-api-keynever leaves the configured origin.
CLI (ck auth, admin gate, key never on argv): migrate-opencode [--dry-run|--replace|--restore <provider>] [--provider …]... [--serve-by …] and opencode-account add|remove|list. Idempotent nine-step transaction; the handle-file superseded journal makes a crash between tombstone and revoke converge on rerun. Compare/restore read material through the ordinary consumer credential.get with a handle the CLI minted — no new admin op, nothing secret-returning added to the admin surface.
Packages (bun workspace beside the cargo one; CI gets setup-bun): @cortexkit/claustrum-client — detect / identity / wire / errors extracted from anthropic-auth's soak-proven client, policy-free; @cortexkit/opencode-claustrum — the plugin. packages/opencode/golden/{tombstone,handles}.json are the single cross-language source; Rust pins them with include_str!, TS imports them, anthropic-auth vendors them by SHA (cortexkit/anthropic-auth#182).
Verification: scripts/gate.sh green (workspace floor 501 + 57 bun tests + two crash-seam arms; release binary is seam-free). scripts/accept-opencode-custody.sh ran against the live daemon on a scratch XDG home: migrate → real routed request served through the vault → hand-restored key refused as split custody (sentinel never sent) → --restore round-trips the key and revokes the handle; oauth:anthropic* and legacy apikey:* byte-identical throughout. Two defects only the live arm found (OpenCode treats every export of a plugin module as a plugin; "inject nothing" on the split cell let stock OpenCode serve the local key) are fixed with tests.
Out of scope, seams left: OAuth main slots (xai is proven to take the same fetch; anthropic stays with anthropic-auth by serve), Claude Code / Codex.
Design doc: docs/opencode-custody-design.md. Draft until the maintainer has had a look at the admin/CLI surface.
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.
Summary by cubic
Implements #17’s static-key custody slice for OpenCode: type:"api" credentials move from auth.json into the vault, leaving a non-secret tombstone while @cortexkit/opencode-claustrum serves keys through an in-process fetch proxy. Existing installations require migration; unsupported provider shapes and split custody fail closed, while Anthropic OAuth remains owned by anthropic-auth.
CLI and safety
- Adds admin-gated
ck auth migrate-opencodewith dry-run, provider selection, restore, replace, and force-shape modes, plusopencode-account add|remove|list. - Atomically writes mode-0600 auth and handle files, with crash recovery that converges without rotating handles.
- Refuses tombstones as import credentials and validates provider, handle, and secret-bearing file data consistently across Rust and TypeScript.
- Supports ordered account failover, record-version-fenced 401 reporting, 429/402 cooldowns, and same-origin-only redirects.
- Warns when a provider’s tombstoned shape changes, and
--restorereturns the credential toauth.jsonwhile revoking its handle.
Verification
- Adds
@cortexkit/claustrum-client,@cortexkit/opencode-claustrum, hermetic Bun tests, and coverage for the shipped plugin bundle. - Adds Rust CLI and crash-seam tests, Windows CI coverage, and live daemon acceptance tests for migration, serving, split-custody refusal, and restore.