From 25e9fe51f8d54c64b40e37248fa80ff36208830d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 06:37:21 +0000 Subject: [PATCH 1/5] test(server): reproduce issue 8578 auth-like Codex entries leaking into shadow homes Co-Authored-By: Claude Opus 5.5 --- .../provider/Drivers/CodexHomeLayout.test.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts index 093777e67db5d..cf15f79d92800 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts @@ -9,6 +9,7 @@ import { CodexSettings } from "@t3tools/contracts"; import { CodexShadowHomeEntryConflictError, CodexShadowHomePathConflictError, + CodexShadowHomePrivateEntrySymlinkError, materializeCodexShadowHome, resolveCodexHomeLayout, } from "./CodexHomeLayout.ts"; @@ -159,6 +160,82 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { }), ); + it.effect.skipIf(!symlinksSupported)( + "keeps auth-like entries private to each home without touching shared copies", + () => + 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 authLikeNames = [ + "auth-fixture-a.json", + "auth_fixture_b.json", + "auth.json.bak", + "AUTH.Backup", + ]; + + for (const name of authLikeNames) { + yield* writeTextFile(path.join(sharedHome, name), `{"shared":"${name}"}\n`); + } + yield* writeTextFile(path.join(sharedHome, "author.json"), '{"author":true}\n'); + yield* writeTextFile(path.join(shadowHome, "auth.json"), '{"shadow":true}\n'); + + const layout = yield* resolveCodexHomeLayout( + decodeCodexSettings({ homePath: sharedHome, shadowHomePath: shadowHome }), + ); + + yield* materializeCodexShadowHome(layout); + + for (const name of authLikeNames) { + const shadowEntry = yield* fileSystem + .stat(path.join(shadowHome, name)) + .pipe(Effect.result); + expect({ name, tag: shadowEntry._tag }).toEqual({ name, tag: "Failure" }); + expect(yield* fileSystem.readFileString(path.join(sharedHome, name))).toBe( + `{"shared":"${name}"}\n`, + ); + } + expect(yield* fileSystem.readLink(path.join(shadowHome, "author.json"))).toBe( + path.join(sharedHome, "author.json"), + ); + expect(yield* fileSystem.readFileString(path.join(shadowHome, "auth.json"))).toBe( + '{"shadow":true}\n', + ); + }), + ); + + it.effect.skipIf(!symlinksSupported)( + "rejects existing auth-like shadow symlinks even when the shared entry is absent", + () => + 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 orphanLink = path.join(shadowHome, "auth-orphan.json"); + + yield* writeTextFile(path.join(shadowHome, "auth.json"), '{"shadow":true}\n'); + yield* fileSystem.symlink(path.join(sharedHome, "auth-orphan.json"), orphanLink); + + const layout = yield* resolveCodexHomeLayout( + decodeCodexSettings({ homePath: sharedHome, shadowHomePath: shadowHome }), + ); + + const result = yield* materializeCodexShadowHome(layout).pipe(Effect.result); + + expect(result).toMatchObject({ + _tag: "Failure", + failure: { entryName: "auth-orphan.json", path: orphanLink }, + }); + expect(result._tag === "Failure" && result.failure).toBeInstanceOf( + CodexShadowHomePrivateEntrySymlinkError, + ); + }), + ); + it.effect.skipIf(!symlinksSupported)( "replaces a Codex-created local sqlite maintenance lock with the shared lock", () => From e85b25d13d3203be20bb51e6d42590277548f3f3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 06:52:21 +0000 Subject: [PATCH 2/5] fix(server): Codex shadow homes keep auth-like files private Credential backups and alternate auth files such as auth.json.bak or auth-work.json were symlinked from the shared Codex home into every shadow home. Entries matching /^auth(?:$|[._-])/i are now treated as private, and any existing auth-like symlink in a shadow home is rejected, including dangling ones. Regular private files stay, author.json stays shared, and models_cache.json handling is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../src/provider/Drivers/CodexHomeLayout.ts | 56 +++++++++++++------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.ts index b0bb7df4e2d46..7216b5a2c71f5 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.ts @@ -29,6 +29,9 @@ const KNOWN_SHARED_DIRECTORIES = [ ] as const; const PRIVATE_ENTRY_NAMES = new Set(["auth.json", "models_cache.json"]); +// Credential backups and alternate auth files (auth.json.bak, auth-work.json) stay per-home, +// but unrelated names such as author.json remain shared. +const AUTH_LIKE_ENTRY_NAME = /^auth(?:$|[._-])/i; // Finder maintains metadata separately for each directory, including shadow homes. const SHADOW_LOCAL_ENTRY_NAMES = new Set(["log", "memories", "tmp", ".DS_Store"]); // Codex creates these runtime locks itself when it starts in a shadow home before the shared @@ -300,21 +303,38 @@ const ensureShadowAuthIsPrivate = Effect.fn("CodexHomeLayout.ensureShadowAuthIsP readonly effectiveHomePath: string; }): Effect.fn.Return { const path = yield* Path.Path; - const entryName = "auth.json"; - const authPath = path.join(input.effectiveHomePath, entryName); - const state = yield* readLinkState({ - ...input, - entryName, - linkPath: authPath, - }); - if (state._tag === "Symlink") { - return yield* new CodexShadowHomePrivateEntrySymlinkError({ - sharedHomePath: input.sharedHomePath, - effectiveHomePath: input.effectiveHomePath, - entryName, - path: authPath, - }); - } + const shadowEntryNames = yield* input.fileSystem.readDirectory(input.effectiveHomePath).pipe( + Effect.catchTags({ + PlatformError: (cause) => + new CodexShadowHomeFileSystemError({ + sharedHomePath: input.sharedHomePath, + effectiveHomePath: input.effectiveHomePath, + operation: "readDirectory", + path: input.effectiveHomePath, + cause, + }), + }), + ); + yield* Effect.forEach( + shadowEntryNames.filter((entryName) => AUTH_LIKE_ENTRY_NAME.test(entryName)), + Effect.fnUntraced(function* (entryName) { + const authPath = path.join(input.effectiveHomePath, entryName); + const state = yield* readLinkState({ + ...input, + entryName, + linkPath: authPath, + }); + if (state._tag === "Symlink") { + return yield* new CodexShadowHomePrivateEntrySymlinkError({ + sharedHomePath: input.sharedHomePath, + effectiveHomePath: input.effectiveHomePath, + entryName, + path: authPath, + }); + } + }), + { discard: true }, + ); }, ); @@ -373,7 +393,11 @@ export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome" ); const entries = new Set(KNOWN_SHARED_DIRECTORIES); for (const entryName of sharedEntryNames) { - if (!PRIVATE_ENTRY_NAMES.has(entryName) && !SHADOW_LOCAL_ENTRY_NAMES.has(entryName)) { + if ( + !PRIVATE_ENTRY_NAMES.has(entryName) && + !SHADOW_LOCAL_ENTRY_NAMES.has(entryName) && + !AUTH_LIKE_ENTRY_NAME.test(entryName) + ) { entries.add(entryName); } } From 488744dcd0bd3960ea11194d5b10aaafb046cac2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 01:32:15 +0000 Subject: [PATCH 3/5] test(server): stale auth.lock shadow links are removed, not rejected AI-assisted; verified with the tests and tools listed above. --- apps/server/src/provider/Drivers/CodexHomeLayout.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts index 1acb9844efba5..9d2f2a87db824 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts @@ -180,6 +180,7 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { "auth_fixture_b.json", "auth.json.bak", "AUTH.Backup", + "auth.lock", ]; for (const name of authLikeNames) { @@ -187,6 +188,11 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { } yield* writeTextFile(path.join(sharedHome, "author.json"), '{"author":true}\n'); yield* writeTextFile(path.join(shadowHome, "auth.json"), '{"shadow":true}\n'); + // Older shadow homes linked auth.lock; that stale link is removed, not rejected. + yield* fileSystem.symlink( + path.join(sharedHome, "auth.lock"), + path.join(shadowHome, "auth.lock"), + ); const layout = yield* resolveCodexHomeLayout( decodeCodexSettings({ homePath: sharedHome, shadowHomePath: shadowHome }), From b0c255fac69fcdb6eaafd0dfbe86768e541e4d4a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 01:43:46 +0000 Subject: [PATCH 4/5] fix(server): remove auth-like shadow links older versions made instead of failing AI-assisted; verified with the tests and tools listed above. --- .../provider/Drivers/CodexHomeLayout.test.ts | 16 +++++---- .../src/provider/Drivers/CodexHomeLayout.ts | 33 +++++++++++++++---- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts index 9d2f2a87db824..e9bfbde2207e8 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts @@ -188,11 +188,10 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { } yield* writeTextFile(path.join(sharedHome, "author.json"), '{"author":true}\n'); yield* writeTextFile(path.join(shadowHome, "auth.json"), '{"shadow":true}\n'); - // Older shadow homes linked auth.lock; that stale link is removed, not rejected. - yield* fileSystem.symlink( - path.join(sharedHome, "auth.lock"), - path.join(shadowHome, "auth.lock"), - ); + // Older versions linked these into shadow homes; those links are removed, not rejected. + for (const name of ["auth.lock", "auth.json.bak"]) { + yield* fileSystem.symlink(path.join(sharedHome, name), path.join(shadowHome, name)); + } const layout = yield* resolveCodexHomeLayout( decodeCodexSettings({ homePath: sharedHome, shadowHomePath: shadowHome }), @@ -219,7 +218,7 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { ); it.effect.skipIf(!symlinksSupported)( - "rejects existing auth-like shadow symlinks even when the shared entry is absent", + "rejects auth-like shadow symlinks that point outside the shared home, even dangling ones", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; @@ -230,7 +229,10 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { const orphanLink = path.join(shadowHome, "auth-orphan.json"); yield* writeTextFile(path.join(shadowHome, "auth.json"), '{"shadow":true}\n'); - yield* fileSystem.symlink(path.join(sharedHome, "auth-orphan.json"), orphanLink); + yield* fileSystem.symlink( + path.join(shadowRoot, "elsewhere", "auth-orphan.json"), + orphanLink, + ); const layout = yield* resolveCodexHomeLayout( decodeCodexSettings({ homePath: sharedHome, shadowHomePath: shadowHome }), diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.ts index 56d2c0a2633b4..08d2ff5f57f91 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.ts @@ -353,14 +353,33 @@ const ensureShadowAuthIsPrivate = Effect.fn("CodexHomeLayout.ensureShadowAuthIsP entryName, linkPath: authPath, }); - if (state._tag === "Symlink") { - return yield* new CodexShadowHomePrivateEntrySymlinkError({ - sharedHomePath: input.sharedHomePath, - effectiveHomePath: input.effectiveHomePath, - entryName, - path: authPath, - }); + if (state._tag !== "Symlink") return; + // Older versions linked auth-like backups to the shared copy; drop those, reject the rest. + const sharedEntryPath = path.join(input.sharedHomePath, entryName); + if ( + entryName !== "auth.json" && + path.resolve(path.dirname(authPath), state.target) === sharedEntryPath + ) { + return yield* input.fileSystem.remove(authPath).pipe( + Effect.catchTags({ + PlatformError: (cause) => + new CodexShadowHomeFileSystemError({ + sharedHomePath: input.sharedHomePath, + effectiveHomePath: input.effectiveHomePath, + operation: "remove", + path: authPath, + entryName, + cause, + }), + }), + ); } + return yield* new CodexShadowHomePrivateEntrySymlinkError({ + sharedHomePath: input.sharedHomePath, + effectiveHomePath: input.effectiveHomePath, + entryName, + path: authPath, + }); }), { discard: true }, ); From 28651d331720b6d27aaa708e61315cfd56e41fa4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 01:55:42 +0000 Subject: [PATCH 5/5] fix(server): drop any auth-like shadow link except auth.json instead of failing AI-assisted; verified with the tests and tools listed above. --- .../src/provider/Drivers/CodexHomeLayout.test.ts | 14 ++++---------- .../server/src/provider/Drivers/CodexHomeLayout.ts | 9 +++------ 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts index e9bfbde2207e8..f61d46d3e8e9f 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts @@ -9,7 +9,6 @@ import { CodexSettings } from "@t3tools/contracts"; import { CodexShadowHomeEntryConflictError, CodexShadowHomePathConflictError, - CodexShadowHomePrivateEntrySymlinkError, materializeCodexShadowHome, resolveCodexHomeLayout, } from "./CodexHomeLayout.ts"; @@ -218,7 +217,7 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { ); it.effect.skipIf(!symlinksSupported)( - "rejects auth-like shadow symlinks that point outside the shared home, even dangling ones", + "removes auth-like shadow symlinks that point outside the shared home, even dangling ones", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; @@ -238,15 +237,10 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { decodeCodexSettings({ homePath: sharedHome, shadowHomePath: shadowHome }), ); - const result = yield* materializeCodexShadowHome(layout).pipe(Effect.result); + yield* materializeCodexShadowHome(layout); - expect(result).toMatchObject({ - _tag: "Failure", - failure: { entryName: "auth-orphan.json", path: orphanLink }, - }); - expect(result._tag === "Failure" && result.failure).toBeInstanceOf( - CodexShadowHomePrivateEntrySymlinkError, - ); + const orphanLinkResult = yield* fileSystem.readLink(orphanLink).pipe(Effect.result); + expect(orphanLinkResult._tag).toBe("Failure"); }), ); diff --git a/apps/server/src/provider/Drivers/CodexHomeLayout.ts b/apps/server/src/provider/Drivers/CodexHomeLayout.ts index 08d2ff5f57f91..1c125be18da7f 100644 --- a/apps/server/src/provider/Drivers/CodexHomeLayout.ts +++ b/apps/server/src/provider/Drivers/CodexHomeLayout.ts @@ -354,12 +354,9 @@ const ensureShadowAuthIsPrivate = Effect.fn("CodexHomeLayout.ensureShadowAuthIsP linkPath: authPath, }); if (state._tag !== "Symlink") return; - // Older versions linked auth-like backups to the shared copy; drop those, reject the rest. - const sharedEntryPath = path.join(input.sharedHomePath, entryName); - if ( - entryName !== "auth.json" && - path.resolve(path.dirname(authPath), state.target) === sharedEntryPath - ) { + // Older versions linked auth-like backups into shadow homes. A link holds no data, + // so drop it; only a linked auth.json means the shadow home has no credentials of its own. + if (entryName !== "auth.json") { return yield* input.fileSystem.remove(authPath).pipe( Effect.catchTags({ PlatformError: (cause) =>