Skip to content

Serialize shared MCP OAuth credential stores#30292

Merged
stevenlee-oai merged 12 commits into
mainfrom
dev/stevenlee/mcp-oauth-independent-1-store-locks
Jul 7, 2026
Merged

Serialize shared MCP OAuth credential stores#30292
stevenlee-oai merged 12 commits into
mainfrom
dev/stevenlee/mcp-oauth-independent-1-store-locks

Conversation

@stevenlee-oai

@stevenlee-oai stevenlee-oai commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Codex Thread 019edd6d-6f14-74e2-853c-345d1803d4a6

Stack

Review and merge in order. Every layer is independently correct and documents its safe stopping point.

  1. openai/codex#30292 — aggregate File/Secrets store locking
  2. openai/codex#30293 — resolve and lifecycle-pin the exact OAuth store
  3. openai/codex#30416 — serialized authoritative refresh transaction
  4. openai/codex#30294 — Codex-owned transport refresh and one-shot 401 recovery
  5. openai/codex#30295 — login/logout transaction serialization
  6. openai/codex#30296 — diagnostic-only Auto store drift reporting

This PR is layer 1.

Why

MCP OAuth credentials stored in File or Secrets share one aggregate map. Concurrent read-modify-write operations for different MCP servers can both read the same snapshot and let the later write discard the earlier update. That is a correctness problem independent of refresh-token rotation.

What this PR does

  • Adds a bounded cross-process lock around aggregate File and Secrets loads, saves, and deletes.
  • Distinguishes aggregate-lock failures from Secrets backend unavailability, so Auto can fall back only for the latter and cannot bypass serialization by reading or writing File.
  • Keeps Direct keyring operations outside this lock because they are already per credential.
  • Releases the Secrets aggregate lock before legacy File cleanup so cross-store cleanup cannot create nested aggregate-lock ordering.
  • Tests actual contention by waiting for an observed WouldBlock, rather than assuming a sleeping worker reached the lock.
  • Tests load and save with only the Secrets lock path broken while fallback File remains readable and writable.

Decisions and non-goals

  • This lock protects aggregate-store read-modify-write integrity only. It does not choose a credential authority or serialize an OAuth refresh transaction.
  • The lock is scoped to the active CODEX_HOME, matching the aggregate files it protects.
  • Lock waits are bounded, and coordination failures are surfaced rather than treated as evidence that Secrets is unavailable.

Safe stopping point

This PR can merge alone. It prevents lost updates and partial aggregate reads. Auto can still resolve again during a client lifecycle until layer 2, and concurrent refreshes remain possible until layer 3.

Validation

  • just test -p codex-rmcp-client (96 passed; expected environment skips)
  • Focused aggregate File/Secrets lock contention and Auto fallback tests

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5c85609830

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread codex-rs/rmcp-client/src/oauth.rs
@stevenlee-oai stevenlee-oai force-pushed the dev/stevenlee/mcp-oauth-independent-1-store-locks branch 2 times, most recently from c99bdc7 to e4dcca6 Compare June 26, 2026 21:48
@stevenlee-oai stevenlee-oai force-pushed the dev/stevenlee/mcp-oauth-independent-1-store-locks branch from e4dcca6 to f1e1c17 Compare June 26, 2026 22:25
@stevenlee-oai

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f1e1c1711a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread codex-rs/rmcp-client/src/oauth.rs
@stevenlee-oai

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 8f1d2f1ab6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread codex-rs/rmcp-client/src/oauth.rs Outdated

use codex_keyring_store::tests::MockKeyringStore;

#[path = "store_lock_tests.rs"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This should be done on the store_lock module itself, rather than transitively through this test module.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[from Codex] Addressed in 9b7f3b3. The store-lock tests are now included from the store_lock module itself instead of transitively through oauth.rs.

Comment thread codex-rs/rmcp-client/Cargo.toml Outdated
[target.'cfg(any(target_os = "freebsd", target_os = "openbsd"))'.dependencies]
keyring = { workspace = true, features = ["sync-secret-service"] }

# This test is compiled through `#[path]` inside the inline `oauth::tests` module. Cargo-shear

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This should be removeable if you have the test module included in the right place (see other comment).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[from Codex] Addressed in 9b7f3b3. Moving the test module under store_lock made the cargo-shear ignored-paths exception unnecessary, so I removed it.

