Skip to content

Preserve shared OAuth clients when a replica cannot read them - #922

Open
rudycelekli wants to merge 2 commits into
CopilotKit:mainfrom
rudycelekli:fix/preserve-oauth-client-decryption-fault-20261030
Open

rudycelekli wants to merge 2 commits into
CopilotKit:mainfrom
rudycelekli:fix/preserve-oauth-client-decryption-fault-20261030

Conversation

@rudycelekli

Copy link
Copy Markdown
Contributor

What this changes

A replica that cannot decrypt a stored dynamic OAuth client currently treats it as absent, registers a replacement, and points the shared server row at a client encrypted with its own key. A correctly configured replica then loses access to the client it was using.

Keep read/decryption faults distinct from an absent or revoked credential in both the initial lookup and the lookup under the registration lock. Failed reads now stop the connection attempt before a vendor registration or shared credential change. Connect records the authenticated person and app in a durable mcp.oauth_client_read_failed event, then returns a safe retryable 503. Only typed client-read faults take this path; vendor registration and later writes retain their existing handling. Malformed stored JSON still uses the existing recovery path, with its parsing error contained so plaintext cannot be relayed.

Related: #786, item 3.

Where it runs

  • New state that outlives a request? One read-failure audit row per failed Connect attempt, stored in the existing shared Postgres audit table. No process-local state.
  • What happens on the second replica? Its working client remains available. A failing replica does not replace the shared registration.
  • Anything serialised? Existing registration advisory transaction lock is unchanged. Both reads now raise a safe typed fault before replacement is possible.
  • Anything fanned out to a browser? No new fan-out.
  • New listener, port, or schedule? None.

Boundary and audit

  • Existing connection/gateway boundaries are unchanged.
  • A failed Connect lookup writes a durable mcp.oauth_client_read_failed audit row naming the authenticated actor and server, with fixed operation/reason fields and no thrown error or cause. It performs no registration or vault write.
  • No new client-supplied trust or secret-bearing errors. JSON parsing is separated from decryption and remains contained.

Changelog

  • Added an Unreleased entry and the fault-handling rule in the architecture guide.

Proof

Base: 743649efc3f21d7bf0badbe6af2bcd5daedd97ce. Candidate head: 0dae797a9437fd5aae7eb649b6b377d57cd00248.

  • Two blackbox regressions use real encrypted client rows and a differently keyed store, before and under the registration lock. Before: both fail; each replaces the client and a healthy store can no longer read it. After: both pass, with zero vendor registrations, unchanged pointers, and the original client still readable.
  • All 28 dynamic-client regression tests passed (114 assertions), including two new real-database route checks for durable audit identity, safe 503, unchanged client, and no secret-bearing reader error in the response or audit. Both route checks fail against the preceding unaudited implementation (500). A negative control confirms vendor registration faults are not mislabeled as read failures. Existing first-registration, concurrent-connect, malformed-client recovery, and secret-safe failure controls pass.
  • Format, lint, workspace typecheck, the test-typecheck baseline guard, and build all pass on this exact head. A separate routing candidate at d428f669f10e21700b6d5eb1fb14e90c9cc59cfe, on the same upstream base, ran the full test:ci: 11,017 passed, 210 skipped, two failed (recorder held-steps service-worker timing and concurrent routine-sweep queue expectation). Both cases passed when isolated on unchanged base and that routing candidate; suite-order/timing causality remains unresolved. That full run was not executed on this OAuth head, and is not claimed as a passing full-suite result here.

The targeted tests use a dedicated disposable test database and injected vendor operations; no live vendor credentials or application database are used.

AI assistance: implemented and locally validated with OpenAI Codex. Maintainer approval and hosted CI remain required.

Treat missing or revoked credentials as absent, but propagate read/decryption faults before a dynamic registration can replace the client's shared credential. Keep malformed JSON recovery separate so parsing never leaks a stored secret.

AI assistance: implemented and validated with OpenAI Codex; maintainer review remains required.
Implemented and validated with OpenAI Codex assistance.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant