Repository navigation
Conversation
|
Waiting for This PR needs a linked issue with the |
…back endpoint Remove the provider-serving listener and share reload-safe hook claims. Substitute HTTP material only in Authorization; refuse URLs and other headers. Fail closed on a cached redirect probe and correlate response feedback in place. Keep WebSocket custody and the byte-identical OpenCode 1 bundle.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linked issue
Refs #75
Parked, not for merge. Per the maintainer on #75, vault custody for OpenCode belongs in the shared custody library the provider plugins use (the follow-up design is cortexkit/common-auth#5). This draft stays open as a reference for the OpenCode 2 seam. It now uses the shape the maintainer prefers: the key is substituted in place, with no listener.
What changes
The custody plugin gets an OpenCode 2 entry. The OpenCode 1 entry is untouched: its bundle builds byte-identical (sha256
42607b70…), andserve.tschanges only byexporton three existing helpers.OpenCode 2's native providers never go through the AI SDK
fetch, so the V1 config hook has nothing to hook. The entry uses the session hooks instead:http.request: for a provider the handle manifest assigns to this plugin, the entry writes the vault key into the request'sAuthorizationheader, in place of the placeholder. OpenCode 2 then sends the request with its own client.experimental.ws.handshake(xai's responses route): the same, into the handshake'sAuthorizationheader.http.response: a 401 is reported to the vault with the record version that was actually served, and that account's cached key is dropped. A 429 or 402 puts the account on a cooldown. The response itself passes through untouched.Why only
Authorization. OpenCode 2 follows redirects itself, and a plugin can't stop it: 2.0.32 rebuilds the hook's request withHttpClientRequest.fromWeb, which drops the redirect mode. What keeps the key at the provider's origin is that the runtime'sfetchstripsAuthorizationon a cross-origin redirect, as the Fetch standard requires, and strips nothing else. Measured through the shippedopencode2.0.32 binary (embedded Bun 1.4.2) with a plugin setting headers inhttp.request: on cross-origin 301, 302, 303, 307 and 308,Authorizationnever reached the target, whilex-api-keyandx-goog-api-keydid. So the entry substitutes only intoAuthorizationand refuses a placeholder anywhere else (URL, any other header) withCustodyPlacementError. The custody log row names the refused location in the sink's code grammar (x_api_key,url, orother_header), never a value. deepseek, minimax, xai and synthetic all send their key asAuthorization: Bearerin OpenCode 2's drivers.Startup check. Once per process, before the first substitution, the entry checks that the runtime still behaves this way. It starts two short-lived servers on
127.0.0.1, sends a probeAuthorizationthrough a cross-origin redirect for each of the five redirect statuses and through a WebSocket upgrade answered with a cross-origin 302, waits 150 ms after the WebSocket probe ends so a late follow is still caught, and stops both servers. If the probe header reaches the target, or the probe can't run within two seconds, every owned request is refused withCustodyRedirectUnsafeErrorand nothing is substituted. There's no switch to skip it. The check covers the runtime'sfetchandWebSocket, not OpenCode's own transport, so each OpenCode version still needs the live run below.Unchanged from the earlier rounds: a request for an owned provider without its placeholder is refused as a split, and a placeholder for a provider no handle covers is refused as an orphan. One process-wide registry handles reload overlap, so each request is substituted once, by the newest instance, and its response goes back to the instance that served it.
bun run enroll -- --oc2 --provider <p>writes the placeholder throughopencode auth importandopencode auth switch. It refuses providers whose built-in OpenCode 2 plugin calls out with the stored credential itself, and API-key placeholders for openai and xai (OpenCode 2's voice feature sends a stored key to the realtime API). OAuth placeholders carryexpires9999-12-31, so OpenCode 2 core never tries to refresh them. Install by adding the absolute path ofdist/opencode2toplugins: OpenCode 2 ignores a plugin path that is a file.Known limits, also in
docs/opencode-plugin.md:http.requestandhttp.responsehooks of other plugins can see it. A plugin in the same process is not a trust boundary, as with OpenCode 1.ctx.generate.texton an owned model bypasses session hooks. That request reaches the provider with the placeholder and fails with its 401.enroll --oc2detects and replaces it../serverexport is the serve library, and OpenCode 2 resolves<package>/serverfirst.How it was checked
./scripts/gate.shat the head of this branch (e1834b2, on master9276c73): passed, including the 9 real-daemon e2e arms and the workspace floor (1150, equal to master). Two arms can't run on this machine: the native key store (no dbus/gnome-keyring) and the Windows cross type-check (target not installed). The branch changes no Rust.opencode2.0.32 binary (embedded Bun 1.4.2): isolated HOME and XDG dirs, provider stubs on127.0.0.1, vault credentials holding fake material, outbound HTTPS blocked, plugin loaded fromdist/opencode2. The full suite ran twice at ee0818b. The two placement arms and the startup check ran twice again at e1834b2. Every run agreed.Authorizationheader, no material and no placeholder. Same-origin 307 control: the second hop carried the material.Authorizationand no placeholder.x-api-key, over HTTP and over the WebSocket handshake: refused, 0 stub requests, log rowerrorCode: x_api_key.CustodyRedirectUnsafeErrorin the custody log).expires: 0) failed with an auth error and sent nothing.