match file.try_lock() {
Ok(()) => return Ok(Self { _file: file }),
Err(std::fs::TryLockError::WouldBlock) if started.elapsed() >= acquire_timeout => {
anyhow::bail!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can you use thiserror to define an error enum for this module instead of stringifying the failure mode?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[from Codex] Addressed in 9b7f3b3. OAuthStoreLockFailure is now a thiserror enum with explicit CODEX_HOME, create-dir, open, timeout, and lock variants while preserving the existing anyhow downcast behavior used to prevent Auto fallback on coordination failures.

if !reported_contention {
tracing::debug!(
target: LOCK_CONTENTION_EVENT_TARGET,
store = store.description(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: should description just be an impl std::fmt::Display for OauthStore?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[from Codex] Addressed in 9b7f3b3. OAuthStore now implements Display and the lock code uses that instead of a separate description() helper.


impl OAuthStoreLock {
pub(super) fn acquire(store: OAuthStore) -> Result<Self> {
let codex_home = find_codex_home().context(OAuthStoreLockFailure {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Sorry for not raising this earlier (easier to see it's an issue with a smaller more focused change), but how is this going to work in situations where there's no CODEX_HOME? We're generally working to make that possible for other embedders (can share details on slack) so we need to think through how this lock will work in that environment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

From Codex

“no CODEX_HOME env var” is already supported by defaulting to ~/.codex. “No home / no local filesystem authority at all” is not supported by the current File/Secrets credential stores.
For #30292, the new lock is consistent with the existing local store contract: it puts the lock under the same resolved codex home, e.g. ~/.codex/mcp-oauth-locks/.... That means this PR is not introducing the first dependency on codex-home-like local state for File/Secrets mode; the credential stores already have that dependency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[from Codex] Added a code comment in 9b7f3b3 documenting the boundary. This lock intentionally follows the existing local File/Secrets credential-store authority: when CODEX_HOME is unset, those stores already resolve to the default home (~/.codex); when an embedder has no local home/filesystem authority at all, those stores already cannot operate as-is. A future provider-backed credential store should provide its own matching lock authority rather than using this local lock path.

Comment thread codex-rs/rmcp-client/src/oauth/tests/store_lock_tests.rs
@stevenlee-oai

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b7f3b3abb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread codex-rs/rmcp-client/src/oauth.rs
.unwrap_or_else(PoisonError::into_inner);
let dir = tempdir().expect("create CODEX_HOME temp dir");
unsafe {
std::env::set_var("CODEX_HOME", dir.path());

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.

P2 Badge Stop mutating CODEX_HOME in this test helper

Because this helper mutates the process-global CODEX_HOME, it only serializes the new store_lock_tests with this module-local lock; under single-process runners such as Bazel/libtest, the existing oauth.rs tests in the same binary use a different TempCodexHome guard and can run concurrently, so either module can read/write the other temp credential store or remove CODEX_HOME mid-test. Share one serialization guard or pass the home path into the helpers instead.

AGENTS.md reference: AGENTS.md:L210-L214

Useful? React with 👍 / 👎.

@stevenlee-oai

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

Codex Review: Didn't find any major issues. 👍

Reviewed commit: b21923d777

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread codex-rs/rmcp-client/Cargo.toml Outdated
Comment thread codex-rs/rmcp-client/src/oauth/tests/store_lock_tests.rs
Comment thread codex-rs/rmcp-client/src/oauth/store_lock.rs Outdated
Comment thread codex-rs/rmcp-client/src/oauth/store_lock.rs Outdated
@stevenlee-oai

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: ceef76befc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread codex-rs/rmcp-client/src/oauth/store_lock.rs Outdated
@stevenlee-oai

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d5eb78ec3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -0,0 +1,181 @@
//! Cross-process serialization for MCP OAuth stores shared by multiple credentials.

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.

P3 Badge Split the oversized OAuth locking change

This patch is 830 changed lines (git show --numstat: 775 insertions, 55 deletions) and the OAuth locking work is not mechanical, so it crosses the repo's 800-line review-size limit. Please split into a smaller coherent first stage, such as introducing the lock primitive plus File-store serialization and its tests, then follow with Secrets locking and Auto fallback behavior.

AGENTS.md reference: AGENTS.md:L127-L131

Useful? React with 👍 / 👎.

@stevenlee-oai stevenlee-oai enabled auto-merge (squash) July 7, 2026 03:24
@stevenlee-oai stevenlee-oai merged commit 6cf42cf into main Jul 7, 2026
64 of 66 checks passed
@stevenlee-oai stevenlee-oai deleted the dev/stevenlee/mcp-oauth-independent-1-store-locks branch July 7, 2026 04:03
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants