Repository navigation
Conversation
…y api keys
After auth logout, loadWellknownEntry fell back to the KV-cached remote
config and substituted {env:VAR} with env undefined, resolving to "".
The stale document's empty api-key header then overlaid the
still-authenticated endpoint's wiring (later document wins in
mergeHeaders), producing 401s that survived service restarts.
Now an entry whose manifest declares auth but has no usable key
credential contributes no documents, so provider wiring follows the set
of currently authenticated endpoints. A failed refresh with a
credential present still keeps the last resolved config.
Fixes anomalyco#54645
|
The following comment was made by an LLM, it may be inaccurate: |
There was a problem hiding this comment.
I reproduced #54645 on the base with the issue's two-endpoint script against opencode serve from source. After logging out of B, the gateway got x-api-key="" and returned 401, also after a restart. With this PR it gets key-a in both cases. The core config tests and typecheck pass, and the new test fails on the base as it should.
Public API: unchanged.
The fix itself looks right. Two things are worth addressing (see the inline comments):
- Logging out now drops the endpoint's whole cached config, including its allowlist and policies. #53667 deliberately kept that config when the credential was missing, so this needs maintainers to confirm it is intended.
- The new test file is much larger than this fix needs.
| // A failed refresh with a credential present keeps the last resolved config so | ||
| // remote providers and allowlists do not disappear; only removing the source | ||
| // clears it. | ||
| if (auth && !variables) return [] |
There was a problem hiding this comment.
This partly reverts #53667 (f108fa492), which deliberately kept an endpoint's last config even with no credential. Its comment said "including a missing credential", and the aim was that remote providers and enabled_providers/policies would not disappear. After this change, logging out of an endpoint also drops its allowlist and provider-use policies, not just the header that can no longer resolve. That probably matches what logout should mean, but please say so in the description so maintainers can confirm the trade-off. A narrower alternative would keep the cached config and skip only the values whose {env:} cannot be resolved.
There was a problem hiding this comment.
Fair point — stating the trade-off explicitly (also added to the PR description):
What changes: after auth logout, the endpoint contributes no documents, so its cached config — including allowlist and provider-use policies — drops from the effective config until re-login. That is a deliberate narrowing of #53667, not an accident.
Why the split is principled: #53667's intent (a failed refresh must not make remote providers/allowlists disappear) is preserved — when the credential exists but the remote fetch fails, variables is still defined and the cached config keeps being used. The new return [] only fires when there is no credential at all (auth declared, credentials.list empty), i.e. after logout. In that state the endpoint cannot authenticate any request, so keeping its documents only preserves the #54645 shadowing vector: the empty-substituted header overlays the still-authenticated endpoint's wiring (later document wins in mergeHeaders → 401s surviving restarts).
Why not the narrower alternative (keep cached config, skip only the header): the shadowing is not limited to the key header — any field of the stale cached document overlays the live endpoint's — and listing models that cannot authenticate is worse UX than removing them until auth login re-resolves everything (policies included). Logout meaning "the endpoint is gone from the effective config" is the coherent semantic here.
There was a problem hiding this comment.
Thanks, that matches the code. The early return [] only applies when auth is required and there's no usable key, so a failed refresh with a credential still falls back to wellknown.cached. The case #53667 protects is unchanged. With the trade-off now stated in the description, whether logout should also drop the endpoint's policies is a decision for maintainers. I have nothing further on this point.
| return Layer.mergeAll(built, Watcher.testLayer) | ||
| } | ||
|
|
||
| it.live( |
There was a problem hiding this comment.
This file is about 200 lines, mostly a copy of the KV/Credential/Bus setup in config.test.ts, to check one assertion. The changed expectation in config.test.ts (toEqual(removed) after logout) already covers logout dropping the endpoint's documents. Could you either drop this file or turn it into a short two-endpoint case next to the existing test? The aim is to keep the change small.
There was a problem hiding this comment.
Done in 2020b9f — the standalone 197-line file is gone; the regression is now a compact two-endpoint case inside config.test.ts next to the existing logout test, reusing its fixtures. 43/43 tests pass in that file.
There was a problem hiding this comment.
Thanks, I checked 2020b9f. The separate file is gone, and the source change is the same as the one I verified. The new case is shorter, but it's still about 130 lines: it builds its own servers, credential mock and layer rather than reusing the existing logout test's fixtures. It's fine if maintainers are happy with that size. Since the existing test's toEqual(removed) already covers logout, a much smaller case would be enough.
The point I raised on config.ts is still open. Logging out now drops the endpoint's whole cached config, including its enabled_providers and policies. #53667 deliberately kept that config. A sentence in the PR description saying this is intended would help maintainers decide.
…mpact two-endpoint case Replace the standalone 197-line wellknown-stale-wiring.test.ts with a shorter two-endpoint case next to the existing logout test, reusing its fixture pattern. Also documents the logout trade-off: a logged-out endpoint drops its allowlist/provider-use policies too, not just the unresolvable header.
|
Thanks for the review — both addressed in 2020b9f: Test size: dropped the standalone 197-line Revert-of-#53667 trade-off: you're right that this partly reverts f108fa4. The deliberate behavior there (keep last config "including a missing credential") is exactly what produced #54645: after logout, the stale document's |
|
Automated PR Cleanup Thank you for contributing to opencode. Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions. This PR was closed because it matched the following cleanup criteria:
PRs created within the last month are not affected by this cleanup. If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate. Thanks again for taking the time to contribute. |
Issue for this PR
Fixes #54645
Type of change
What does this PR do?
After
auth logout,loadWellknownEntryinpackages/core/src/config.tsfell back to the KV-cached remote config and substituted the manifest's{env:VAR}placeholders withenv: undefined, whichConfigVariable.substituteresolves to"". The stale endpoint document — carrying an emptyx-api-keyheader — was then merged over the still-authenticated endpoint's wiring (later documents win inmergeHeaders), so requests went out with a blank key and 401'd even though another endpoint was still logged in. Restarting didn't help because the cached config survived.The fix: an entry whose manifest declares auth but has no usable key credential now contributes no documents, so provider wiring follows the set of currently authenticated endpoints. A failed refresh with a credential present still keeps the last resolved config — only the logged-out case drops it.
Trade-off (vs #53667): this partly reverts
f108fa492, which deliberately kept an endpoint's last config "including a missing credential" so remote providers andenabled_providers/policies would not disappear. That behavior is exactly what produced #54645: after logout, the stale document's{env:VAR}placeholders resolve to""and — because later documents win inmergeHeaders— shadow the wiring of still-authenticated endpoints, causing 401s that survive restarts. Logging out an endpoint now also drops its allowlist andprovider.usepolicies, not just the unresolvable header. That matches what logout should mean: the endpoint can no longer serve traffic, so neither its wiring nor its policies should linger. A narrower alternative would keep the cached config and skip only the values whose{env:}cannot be resolved; flag it if you prefer that.How did you verify your code works?
packages/core/test/config/config.test.ts("logged-out wellknown endpoint does not shadow still-authenticated endpoints"): spins up two mock well-known endpoints (A and B) wiring a shared provider'sx-api-keyto{env:TOKEN_A}/{env:TOKEN_B}, logs in both, logs out B, and asserts the provider falls back to A's wiring instead of B's empty key. Fails on unfixed code, passes with the fix.bun test test/config/config.test.ts: 43/43 green.oxlintclean on the changed file; per-packagetypecheckpasses.Screenshots / recordings
n/a — no UI change.
Checklist