Skip to content

fix(core): drop logged-out wellknown endpoints instead of wiring empty api keys - #54687

Closed
nosmile99 wants to merge 2 commits into
anomalyco:devfrom
nosmile99:stale-wellknown-wiring
Closed

nosmile99 wants to merge 2 commits into
anomalyco:devfrom
nosmile99:stale-wellknown-wiring

Conversation

@nosmile99

@nosmile99 nosmile99 commented Oct 12, 2026 •

Copy link
Copy Markdown

Issue for this PR

Fixes #54645

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

After auth logout, loadWellknownEntry in packages/core/src/config.ts fell back to the KV-cached remote config and substituted the manifest's {env:VAR} placeholders with env: undefined, which ConfigVariable.substitute resolves to "". The stale endpoint document — carrying an empty x-api-key header — was then merged over the still-authenticated endpoint's wiring (later documents win in mergeHeaders), 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 and enabled_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 in mergeHeaders — shadow the wiring of still-authenticated endpoints, causing 401s that survive restarts. Logging out an endpoint now also drops its allowlist and provider.use policies, 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?

  • New regression test in 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's x-api-key to {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.
  • Updated the existing test ("keeps the last resolved wellknown config across failures and restarts while authenticated, drops it after logout") for the new logout behavior.
  • bun test test/config/config.test.ts: 43/43 green. oxlint clean on the changed file; per-package typecheck passes.

Screenshots / recordings

n/a — no UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

…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
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@nosmile99

Copy link
Copy Markdown
Author

Thanks for the review — both addressed in 2020b9f:

Test size: dropped the standalone 197-line wellknown-stale-wiring.test.ts and folded the two-endpoint regression (login A+B, logout B, assert A's wiring survives) into config.test.ts right next to the existing logout test, reusing its fixture pattern (~90 lines, no duplicated KV/Credential/Bus setup). Verified it fails on the unfixed code and passes with the fix; full config.test.ts is 43/43 green, oxlint + typecheck clean.

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 {env:VAR} placeholders resolve to "" and — because later documents win in mergeHeaders — shadow the wiring of still-authenticated endpoints, causing 401s that survive restarts. Keeping the cached config while dropping only the unresolvable header values would fix the 401 but leave the logged-out endpoint's allowlist/provider.use policies attached to a dead endpoint. I've stated this trade-off explicitly in the PR description; happy to go the narrower route if maintainers prefer it.

@github-actions

Copy link
Copy Markdown
Contributor

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:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

providers: stale wellknown API-key wiring in the shared service sends an empty key after auth login/logout

1 participant