Repository navigation
Design: vault custody on OpenCode 2 for providers with no CortexKit auth plugin #5
Description
Follow-up to cortexkit/claustrum#75.
Claustrum's own OpenCode plugin (packages/opencode there) serves vault-held keys on OpenCode 1 for providers that have no CortexKit auth plugin, such as deepseek, minimax, synthetic and xai. On #75 the claustrum maintainer parked that plugin: vault custody for OpenCode is moving into this library, and OpenCode 2 support belongs in that design. They also prefer substituting the key in place over the loopback listener the claustrum draft (cortexkit/claustrum#76) used. So these providers need a home here, and nothing covers them yet: every installOpenCode2Auth binds one adapter.providerID to a provider-specific adapter, and no plugin serves API-key providers that lack a dedicated plugin.
What already fits
Most of it is provider-agnostic already:
src/claustrum: the enrolled consumer, category grants,listScoped/getScopedand scoped failure reporting.ClaustrumFamilywithapiKeys: truealready describes an API-key family, and the vault gives a new credential a<provider>-nativecategory.src/store:api-keyrows with anauthorization-bearerheader kind.src/opencode2: the placeholder credential in OpenCode 2's store, and thehttp.request/experimental.ws.handshakehooks that write the key in place.
In-place substitution and redirects
Writing the key into the request is the shape the claustrum maintainer wants. It hands the key to OpenCode 2's own HTTP client, which follows redirects itself, and a plugin can't stop that: OpenCode 2.0.32 rebuilds the hook's request with HttpClientRequest.fromWeb (packages/core/src/session/model-request.ts:345), which keeps the URL, method, headers and body and drops the redirect mode.
That's safe for these four providers, measured. Through the shipped opencode 2.0.32 binary (embedded Bun 1.4.2), a test plugin set the key header in http.request and a stub answered with 301, 302, 303, 307 and 308 pointing at another origin. In every case Authorization did not reach the target. On a same-origin hop, and with no redirect, it did, so the zero is real. Both runs agreed. All four providers send their key as Authorization: Bearer in OpenCode 2's native drivers (packages/ai/src/providers/deepseek.ts:40, minimax.ts:112, xai.ts:68, and openai-compatible.ts:33 for synthetic). The Fetch standard strips Authorization on a cross-origin redirect and nothing else, so this design depends on the key being in Authorization; a provider that sends its key in any other header needs a redirect check of its own.
Decisions needed
- Where it lives. A small plugin built on this library that installs one adapter per configured provider, or a config-driven API-key adapter inside the library that a host plugin enables.
- Vault model. One enrolled consumer for the plugin, with a category grant per provider (
deepseek-nativeand so on). xai is the odd one out: its vault credential is OAuth, refreshed by the vault, so it needs an OAuth family rather thanapiKeys. - OAuth placeholders. OpenCode 2 core refreshes an OAuth credential when it is within five minutes of expiry (
packages/core/src/integration.ts:733-739). The claustrum draft gave its OAuth placeholder an expiry of 9999-12-31 so core never calls a refresh with it. How does this library's placeholder handle that for xai? - Providers to refuse. The claustrum draft inventoried OpenCode 2's built-in provider plugins that read the stored credential and call out on their own: azure, digitalocean, github-copilot, gitlab, modal, opencode, snowflake-cortex, and the web-search integrations. A placeholder there would be sent outside the hooks. It also refuses an API-key placeholder for openai and xai, because OpenCode 2's voice feature sends a stored key to the realtime API (
packages/core/src/voice.ts:50-69). - Scope. Is it accounts and failover only, with no quota or sidebar, for these providers?
cortexkit/claustrum#76 stays open as a reference. It has the live-run harness and the other OpenCode 2 facts it found: a configured plugin file path is ignored (load from a directory), how opencode auth import and switch write a placeholder, and the refusal inventory with source lines.
Activity