From 6184ba75497c4cc7d7f09b09f086bff9370029a5 Mon Sep 17 00:00:00 2001 From: Josh Robins <90800168+JRobzs@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:17:36 -0500 Subject: [PATCH 1/4] fix(server): start Codex tools in symlinked workspaces --- .../Adapters/CodexAdapterV2.test.ts | 218 +++++++++++++++++- .../Adapters/CodexAdapterV2.ts | 36 ++- 2 files changed, 241 insertions(+), 13 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 4471be814bf39..83a1a0395e89e 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -6,6 +6,9 @@ import { } from "./ProviderTextDeltaCoalescer.ts"; import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; +import * as HostProcess from "@t3tools/shared/HostProcess"; +// @effect-diagnostics-next-line nodeBuiltinImport:off -- Directory junctions keep these fixtures available on Windows. +import * as NodeFSP from "node:fs/promises"; import { CommandId, CheckpointId, @@ -1174,6 +1177,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { onRequest: (method: string, params: unknown) => Effect.Effect = () => Effect.void, readChildMetadata?: Parameters[2], beforeEmitInbound?: CodexReplay.CodexAppServerReplayDriver["beforeEmitInbound"], + runtimePolicy = CODEX_TEST_RUNTIME_POLICY, ) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; @@ -1247,12 +1251,12 @@ describe("CodexAdapterV2 post-settle continuation", () => { threadId, providerSessionId: ProviderSessionId.make(`provider-session-${transcript.scenario}`), modelSelection: CODEX_TEST_MODEL_SELECTION, - runtimePolicy: CODEX_TEST_RUNTIME_POLICY, + runtimePolicy, }); const providerThread = yield* runtime.ensureThread({ threadId, modelSelection: CODEX_TEST_MODEL_SELECTION, - runtimePolicy: CODEX_TEST_RUNTIME_POLICY, + runtimePolicy, }); const events: Array = []; const firstTerminal = yield* Deferred.make(); @@ -1527,6 +1531,204 @@ describe("CodexAdapterV2 post-settle continuation", () => { ), ); + it.effect.each(["darwin", "win32"] as const)( + "resolves the Codex workspace per platform (%s) without changing the registered path", + (platform) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-symlink-" }); + const target = `${root}/physical`; + const alias = `${root}/alias`; + yield* fs.makeDirectory(target); + yield* Effect.tryPromise(() => NodeFSP.symlink(target, alias, "junction")); + const expectedCwd = platform === "win32" ? alias : yield* fs.realPath(target); + const policy = { ...CODEX_TEST_RUNTIME_POLICY, runtimeMode: "auto" as const, cwd: alias }; + const requests: Array = []; + const preamble = codexReplayPreamble({ + nativeThreadId: "physical-workspace", + nativeTurnId: "physical-turn", + prompt: "Inspect the workspace.", + }); + const entries: Array = [ + ...preamble.slice(0, 5), + { + type: "expect_outbound", + label: "resume alias", + frame: { + id: 3, + method: "thread/resume", + params: { + threadId: "physical-workspace", + excludeTurns: true, + config: CodexAdapterV2.CODEX_THREAD_CONFIG, + }, + }, + }, + { + type: "emit_inbound", + label: "resume alias", + frame: { + id: 3, + result: { thread: { id: "physical-workspace", updatedAt: 1782622440 } }, + }, + }, + { + type: "expect_outbound", + label: "turn/start", + frame: { + id: 4, + method: "turn/start", + params: { + threadId: "physical-workspace", + input: [{ type: "text", text: "Inspect the workspace." }], + cwd: "/workspace", + model: "gpt-5.4", + approvalPolicy: "on-request", + approvalsReviewer: "auto_review", + sandboxPolicy: { type: "workspaceWrite" }, + summary: "detailed", + collaborationMode: { mode: "default", settings: { model: "gpt-5.4" } }, + }, + }, + }, + ...preamble + .slice(6) + .map((entry) => + entry.type !== "runtime_exit" && Predicate.hasProperty(entry.frame, "id") + ? withReplayRequestId(entry, 4) + : entry, + ), + ]; + const transcript = yield* decodeReplayTranscriptJson( + (yield* encodeReplayTranscriptJson( + makeCodexReplayTranscript({ + scenario: `physical-workspace-${platform}`, + entries, + }), + )).replaceAll( + yield* encodeStringJson("/workspace"), + yield* encodeStringJson(expectedCwd), + ), + ); + const harness = yield* makeCodexReplayHarness( + transcript, + () => Effect.void, + (method, params) => + Effect.sync(() => { + if (method === "thread/start" || method === "thread/resume") requests.push(params); + }), + undefined, + undefined, + policy, + ); + yield* harness.runtime.resumeThread({ + providerThread: harness.providerThread, + runtimePolicy: policy, + modelSelection: CODEX_TEST_MODEL_SELECTION, + }); + assert.lengthOf(requests, 2); + for (const request of requests) assert.propertyVal(request, "cwd", expectedCwd); + yield* harness.runtime.startTurn({ + ...makeCodexTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make("physical-attempt"), + text: "Inspect the workspace.", + }), + runtimePolicy: policy, + }); + assert.equal(policy.cwd, alias); + assert.equal(harness.runtime.providerSession.cwd, alias); + }).pipe( + Effect.scoped, + Effect.provideService(HostProcess.Platform, platform), + Effect.provide( + Layer.mergeAll( + IdAllocator.layer, + McpProviderSessions.layer, + NodeServices.layer, + NodeCrypto.layer, + ), + ), + ), + ); + + it.effect("follows a workspace alias retargeted between Codex turns", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-retarget-" }); + const alias = `${root}/alias`; + const targets = [`${root}/first`, `${root}/second`]; + for (const target of targets) yield* fs.makeDirectory(target); + yield* Effect.tryPromise(() => NodeFSP.symlink(targets[0]!, alias, "junction")); + const entries: Array = []; + for (const [index, target] of targets.entries()) { + const preamble = codexReplayPreamble({ + nativeThreadId: "retargeted-workspace", + nativeTurnId: `retargeted-turn-${index}`, + prompt: `Inspect workspace ${index}.`, + }); + const transcript = yield* decodeReplayTranscriptJson( + (yield* encodeReplayTranscriptJson( + makeCodexReplayTranscript({ + scenario: "retargeted-workspace", + entries: + index === 0 + ? preamble + : preamble + .slice(5) + .map((entry) => + entry.type !== "runtime_exit" && Predicate.hasProperty(entry.frame, "id") + ? withReplayRequestId(entry, 4) + : entry, + ), + }), + )).replaceAll( + yield* encodeStringJson("/workspace"), + yield* encodeStringJson(yield* fs.realPath(target)), + ), + ); + entries.push(...transcript.entries); + } + const policy = { ...CODEX_TEST_RUNTIME_POLICY, cwd: alias }; + const harness = yield* makeCodexReplayHarness( + makeCodexReplayTranscript({ scenario: "retargeted-workspace", entries }), + undefined, + undefined, + undefined, + undefined, + policy, + ); + for (const [index, target] of targets.entries()) { + yield* fs.remove(alias); + yield* Effect.tryPromise(() => NodeFSP.symlink(target, alias, "junction")); + yield* harness.runtime.startTurn({ + ...makeCodexTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make(`retargeted-attempt-${index}`), + text: `Inspect workspace ${index}.`, + }), + runtimePolicy: policy, + }); + } + assert.equal(policy.cwd, alias); + }).pipe( + Effect.scoped, + Effect.provideService(HostProcess.Platform, "darwin"), + Effect.provide( + Layer.mergeAll( + IdAllocator.layer, + McpProviderSessions.layer, + NodeServices.layer, + NodeCrypto.layer, + ), + ), + ), + ); + it.effect("adds the session MCP server without replacing launch-argument servers", () => Effect.gen(function* () { const scenario = "launch-mcp-servers"; @@ -3159,7 +3361,9 @@ describe("CodexAdapterV2 post-settle continuation", () => { if (outcome === "recover") { const fs = yield* FileSystem.FileSystem; - const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-capacity-retry-" }); + const cwd = yield* fs.realPath( + yield* fs.makeTempDirectoryScoped({ prefix: "t3-capacity-retry-" }), + ); const localTranscript = yield* decodeReplayTranscriptJson( (yield* encodeReplayTranscriptJson( makeCodexReplayTranscript({ scenario, entries }), @@ -4417,7 +4621,9 @@ describe("CodexAdapterV2 post-settle continuation", () => { return Effect.scoped( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bg-stop-workspace-" }); + const cwd = yield* fs.realPath( + yield* fs.makeTempDirectoryScoped({ prefix: "t3-bg-stop-workspace-" }), + ); const localTranscript = yield* decodeReplayTranscriptJson( (yield* encodeReplayTranscriptJson(transcript)).replaceAll( yield* encodeStringJson("/workspace"), @@ -4544,7 +4750,9 @@ describe("CodexAdapterV2 post-settle continuation", () => { Effect.scoped( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bg-stale-workspace-" }); + const cwd = yield* fs.realPath( + yield* fs.makeTempDirectoryScoped({ prefix: "t3-bg-stale-workspace-" }), + ); const staleTranscript = makeCodexReplayTranscript({ scenario: "codex-bg-stop-untracked", entries: [ diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 688de092c03d0..3be1159db2558 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -1685,6 +1685,18 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( const { clientFactory, digest, fileSystem, idAllocator, serverConfig } = adapterOptions; const continuationRequests = adapterOptions.continuationRequests; + const resolveCodexRuntimePolicy = Effect.fn("CodexAdapterV2.resolveCodexRuntimePolicy")( + function* (policy: ProviderAdapter.ProviderAdapterV2RuntimePolicy) { + const cwd = policy.cwd; + // Windows realpath can rewrite mapped drives to network paths; preserve that platform's cwd. + if (cwd === null || (yield* HostProcess.Platform) === "win32") return policy; + return { + ...policy, + cwd: yield* fileSystem.realPath(cwd).pipe(Effect.orElseSucceed(() => cwd)), + }; + }, + ); + return ProviderAdapter.ProviderAdapterV2.of({ instanceId: adapterOptions.instanceId, driver: CODEX_PROVIDER, @@ -6750,7 +6762,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( const turnStartParams = yield* buildCodexTurnStartParams({ nativeThreadId: threadId, codexInput, - runtimePolicy: turnInput.runtimePolicy, + runtimePolicy: yield* resolveCodexRuntimePolicy(turnInput.runtimePolicy), modelSelection: turnInput.modelSelection, hasT3Mcp: mcpSession !== undefined, browserToolsAvailable: mcpSession?.browserToolsAvailable ?? true, @@ -7056,8 +7068,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( } return false; }), - ensureThread: (threadInput) => - ensureInitialized.pipe( + ensureThread: Effect.fn("CodexAdapterV2.ensureThread")(function* (threadInput) { + const runtimePolicy = yield* resolveCodexRuntimePolicy(threadInput.runtimePolicy); + return yield* ensureInitialized.pipe( Effect.andThen(mcpSessions.read(threadInput.threadId)), Effect.flatMap((mcpSession) => client.request( @@ -7066,7 +7079,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( threadId: threadInput.threadId, mcpSession, modelSelection: threadInput.modelSelection, - runtimePolicy: threadInput.runtimePolicy, + runtimePolicy, }), ), ), @@ -7088,7 +7101,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( cause, }), ), - ), + ); + }), resumeThread: Effect.fn("CodexAdapterV2.resumeThread")( function* (threadInput) { const nativeThreadId = yield* getNativeThreadId(threadInput.providerThread); @@ -7103,7 +7117,9 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( : { modelSelection: threadInput.modelSelection }), ...(threadInput.runtimePolicy === undefined ? {} - : { runtimePolicy: threadInput.runtimePolicy }), + : { + runtimePolicy: yield* resolveCodexRuntimePolicy(threadInput.runtimePolicy), + }), }), }); const response = yield* ensureInitialized.pipe( @@ -7867,7 +7883,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( threadId: threadInput.providerThread.appThreadId, mcpSession: yield* readMcpSession(threadInput.providerThread.appThreadId), modelSelection: input.modelSelection, - runtimePolicy: input.runtimePolicy, + runtimePolicy: yield* resolveCodexRuntimePolicy(input.runtimePolicy), }), }); } @@ -7924,7 +7940,11 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( : { modelSelection: threadInput.modelSelection }), ...(threadInput.runtimePolicy === undefined ? {} - : { runtimePolicy: threadInput.runtimePolicy }), + : { + runtimePolicy: yield* resolveCodexRuntimePolicy( + threadInput.runtimePolicy, + ), + }), }), }), ), From f83734425f196be37fede29ffeb584d32f0eb053 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 21:24:02 +0000 Subject: [PATCH 2/4] fix(server): resolve the Codex turn cwd once per user turn Capacity retries and goal continuations reuse the turn input, so they now carry the already-resolved cwd instead of touching the filesystem again between a retry and its turn/start. AI-assisted; verified with the tests and tools listed above. --- .../Adapters/CodexAdapterV2.ts | 81 ++++++++++--------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 31b69d254e7d4..8d62ec2b8f27c 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -7269,9 +7269,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( const threadId = yield* getNativeThreadId(turnInput.providerThread); const mcpSession = yield* mcpSessions.read(turnInput.threadId); const models = adapterOptions.models === undefined ? [] : yield* adapterOptions.models; - const runtimePolicy = yield* resolveCodexRuntimePolicy(turnInput.runtimePolicy); const configuredReasoningSummary = yield* client - .request("config/read", { cwd: runtimePolicy.cwd, includeLayers: false }) + .request("config/read", { cwd: turnInput.runtimePolicy.cwd, includeLayers: false }) .pipe( Effect.map((response) => response.config.model_reasoning_summary ?? undefined), Effect.catch(() => @@ -7282,7 +7281,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( cyberAccess: codexCyberAccess(models, turnInput.modelSelection.model), nativeThreadId: threadId, codexInput, - runtimePolicy, + runtimePolicy: turnInput.runtimePolicy, modelSelection: turnInput.modelSelection, hasT3Mcp: mcpSession !== undefined, browserToolsAvailable: mcpSession?.browserToolsAvailable ?? true, @@ -7750,41 +7749,47 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( }), ), ), - startTurn: (turnInput) => { - const goalCommand = - turnInput.restartContinuationOfRunId === undefined - ? parseGoalCommand(turnInput.message) - : null; - if (goalCommand !== null) return runGoalCommand(turnInput, goalCommand); - const nativeThreadId = turnInput.providerThread.nativeThreadRef?.nativeId; - return Effect.gen(function* () { - if ( - turnInput.restartContinuationOfRunId === undefined && - nativeThreadId != null && - goalsPausedForWake.has(nativeThreadId) && - goalsByNativeThread.get(nativeThreadId)?.status === "paused" - ) { - return yield* activateGoal(turnInput, nativeThreadId, toCodexInput(turnInput)); - } - yield* startNativeTurn( - turnInput, - turnInput.restartContinuationOfRunId === undefined - ? yield* toCodexInput(turnInput) - : [], - ); - }).pipe( - Effect.mapError( - (cause) => - new ProviderAdapter.ProviderAdapterTurnStartError({ - driver: CODEX_PROVIDER, - threadId: turnInput.threadId, - providerThreadId: turnInput.providerThread.id, - runId: turnInput.runId, - cause, - }), - ), - ); - }, + // Resolved once per turn so capacity retries and goal continuations reuse the physical cwd. + startTurn: (requestedTurnInput) => + Effect.flatMap( + resolveCodexRuntimePolicy(requestedTurnInput.runtimePolicy), + (runtimePolicy) => { + const turnInput = { ...requestedTurnInput, runtimePolicy }; + const goalCommand = + turnInput.restartContinuationOfRunId === undefined + ? parseGoalCommand(turnInput.message) + : null; + if (goalCommand !== null) return runGoalCommand(turnInput, goalCommand); + const nativeThreadId = turnInput.providerThread.nativeThreadRef?.nativeId; + return Effect.gen(function* () { + if ( + turnInput.restartContinuationOfRunId === undefined && + nativeThreadId != null && + goalsPausedForWake.has(nativeThreadId) && + goalsByNativeThread.get(nativeThreadId)?.status === "paused" + ) { + return yield* activateGoal(turnInput, nativeThreadId, toCodexInput(turnInput)); + } + yield* startNativeTurn( + turnInput, + turnInput.restartContinuationOfRunId === undefined + ? yield* toCodexInput(turnInput) + : [], + ); + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapter.ProviderAdapterTurnStartError({ + driver: CODEX_PROVIDER, + threadId: turnInput.threadId, + providerThreadId: turnInput.providerThread.id, + runId: turnInput.runId, + cause, + }), + ), + ); + }, + ), steerTurn: Effect.fn("CodexAdapterV2.steerTurn")( function* (turnInput) { const threadId = yield* getNativeThreadId(turnInput.providerThread); From 017d9e0b123833e2713b482d8948cfbc2683b202 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 21:26:26 +0000 Subject: [PATCH 3/4] test(server): create Codex workspace aliases through FileSystem AI-assisted; verified with the tests and tools listed above. --- .../src/orchestration-v2/Adapters/CodexAdapterV2.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 237a66b4ffe63..0b3ed16ed3503 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -6,8 +6,6 @@ import { } from "./ProviderTextDeltaCoalescer.ts"; import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; -// @effect-diagnostics-next-line nodeBuiltinImport:off -- Directory junctions keep these fixtures available on Windows. -import * as NodeFSP from "node:fs/promises"; import { CommandId, CheckpointId, @@ -1772,7 +1770,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { const target = `${root}/physical`; const alias = `${root}/alias`; yield* fs.makeDirectory(target); - yield* Effect.tryPromise(() => NodeFSP.symlink(target, alias, "junction")); + yield* fs.symlink(target, alias); const expectedCwd = platform === "win32" ? alias : yield* fs.realPath(target); const policy = { ...CODEX_TEST_RUNTIME_POLICY, runtimeMode: "auto" as const, cwd: alias }; const requests: Array = []; @@ -1897,7 +1895,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { const alias = `${root}/alias`; const targets = [`${root}/first`, `${root}/second`]; for (const target of targets) yield* fs.makeDirectory(target); - yield* Effect.tryPromise(() => NodeFSP.symlink(targets[0]!, alias, "junction")); + yield* fs.symlink(targets[0]!, alias); const entries: Array = []; for (const [index, target] of targets.entries()) { const preamble = codexReplayPreamble({ @@ -1942,7 +1940,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { ); for (const [index, target] of targets.entries()) { yield* fs.remove(alias); - yield* Effect.tryPromise(() => NodeFSP.symlink(target, alias, "junction")); + yield* fs.symlink(target, alias); yield* harness.runtime.startTurn({ ...makeCodexTestTurnInput({ threadId: harness.threadId, From 4eff224c7be2bddd6284e08c0b97b171204a10cb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 21:40:01 +0000 Subject: [PATCH 4/4] fix(server): keep Codex project trust on the registered workspace path Resolving the cwd for every Codex request moved threads off the path the user trusted, so Codex skipped project config, hooks and exec policies. Threads and config reads now keep the registered path; only a macOS workspace-write turn, where Seatbelt rejects a symlinked writable root, runs in the physical directory. AI-assisted; verified with the tests and tools listed above. --- .../Adapters/CodexAdapterV2.test.ts | 163 ++++++------------ .../Adapters/CodexAdapterV2.ts | 130 +++++++------- 2 files changed, 120 insertions(+), 173 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 0b3ed16ed3503..78622ef75b743 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -748,7 +748,7 @@ function codexReplayPreamble(input: { ]; } -/** Every turn/start follows a config/read; inserts that exchange and shifts later client ids. */ +/** Every turn/start follows a config/read; inserts any missing exchange and shifts later client ids. */ function withConfigReadBeforeTurnStart( entries: ReadonlyArray, ): Array { @@ -766,7 +766,9 @@ function withConfigReadBeforeTurnStart( offset = 0; clientIds = new Map(); } - if (frame.method === "turn/start") { + const previous = result.at(-1); + const hasConfigRead = previous?.type === "emit_inbound" && previous.label === "config/read"; + if (frame.method === "turn/start" && !hasConfigRead) { const id = frame.id + offset; const params = Predicate.isObject(frame.params) ? frame.params : {}; result.push( @@ -1761,9 +1763,13 @@ describe("CodexAdapterV2 post-settle continuation", () => { ), ); - it.effect.each(["darwin", "win32"] as const)( - "resolves the Codex workspace per platform (%s) without changing the registered path", - (platform) => + it.effect.each([ + { platform: "darwin", runtimeMode: "auto", resolves: true }, + { platform: "darwin", runtimeMode: "full-access", resolves: false }, + { platform: "win32", runtimeMode: "auto", resolves: false }, + ] as const)( + "runs $platform $runtimeMode Codex turns in the physical workspace: $resolves", + ({ platform, runtimeMode, resolves }) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-symlink-" }); @@ -1771,8 +1777,20 @@ describe("CodexAdapterV2 post-settle continuation", () => { const alias = `${root}/alias`; yield* fs.makeDirectory(target); yield* fs.symlink(target, alias); - const expectedCwd = platform === "win32" ? alias : yield* fs.realPath(target); - const policy = { ...CODEX_TEST_RUNTIME_POLICY, runtimeMode: "auto" as const, cwd: alias }; + const turnCwd = resolves ? yield* fs.realPath(target) : alias; + const policy = { ...CODEX_TEST_RUNTIME_POLICY, runtimeMode, cwd: alias }; + const turnDefaults = + runtimeMode === "auto" + ? { + approvalPolicy: "on-request", + approvalsReviewer: "auto_review", + sandboxPolicy: { type: "workspaceWrite" }, + } + : { + approvalPolicy: "never", + approvalsReviewer: "user", + sandboxPolicy: { type: "dangerFullAccess" }, + }; const requests: Array = []; const preamble = codexReplayPreamble({ nativeThreadId: "physical-workspace", @@ -1802,20 +1820,33 @@ describe("CodexAdapterV2 post-settle continuation", () => { result: { thread: { id: "physical-workspace", updatedAt: 1782622440 } }, }, }, + // Config reads keep the registered path, as the thread requests do. { type: "expect_outbound", - label: "turn/start", + label: "config/read", frame: { id: 4, + method: "config/read", + params: { cwd: "/workspace", includeLayers: false }, + }, + }, + { + type: "emit_inbound", + label: "config/read", + frame: { id: 4, result: { config: {}, origins: {} } }, + }, + { + type: "expect_outbound", + label: "turn/start", + frame: { + id: 5, method: "turn/start", params: { threadId: "physical-workspace", input: [{ type: "text", text: "Inspect the workspace." }], - cwd: "/workspace", + cwd: "/turn-workspace", model: "gpt-5.4", - approvalPolicy: "on-request", - approvalsReviewer: "auto_review", - sandboxPolicy: { type: "workspaceWrite" }, + ...turnDefaults, summary: "auto", collaborationMode: { mode: "default", settings: { model: "gpt-5.4" } }, }, @@ -1825,20 +1856,22 @@ describe("CodexAdapterV2 post-settle continuation", () => { .slice(6) .map((entry) => entry.type !== "runtime_exit" && Predicate.hasProperty(entry.frame, "id") - ? withReplayRequestId(entry, 4) + ? withReplayRequestId(entry, 5) : entry, ), ]; const transcript = yield* decodeReplayTranscriptJson( (yield* encodeReplayTranscriptJson( makeCodexReplayTranscript({ - scenario: `physical-workspace-${platform}`, + scenario: `physical-workspace-${platform}-${runtimeMode}`, entries, }), - )).replaceAll( - yield* encodeStringJson("/workspace"), - yield* encodeStringJson(expectedCwd), - ), + )) + .replaceAll(yield* encodeStringJson("/workspace"), yield* encodeStringJson(alias)) + .replaceAll( + yield* encodeStringJson("/turn-workspace"), + yield* encodeStringJson(turnCwd), + ), ); const harness = yield* makeCodexReplayHarness( transcript, @@ -1861,7 +1894,8 @@ describe("CodexAdapterV2 post-settle continuation", () => { modelSelection: CODEX_TEST_MODEL_SELECTION, }); assert.lengthOf(requests, 2); - for (const request of requests) assert.propertyVal(request, "cwd", expectedCwd); + // Codex keys project trust and config on the thread cwd, so thread requests keep the alias. + for (const request of requests) assert.propertyVal(request, "cwd", alias); yield* harness.runtime.startTurn({ ...makeCodexTestTurnInput({ threadId: harness.threadId, @@ -1888,85 +1922,6 @@ describe("CodexAdapterV2 post-settle continuation", () => { ), ); - it.effect("follows a workspace alias retargeted between Codex turns", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-retarget-" }); - const alias = `${root}/alias`; - const targets = [`${root}/first`, `${root}/second`]; - for (const target of targets) yield* fs.makeDirectory(target); - yield* fs.symlink(targets[0]!, alias); - const entries: Array = []; - for (const [index, target] of targets.entries()) { - const preamble = codexReplayPreamble({ - nativeThreadId: "retargeted-workspace", - nativeTurnId: `retargeted-turn-${index}`, - prompt: `Inspect workspace ${index}.`, - }); - const transcript = yield* decodeReplayTranscriptJson( - (yield* encodeReplayTranscriptJson( - makeCodexReplayTranscript({ - scenario: "retargeted-workspace", - entries: - index === 0 - ? preamble - : preamble - .slice(5) - .map((entry) => - entry.type !== "runtime_exit" && Predicate.hasProperty(entry.frame, "id") - ? withReplayRequestId(entry, 4) - : entry, - ), - }), - )).replaceAll( - yield* encodeStringJson("/workspace"), - yield* encodeStringJson(yield* fs.realPath(target)), - ), - ); - entries.push(...transcript.entries); - } - const policy = { ...CODEX_TEST_RUNTIME_POLICY, cwd: alias }; - const harness = yield* makeCodexReplayHarness( - makeCodexReplayTranscript({ scenario: "retargeted-workspace", entries }), - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - policy, - ); - for (const [index, target] of targets.entries()) { - yield* fs.remove(alias); - yield* fs.symlink(target, alias); - yield* harness.runtime.startTurn({ - ...makeCodexTestTurnInput({ - threadId: harness.threadId, - providerThread: harness.providerThread, - now: yield* DateTime.now, - attemptId: RunAttemptId.make(`retargeted-attempt-${index}`), - text: `Inspect workspace ${index}.`, - }), - runtimePolicy: policy, - }); - } - assert.equal(policy.cwd, alias); - }).pipe( - Effect.scoped, - Effect.provideService(HostProcess.Platform, "darwin"), - Effect.provide( - Layer.mergeAll( - IdAllocator.layer, - McpProviderSessions.layer, - NodeServices.layer, - NodeCrypto.layer, - ), - ), - ), - ); - it.effect("adds the session MCP server without replacing launch-argument servers", () => Effect.gen(function* () { const scenario = "launch-mcp-servers"; @@ -4126,9 +4081,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { if (outcome === "recover") { const fs = yield* FileSystem.FileSystem; - const cwd = yield* fs.realPath( - yield* fs.makeTempDirectoryScoped({ prefix: "t3-capacity-retry-" }), - ); + const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-capacity-retry-" }); const localTranscript = yield* decodeReplayTranscriptJson( (yield* encodeReplayTranscriptJson( makeCodexReplayTranscript({ scenario, entries }), @@ -5763,9 +5716,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { return Effect.scoped( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const cwd = yield* fs.realPath( - yield* fs.makeTempDirectoryScoped({ prefix: "t3-bg-stop-workspace-" }), - ); + const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bg-stop-workspace-" }); const localTranscript = yield* decodeReplayTranscriptJson( (yield* encodeReplayTranscriptJson(transcript)).replaceAll( yield* encodeStringJson("/workspace"), @@ -5892,9 +5843,7 @@ describe("CodexAdapterV2 post-settle continuation", () => { Effect.scoped( Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; - const cwd = yield* fs.realPath( - yield* fs.makeTempDirectoryScoped({ prefix: "t3-bg-stale-workspace-" }), - ); + const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bg-stale-workspace-" }); const staleTranscript = makeCodexReplayTranscript({ scenario: "codex-bg-stop-untracked", entries: [ diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 8d62ec2b8f27c..6c1327314e667 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -95,6 +95,7 @@ import * as Function from "effect/Function"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; +import * as Predicate from "effect/Predicate"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; @@ -1777,17 +1778,26 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( const { clientFactory, digest, fileSystem, idAllocator, serverConfig } = adapterOptions; const continuationRequests = adapterOptions.continuationRequests; - const resolveCodexRuntimePolicy = Effect.fn("CodexAdapterV2.resolveCodexRuntimePolicy")( - function* (policy: ProviderAdapter.ProviderAdapterV2RuntimePolicy) { - const cwd = policy.cwd; - // Windows realpath can rewrite mapped drives to network paths; preserve that platform's cwd. - if (cwd === null || (yield* HostProcess.Platform) === "win32") return policy; - return { - ...policy, - cwd: yield* fileSystem.realPath(cwd).pipe(Effect.orElseSucceed(() => cwd)), - }; - }, - ); + // macOS Seatbelt refuses a symlinked writable root, so workspace-write turns run in the physical + // directory. Thread requests keep the registered path for Codex's project trust and config. + const resolveCodexTurnCwd = Effect.fn("CodexAdapterV2.resolveCodexTurnCwd")(function* ( + policy: ProviderAdapter.ProviderAdapterV2RuntimePolicy, + ) { + const cwd = policy.cwd; + const sandboxPolicy = + policy.sandboxPolicy ?? codexRuntimeModeTurnDefaults(policy.runtimeMode).sandboxPolicy; + if ( + cwd === null || + (yield* HostProcess.Platform) !== "darwin" || + !Predicate.hasProperty(sandboxPolicy, "type") || + sandboxPolicy.type !== "workspaceWrite" + ) + return policy; + return { + ...policy, + cwd: yield* fileSystem.realPath(cwd).pipe(Effect.orElseSucceed(() => cwd)), + }; + }); return ProviderAdapter.ProviderAdapterV2.of({ instanceId: adapterOptions.instanceId, @@ -7281,7 +7291,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( cyberAccess: codexCyberAccess(models, turnInput.modelSelection.model), nativeThreadId: threadId, codexInput, - runtimePolicy: turnInput.runtimePolicy, + runtimePolicy: yield* resolveCodexTurnCwd(turnInput.runtimePolicy), modelSelection: turnInput.modelSelection, hasT3Mcp: mcpSession !== undefined, browserToolsAvailable: mcpSession?.browserToolsAvailable ?? true, @@ -7601,9 +7611,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( } return false; }), - ensureThread: Effect.fn("CodexAdapterV2.ensureThread")(function* (threadInput) { - const runtimePolicy = yield* resolveCodexRuntimePolicy(threadInput.runtimePolicy); - return yield* ensureInitialized.pipe( + ensureThread: (threadInput) => + ensureInitialized.pipe( Effect.andThen(mcpSessions.read(threadInput.threadId)), Effect.flatMap((mcpSession) => client.request( @@ -7612,7 +7621,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( threadId: threadInput.threadId, mcpSession, modelSelection: threadInput.modelSelection, - runtimePolicy, + runtimePolicy: threadInput.runtimePolicy, }), ), ), @@ -7634,8 +7643,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( cause, }), ), - ); - }), + ), resumeThread: Effect.fn("CodexAdapterV2.resumeThread")( function* (threadInput) { const nativeThreadId = yield* getNativeThreadId(threadInput.providerThread); @@ -7650,9 +7658,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( : { modelSelection: threadInput.modelSelection }), ...(threadInput.runtimePolicy === undefined ? {} - : { - runtimePolicy: yield* resolveCodexRuntimePolicy(threadInput.runtimePolicy), - }), + : { runtimePolicy: threadInput.runtimePolicy }), }), }); const response = yield* ensureInitialized.pipe( @@ -7749,47 +7755,41 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( }), ), ), - // Resolved once per turn so capacity retries and goal continuations reuse the physical cwd. - startTurn: (requestedTurnInput) => - Effect.flatMap( - resolveCodexRuntimePolicy(requestedTurnInput.runtimePolicy), - (runtimePolicy) => { - const turnInput = { ...requestedTurnInput, runtimePolicy }; - const goalCommand = - turnInput.restartContinuationOfRunId === undefined - ? parseGoalCommand(turnInput.message) - : null; - if (goalCommand !== null) return runGoalCommand(turnInput, goalCommand); - const nativeThreadId = turnInput.providerThread.nativeThreadRef?.nativeId; - return Effect.gen(function* () { - if ( - turnInput.restartContinuationOfRunId === undefined && - nativeThreadId != null && - goalsPausedForWake.has(nativeThreadId) && - goalsByNativeThread.get(nativeThreadId)?.status === "paused" - ) { - return yield* activateGoal(turnInput, nativeThreadId, toCodexInput(turnInput)); - } - yield* startNativeTurn( - turnInput, - turnInput.restartContinuationOfRunId === undefined - ? yield* toCodexInput(turnInput) - : [], - ); - }).pipe( - Effect.mapError( - (cause) => - new ProviderAdapter.ProviderAdapterTurnStartError({ - driver: CODEX_PROVIDER, - threadId: turnInput.threadId, - providerThreadId: turnInput.providerThread.id, - runId: turnInput.runId, - cause, - }), - ), - ); - }, - ), + startTurn: (turnInput) => { + const goalCommand = + turnInput.restartContinuationOfRunId === undefined + ? parseGoalCommand(turnInput.message) + : null; + if (goalCommand !== null) return runGoalCommand(turnInput, goalCommand); + const nativeThreadId = turnInput.providerThread.nativeThreadRef?.nativeId; + return Effect.gen(function* () { + if ( + turnInput.restartContinuationOfRunId === undefined && + nativeThreadId != null && + goalsPausedForWake.has(nativeThreadId) && + goalsByNativeThread.get(nativeThreadId)?.status === "paused" + ) { + return yield* activateGoal(turnInput, nativeThreadId, toCodexInput(turnInput)); + } + yield* startNativeTurn( + turnInput, + turnInput.restartContinuationOfRunId === undefined + ? yield* toCodexInput(turnInput) + : [], + ); + }).pipe( + Effect.mapError( + (cause) => + new ProviderAdapter.ProviderAdapterTurnStartError({ + driver: CODEX_PROVIDER, + threadId: turnInput.threadId, + providerThreadId: turnInput.providerThread.id, + runId: turnInput.runId, + cause, + }), + ), + ); + }, steerTurn: Effect.fn("CodexAdapterV2.steerTurn")( function* (turnInput) { const threadId = yield* getNativeThreadId(turnInput.providerThread); @@ -8447,7 +8447,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( threadId: threadInput.providerThread.appThreadId, mcpSession: yield* readMcpSession(threadInput.providerThread.appThreadId), modelSelection: input.modelSelection, - runtimePolicy: yield* resolveCodexRuntimePolicy(input.runtimePolicy), + runtimePolicy: input.runtimePolicy, }), }); } @@ -8501,9 +8501,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( : { modelSelection: threadInput.modelSelection }), ...(threadInput.runtimePolicy === undefined ? {} - : { - runtimePolicy: yield* resolveCodexRuntimePolicy(threadInput.runtimePolicy), - }), + : { runtimePolicy: threadInput.runtimePolicy }), }), }); const response = yield* ensureInitialized.pipe(