Skip to content

Provider API key cannot be rotated in place; Edit silently keeps the expired key #58043

Open

Description

@lieutenantdaily

Provider API key cannot be rotated in place; Edit silently keeps the expired key

Version: 2.0.22 (also present in current v2 HEAD source)
Area: dashboard provider settings + credential.update API

What happens

  1. A provider API key expires (provider returns 401 api_key_expired on model queries).
  2. The dashboard provider card shows unhealthy + "Could not query models".
  3. The user opens Edit, enters the new key, and saves. No error is shown.
  4. Nothing changes: the stored credential keeps the old key, the provider stays unhealthy, and the model inventory stays stale.

Root cause

Key rotation through Edit is structurally impossible in the current contract:

  • Client sends PATCH /api/credential/{id} with { label } only.
  • Server handler (packages/server/src/handlers/credential.ts, credential.update) applies only { label: ctx.payload.label }; any value sent is silently dropped (verified live: PATCH with a value returns 204 and leaves the stored key untouched).
  • API schema (packages/protocol/src/groups/credential.ts) defines the update payload as Schema.Struct({ label: Schema.String }), described literally as "Update a stored credential label."

So there is no in-place key rotation path at all. The only working path is Remove + re-add (create accepts the full value — verified working).

Why this matters

  • A user doing the obvious thing (Edit → paste new key → save) gets silent success with zero effect, while the provider remains broken. There is no error, no hint that Edit cannot touch secrets.
  • The health check ("Could not query models") swallows the provider's actual error (api_key_expired), so nothing points at the expired key.

Suggested fix

Either:

  • Accept an optional value on credential.update (re-verify it against the provider before persisting), and have the dashboard Edit dialog send it; or
  • If in-place rotation is intentionally unsupported, make the Edit dialog explicit (label-only) and add a first-class "Replace key" action (remove + create in one flow), and surface the provider's auth error in the health message instead of the generic "Could not query models".

Repro (local server, no secrets needed)

  1. Create a credential via POST /api/credential (works, persists).
  2. PATCH /api/credential/{id} with {"label": "...", "value": {...new key...}} → 204.
  3. GET /api/credential → the old key is still stored. No error at any step.

Activity

  1. opencode-agent commented on Oct 22, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed write-up and repro!

    You're asking for a way to replace a stored provider credential's API key in place, rather than removing it and adding it again, and for the real auth error (e.g. api_key_expired) to be shown when model listing fails.

    As it stands, credential.update (PATCH /api/credential/{id}) is intentionally label-only. Its schema is { label: string } and it's described as "Update a stored credential label." Extra fields like value are dropped during payload decoding, which is why you get a 204 and the old key stays. So this works as designed today, but the silent no-op is a fair point, and in-place rotation would be a reasonable addition. I've marked this as a feature request for maintainers to consider.

    One note: I couldn't find the provider card with an Edit dialog or the "Could not query models" message in this repository. OpenCode's own app only offers connect, switch account and disconnect for providers. If you're using a third-party or custom dashboard, its Edit dialog is the part that implies the key can be changed. Until then, removing the credential and creating it again (as you found) is the way to rotate a key.

    Related issues:

  2. Bensonluo commented on Oct 22, 2026

    @Bensonluo

    The update endpoint dropped any value payload — the core Credential.update already supported updating it, only the protocol schema and handler didn't pass it through. I verified the silent-drop repro against a local server and opened #58066 to accept an optional value on credential.update.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions