OpenCode custody: static-key slots served from the vault through an in-process auth proxy

Merged
IC
2mo ago
+11453-242

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 config hook by injecting { apiKey: sentinel, fetch } into cfg.provider[id].options. Config provider options are re-applied after every auth.loader (provider.ts:1643-1650), so the injected fetch also replaces a shipped plugin's — no fork, no per-provider knowledge.
  • The fetch is 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_failure with 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) AND serve: "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-key never 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.


View with [code]smith Autofix with [code]smith
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-opencode with dry-run, provider selection, restore, replace, and force-shape modes, plus opencode-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 --restore returns the credential to auth.json while 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.