Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 166 additions & 4 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CodexReplay.CodexAppServerReplayEntry>,
): Array<CodexReplay.CodexAppServerReplayEntry> {
Expand All @@ -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(
Expand Down Expand Up @@ -1332,6 +1334,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
promptSuggestions: Effect.Effect<boolean> = Effect.succeed(false),
onNotification: (method: string) => Effect.Effect<void> = () => Effect.void,
models?: CodexAdapterV2.CodexAdapterV2Options["models"],
runtimePolicy = CODEX_TEST_RUNTIME_POLICY,
) =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand Down Expand Up @@ -1411,13 +1414,13 @@ describe("CodexAdapterV2 post-settle continuation", () => {
threadId,
providerSessionId: ProviderSessionId.make(`provider-session-${transcript.scenario}`),
modelSelection: CODEX_TEST_MODEL_SELECTION,
runtimePolicy: CODEX_TEST_RUNTIME_POLICY,
runtimePolicy,
promptSuggestions,
});
const providerThread = yield* runtime.ensureThread({
threadId,
modelSelection: CODEX_TEST_MODEL_SELECTION,
runtimePolicy: CODEX_TEST_RUNTIME_POLICY,
runtimePolicy,
});
const events: Array<ProviderAdapter.ProviderAdapterV2Event> = [];
const firstTerminal = yield* Deferred.make<void>();
Expand Down Expand Up @@ -1760,6 +1763,165 @@ describe("CodexAdapterV2 post-settle continuation", () => {
),
);

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-" });
const target = `${root}/physical`;
const alias = `${root}/alias`;
yield* fs.makeDirectory(target);
yield* fs.symlink(target, 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<unknown> = [];
const preamble = codexReplayPreamble({
nativeThreadId: "physical-workspace",
nativeTurnId: "physical-turn",
prompt: "Inspect the workspace.",
});
const entries: Array<CodexReplay.CodexAppServerReplayEntry> = [
...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 } },
},
},
// Config reads keep the registered path, as the thread requests do.
{
type: "expect_outbound",
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: "/turn-workspace",
model: "gpt-5.4",
...turnDefaults,
summary: "auto",
collaborationMode: { mode: "default", settings: { model: "gpt-5.4" } },
},
},
},
...preamble
.slice(6)
.map((entry) =>
entry.type !== "runtime_exit" && Predicate.hasProperty(entry.frame, "id")
? withReplayRequestId(entry, 5)
: entry,
),
];
const transcript = yield* decodeReplayTranscriptJson(
(yield* encodeReplayTranscriptJson(
makeCodexReplayTranscript({
scenario: `physical-workspace-${platform}-${runtimeMode}`,
entries,
}),
))
.replaceAll(yield* encodeStringJson("/workspace"), yield* encodeStringJson(alias))
.replaceAll(
yield* encodeStringJson("/turn-workspace"),
yield* encodeStringJson(turnCwd),
),
);
const harness = yield* makeCodexReplayHarness(
transcript,
() => Effect.void,
(method, params) =>
Effect.sync(() => {
if (method === "thread/start" || method === "thread/resume") requests.push(params);
}),
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
policy,
);
yield* harness.runtime.resumeThread({
providerThread: harness.providerThread,
runtimePolicy: policy,
modelSelection: CODEX_TEST_MODEL_SELECTION,
});
assert.lengthOf(requests, 2);
// 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,
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("adds the session MCP server without replacing launch-argument servers", () =>
Effect.gen(function* () {
const scenario = "launch-mcp-servers";
Expand Down
24 changes: 23 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -1777,6 +1778,27 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
const { clientFactory, digest, fileSystem, idAllocator, serverConfig } = adapterOptions;
const continuationRequests = adapterOptions.continuationRequests;

// 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,
driver: CODEX_PROVIDER,
Expand Down Expand Up @@ -7269,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,
Expand Down
Loading