Repository navigation
fix(workspace): provision provider credentials into each OpenCode V2 spawn - #1993
Open
marcusrbrown wants to merge 2 commits into
Open
marcusrbrown wants to merge 2 commits into
marcusrbrown wants to merge 2 commits into
Conversation
…spawn OpenCode V2 imports auth.json only through a migration that a fresh per-spawn database skips, so every spawn started with an empty credential store and each model call went out with no key (#1992). The supervisor now reads the root-only credential blob, POSTs each API-key entry to /api/credential over loopback after the permission gate, and reads GET /api/integration/<provider> back. A configured model whose provider has no credential never turns ready (credential-missing). Also sets OPENCODE_DISABLE_MODELS_FETCH=1, rewords the provider-overlay startup marker, and extends the V2 workspace image smoke test with a no-network fake Anthropic provider.
…te the effective model - The bearer proxy answers 404 without forwarding for the /api/credential group in any spelling (credential.list returns stored keys in the clear). - The credential gate checks the provider of the effective model read from the merged opencode.json the spawn loads, so an overlay-only model is covered. - A service abort during provisioning or the gate is a clean stop, not a gate failure. - The V2 CI credential smoke step installs its cleanup trap before docker run and asserts the :9200 proxy refuses /api/credential.
fro-bot
requested changes
Oct 30, 2026
fro-bot
left a comment
Owner
There was a problem hiding this comment.
Verdict: CONDITIONAL
Blocking issues
| Priority | Location | Issue and required change |
|---|---|---|
| P1 | opencode-proxy-v2.ts:121–140 | Credential-route denial is bypassable. Both http://localhost/api/credential and /api/credential;ignored/../info pass this filter and are forwarded unchanged with privileged Basic authentication. The pinned upstream router strips the absolute URL’s authority and truncates paths at a literal semicolon, respectively, routing both to /api/credential, which returns stored keys. Reject non-origin-form targets and make authorization and forwarding use consistent upstream path semantics. |
| P2 | opencode-credentials-v2.ts:203–205 | The gate rejects valid config-authenticated providers. With a configured model and provider.<id>.options.apiKey supplied through the overlay, OpenCode can authenticate without a stored credential or environment connection. Integration connection listings omit inline settings, so this gate stops every spawn despite usable authentication. Account for effective provider authentication settings without exposing their values; retain refusal for genuinely missing authentication. The previous workspace integration fixture used precisely this inline-key configuration. |
| P2 | opencode-server-v2.ts:392–404 | The effective model is cached across respawns. The agent-writable config is read before the restart loop. If its model changes from provider A to B, a replacement server loads B while the gate still verifies A; readiness can pass with no credential for B. Resolve the effective model and required providers for each fresh spawn, ideally from that server’s effective configuration. |
Non-blocking concerns
None
Missing tests
- Raw-HTTP absolute-form and semicolon-plus-dot-segment targets, asserting rejection without upstream forwarding; include the pinned real server’s routing behavior.
- Inline-key-only provider configuration reaching readiness while genuinely missing authentication remains refused.
- A model-provider change between spawns, verifying that the replacement checks the new provider.
- Post-header stalls during integration JSON consumption and credential-response cancellation, exercising the documented whole-request timeout.
Risk assessment (HIGH): likelihood of regression, security exposure, or blast radius
The proxy bypass exposes all provisioned provider keys to a bearer holder. The gate also introduces availability regressions for valid provider configurations and can falsely report readiness after respawn.
Verification: bun run - executed one inline raw-HTTP proxy probe. Direct /api/credential returned 404; both bypass targets returned the stub upstream’s 200 and arrived with privileged Basic authentication. Protected-route matching was verified against the pinned Effect 4.0.1 router source. No build, lint, type-check, or test-suite commands were run.
Run Summary
| Field | Value |
|---|---|
| Event | pull_request |
| Repository | fro-bot/agent |
| Run ID | 41096194981 |
| Cache | hit |
| Session | ses_e72333843ffeeExlF6wIsRQshY |
| Scope | 17 changed files; relevant callers and pinned upstream source |
| Delivery | Response-file reviews support a body and verdict, so file-specific findings are line-linked above |
| Review tooling | Automated reviewer-return validation unavailable: helper access denied; findings independently checked against source |
5 tasks
This branch has not been deployed
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.
Fixes #1992.
On OpenCode V2, every model call went out without a key and got back a 401. OpenCode V2 imports
auth.jsonin only one place, migration m39. Each spawn gets a fresh database, and a fresh database is bootstrapped by marking every migration applied without running it. So m39 never ran and thecredentialtable stayed empty. I reproduced it on stock v2.2.0 with a fake provider: three requests arrived with nox-api-keyand noauthorization, and the database had 0 credential rows.Fix
workspace-opencode-authsecret V1 uses). It sends each API-key entry toPOST /api/credentialover loopback, authenticated with that spawn's Basic credential and bounded by a timeout.OPENCODE_CONFIG_CONTENT, logs, errors and/readyz.GET /api/integration/<provider>must show acredentialorenvconnection for the configured model's provider. If no model is configured, every provisioned provider is checked. If neither a model nor a blob exists, nothing is checked, so clone-only deployments still boot.credential-missingand readiness never turns ready. Bad credentials now fail at spawn instead of at the first model call.OPENCODE_DISABLE_MODELS_FETCH=1is now in the V2 env, somodels.opencode.aiis no longer fetched; it was blocked by egress. The bundled catalog is used.provider-overlay=merged-into-config-file. It describes what was actually checked;appliedclaimed more than that.x-api-key, the same as V1, so the provider overlay needed no change.Bearer proxy
Upstream
GET /api/credentialreturns stored values in plaintext. Once the store holds a key, anyone with the gateway bearer could read, create or delete provider keys through:9200. The proxy therefore returns 404 forapi/credential, with no upstream call, on every method and sub-path. It runs after the bearer check.The path is percent-decoded up to three rounds. It's split on
/and\, empty,.and;paramsegments are dropped,..is resolved, and the comparison ignores case. On the real binary,/api/CREDENTIAL,/api/%63redential,/api//credentialand/api/credential;x=1all reached the credential routes. A path that can't be decoded, or still changes after three rounds, is refused.Provisioning goes straight to loopback, not through the proxy. With a key provisioned on the real binary, none of these returned it:
/api/provider,/api/model,/api/integration,/api/config, the session and message routes,/api/mcp,/api/skill,/api/commandand/api/project.Effective model
The gate reads
modelfrom the mergedopencode.jsonthe spawn actually loads. That file already applies the env-over-overlay rule, so a model set only through the overlay is gated too. The file is writable by the agent, so root opens it withO_NOFOLLOWand a 512 KiB cap, and parse errors never quote its contents.An abort during provisioning or the gate counts as a clean stop. It doesn't log a gate failure and doesn't go
degraded.CI
The V2 leg of the Workspace Image Smoke Test now sends a real model request. Before, it only checked readiness, which needs no provider. The job name is unchanged.
baseURLat a fake Anthropic-compatible provider running inside the container (--network none). One prompt goes through:9200. The fake must see the dummy key inx-api-key, the key must not appear in the logs, and:9200must return 404 for/api/credentialand three alternative spellings of it./readyz200, and the provider must get zero requests.I ran both steps locally against the fixed image and the unfixed one. Both pass on the fixed image and fail on the unfixed image:
xApiKey: absentand/readyz200.Verification
x-api-keyon every request. With no credential, the spawn stays not-ready and the provider is never called.check-types,lint,build(dist/unchanged),test:scripts,deploy/scripts(152).