From d3b98c42bcf917dcfbb1e746fc3a7f2ab36a7f84 Mon Sep 17 00:00:00 2001 From: Joshua Date: Sat, 18 Jul 2026 23:35:47 +0100 Subject: [PATCH] port: cross-instance MCP OAuth locks (upstream 24f9c2a08) Share the mcp-oauth-locks directory across concurrent Codex shadow homes by symlinking it into the shared home, replacing any Codex-created local lock dir. All shadow homes then contend on one file-store lock so OAuth serializes, and a stale local lock cannot permanently block auth. Adapted from upstream to the fork's single-error CodexShadowHomeError model. Co-Authored-By: Claude Fable 5 --- .../provider/Drivers/CodexHomeLayout.test.ts | 40 +++++++++++++++++++ .../src/provider/Drivers/CodexHomeLayout.ts | 15 +++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts index 12e98293b12..beef168894e 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts @@ -112,6 +112,9 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { const sessionsTarget = yield* fileSystem.readLink(path.join(shadowHome, "sessions")); const configTarget = yield* fileSystem.readLink(path.join(shadowHome, "config.toml")); + const mcpOauthLocksTarget = yield* fileSystem.readLink( + path.join(shadowHome, "mcp-oauth-locks"), + ); const modelsCacheExists = yield* fileSystem.exists( path.join(shadowHome, "models_cache.json"), ); @@ -122,12 +125,49 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { expect(sessionsTarget).toBe(path.join(sharedHome, "sessions")); expect(configTarget).toBe(path.join(sharedHome, "config.toml")); + expect(mcpOauthLocksTarget).toBe(path.join(sharedHome, "mcp-oauth-locks")); expect(modelsCacheExists).toBe(false); expect(authLinkResult._tag).toBe("Failure"); expect(authContents).toContain("shadow"); }), ); + it.effect("replaces Codex-created local MCP OAuth locks with the shared lock directory", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const sharedHome = yield* makeTempDir("t3code-codex-shared-"); + const shadowRoot = yield* makeTempDir("t3code-codex-shadow-root-"); + const shadowHome = path.join(shadowRoot, "shadow"); + const sharedLocks = path.join(sharedHome, "mcp-oauth-locks"); + const shadowLocks = path.join(shadowHome, "mcp-oauth-locks"); + + // A stale lock left in the shared dir plus a Codex-created local lock dir. + yield* writeTextFile(path.join(sharedLocks, "file-store.lock"), ""); + yield* writeTextFile(path.join(shadowLocks, "file-store.lock"), ""); + + const layout = yield* resolveCodexHomeLayout( + decodeCodexSettings({ + homePath: sharedHome, + shadowHomePath: shadowHome, + }), + ); + + yield* materializeCodexShadowHome(layout); + + // Local lock dir replaced by a symlink to the shared dir, so all shadow + // homes contend on one file-store lock (serialize) and the stale local + // lock cannot block auth forever. + const locksTarget = yield* fileSystem.readLink(shadowLocks); + const sharedLockExists = yield* fileSystem.exists( + path.join(sharedLocks, "file-store.lock"), + ); + + expect(locksTarget).toBe(sharedLocks); + expect(sharedLockExists).toBe(true); + }), + ); + it.effect("accepts Codex-created shadow-local runtime directories", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.ts index 5a7132224ef..15455d92eff 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.ts @@ -26,10 +26,15 @@ const KNOWN_SHARED_DIRECTORIES = [ "plugins", "cache", "logs", + "mcp-oauth-locks", ] as const; const PRIVATE_ENTRY_NAMES = new Set(["auth.json", "models_cache.json"]); const SHADOW_LOCAL_ENTRY_NAMES = new Set(["log", "memories", "tmp"]); +// Runtime dirs Codex may create locally in the shadow home before we link them; +// replace them with the shared symlink so file-store OAuth locks are shared +// across concurrent shadow homes (and a stale local lock cannot block auth). +const REPLACEABLE_SHARED_RUNTIME_DIRECTORIES = new Set(["mcp-oauth-locks"]); function resolveHomePath(path: Path.Path, value: string | undefined): string { const expanded = @@ -157,9 +162,13 @@ const ensureSymlink = Effect.fn("CodexHomeLayout.ensureSymlink")(function* (inpu const state = yield* readLinkState(input.fileSystem, link); if (state._tag === "NotSymlink") { - return yield* new CodexShadowHomeError({ - detail: `Cannot create Codex shadow home because '${link}' already exists and is not a symlink.`, - }); + if (!REPLACEABLE_SHARED_RUNTIME_DIRECTORIES.has(input.entryName)) { + return yield* new CodexShadowHomeError({ + detail: `Cannot create Codex shadow home because '${link}' already exists and is not a symlink.`, + }); + } + yield* normalizeShadowHomeError(input.fileSystem.remove(link, { recursive: true })); + return yield* normalizeShadowHomeError(input.fileSystem.symlink(target, link)); } if (state._tag === "Missing") {