Skip to content
160 changes: 117 additions & 43 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,49 +723,6 @@ describe("CodexAdapterV2 runtime policy", () => {
});

describe("CodexAdapterV2 process spawning", () => {
it("injects cwd, model, and MCP authorization into thread-scoped params", () => {
const threadId = ThreadId.make("thread-codex-mcp");
const mcpSession = {
environmentId: EnvironmentId.make("environment-codex-mcp"),
threadId,
providerSessionId: "mcp-session-codex",
providerInstanceId: ProviderInstanceId.make("codex"),
endpoint: "http://127.0.0.1:43123/mcp",
authorizationHeader: "Bearer secret-codex-token",
browserToolsAvailable: true,
};

assert.deepEqual(
CodexAdapterV2.codexThreadRuntimeParams({
mcpSession,
modelSelection: { model: "gpt-5.4" },
runtimePolicy: {
runtimeMode: "full-access",
interactionMode: "default",
cwd: "/workspace/thread-codex-mcp",
},
}),
{
cwd: "/workspace/thread-codex-mcp",
model: "gpt-5.4",
config: {
"tools.update_plan.enabled": true,
// Codex waits up to 10 s, not 1 s, for MCP servers before the first request (#13437).
mcp_optional_startup_grace_ms: 10_000,
mcp_servers: {
"t3-code": {
url: "http://127.0.0.1:43123/mcp",
tool_timeout_sec: 3_900,
http_headers: {
Authorization: "Bearer secret-codex-token",
},
},
},
},
},
);
});

it.effect("resolves Windows command shims through the shared spawn policy", () =>
Effect.gen(function* () {
const command = yield* CodexAdapterV2.makeCodexAppServerSpawnCommand({
Expand Down Expand Up @@ -2275,6 +2232,123 @@ describe("CodexAdapterV2 post-settle continuation", () => {
),
);

it.effect("adds the session MCP server without replacing launch-argument servers", () =>
Effect.gen(function* () {
const scenario = "launch-mcp-servers";
yield* (yield* McpProviderSessions.McpProviderSessions).set({
environmentId: EnvironmentId.make("environment-codex-mcp"),
threadId: ThreadId.make(`thread-${scenario}`),
providerSessionId: `provider-session-${scenario}`,
providerInstanceId: ProviderInstanceId.make("codex"),
endpoint: "http://127.0.0.1:43123/mcp",
authorizationHeader: "Bearer secret-codex-token",
browserToolsAvailable: true,
});
const requests: Array<unknown> = [];
yield* makeCodexReplayHarness(
makeCodexReplayTranscript({
scenario,
entries: codexReplayPreamble({
nativeThreadId: "launch-mcp-thread",
nativeTurnId: "unused",
prompt: "unused",
}).slice(0, 5),
}),
() => Effect.void,
(method, params) =>
Effect.sync(() => {
if (method === "thread/start") requests.push(params);
}),
);
assert.deepEqual(requests, [
{
cwd: "/workspace",
model: "gpt-5.4",
config: {
"tools.update_plan.enabled": true,
mcp_optional_startup_grace_ms: 10_000,
"mcp_servers.t3-code": {
url: "http://127.0.0.1:43123/mcp",
tool_timeout_sec: 3_900,
http_headers: { Authorization: "Bearer secret-codex-token" },
},
},
},
]);
}).pipe(
Effect.scoped,
Effect.provide(
Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
),
),
);

it.effect("replays historical and dotted MCP config while enforcing other thread config", () =>
Effect.gen(function* () {
const recordedServer = {
url: "http://127.0.0.1:41000/mcp",
http_headers: { Authorization: "Bearer recorded-token" },
};
const currentConfig = {
"tools.update_plan.enabled": true,
"mcp_servers.t3-code": {
url: "http://127.0.0.1:42000/mcp",
http_headers: { Authorization: "Bearer current-token" },
},
"mcp_servers.extra.command": "cat",
};
for (const recordedMcp of [
{ mcp_servers: { "t3-code": recordedServer } },
{ "mcp_servers.t3-code": recordedServer, "mcp_servers.extra.command": "echo" },
]) {
const entries = codexReplayPreamble({
nativeThreadId: "replayed-mcp-thread",
nativeTurnId: "unused",
prompt: "unused",
})
.slice(3, 5)
.map((entry) => withReplayRequestId(entry, 1));
entries[0] = {
type: "expect_outbound",
frame: {
id: 1,
method: "thread/start",
params: { config: { "tools.update_plan.enabled": true, ...recordedMcp } },
},
};
entries.push({
...entries[0],
frame: {
id: 2,
method: "thread/start",
params: { config: { "tools.update_plan.enabled": true, ...recordedMcp } },
},
});
const driver = yield* CodexReplay.makeReplayDriver(
makeCodexReplayTranscript({
scenario: "mcp-config-normalization",
entries,
}),
);
yield* Effect.gen(function* () {
const client = yield* CodexClient.CodexAppServerClient;
const response = yield* client.request("thread/start", { config: currentConfig });
assert.equal(response.thread.id, "replayed-mcp-thread");
assert.isNull((yield* Ref.get(driver.state)).failure);
yield* client
.request("thread/start", {
config: { ...currentConfig, "tools.update_plan.enabled": false },
})
.pipe(Effect.flip);
assert.instanceOf(
(yield* Ref.get(driver.state)).failure,
CodexReplay.CodexAppServerReplayFrameMismatchError,
);
}).pipe(Effect.provide(CodexReplay.layerReplayWithDriver(driver)));
}
}).pipe(Effect.scoped),
);

it.effect("identifies sessions to Codex with the same client info as main", () =>
Effect.gen(function* () {
const transcript = makeCodexReplayTranscript({
Expand Down
15 changes: 7 additions & 8 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,7 @@ export const CODEX_THREAD_CONFIG = {
// that limit so the server's own wait timeout ends a long call.
const CODEX_T3_MCP_TOOL_TIMEOUT_SEC = 65 * 60;

export function codexThreadRuntimeParams(input: {
function codexThreadRuntimeParams(input: {
readonly mcpSession: McpProviderSession.McpProviderSessionConfig | undefined;
readonly modelSelection?: { readonly model: string };
readonly runtimePolicy?: ProviderAdapter.ProviderAdapterV2RuntimePolicy;
Expand All @@ -1321,13 +1321,12 @@ export function codexThreadRuntimeParams(input: {
...(mcpSession === undefined
? {}
: {
mcp_servers: {
"t3-code": {
url: mcpSession.endpoint,
tool_timeout_sec: CODEX_T3_MCP_TOOL_TIMEOUT_SEC,
http_headers: {
Authorization: mcpSession.authorizationHeader,
},
// Override only T3's server so launch-argument MCP servers survive.
"mcp_servers.t3-code": {
url: mcpSession.endpoint,
tool_timeout_sec: CODEX_T3_MCP_TOOL_TIMEOUT_SEC,
http_headers: {
Authorization: mcpSession.authorizationHeader,
},
},
}),
Expand Down
6 changes: 5 additions & 1 deletion packages/effect-codex-app-server/src/replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,11 @@ function normalizeThreadRequest(frame: unknown, ignoredConfigKeys: ReadonlySet<s
}
const { cwd: _cwd, model: _model, config, ...params } = record.params as Record<string, unknown>;
if (typeof config !== "object" || config === null) return { ...record, params };
const { mcp_servers: _mcpServers, ...owned } = config as Record<string, unknown>;
const owned = Object.fromEntries(
Object.entries(config as Record<string, unknown>).filter(
([key]) => key !== "mcp_servers" && !key.startsWith("mcp_servers."),
),
);
for (const key of ignoredConfigKeys) delete owned[key];
return { ...record, params: { ...params, config: owned } };
}
Expand Down
Loading