Skip to content
Merged
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
9 changes: 4 additions & 5 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,12 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { resolveAttachmentPath } from "../../attachmentStore.ts";
import { getCodexServiceTierOptionValue } from "../../codexModelOptions.ts";
import { ServerConfig } from "../../config.ts";
import { buildCodexDeveloperInstructions } from "../../provider/CodexDeveloperInstructions.ts";
import {
describeMcpElicitation,
toMcpElicitationResponse,
} from "../../provider/Layers/CodexSessionRuntime.ts";
import { ServerConfig } from "../../config.ts";
import { buildCodexDeveloperInstructions } from "../../provider/CodexDeveloperInstructions.ts";
} from "../../provider/CodexMcpElicitation.ts";
import {
materializeCodexShadowHome,
resolveCodexHomeLayout,
Expand Down Expand Up @@ -896,8 +896,7 @@ export const resolveCodexForkBoundary = Effect.fn("CodexAdapterV2.resolveForkBou

/**
* The generated `thread/read` response schema does not surface `historyMode`,
* so the probe goes through the raw request channel with a permissive decode
* (mirrors the V1 session runtime's paginated-history detection).
* so the probe goes through the raw request channel with a permissive decode.
*/
const CodexThreadHistoryMetadata = Schema.Struct({
thread: Schema.Struct({
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/orchestration-v2/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ implementation checklist for `apps/server/src/orchestration-v2`.
to `thread/fork` as `lastTurnId`; the fork-then-rollback fallback remains only for source turns
without a native turn reference and only on legacy-history threads.
- Codex provider conversation rollback supports only legacy-history threads. The adapter probes
`historyMode` and fails explicitly on paginated threads; the V1 session runtime's
`thread/turns/list` + `thread/revert` path is the reference for closing this gap.
`historyMode` and fails explicitly on paginated threads. Closing the gap means paging
`thread/turns/list` to find the first removed turn, then `thread/revert` with `beforeTurnId`.
- Native Codex fork-from-earlier-run has a real replay-backed test fixture:
`testkit/fixtures/thread_fork_native_prior_turn`.
- Merge-back from a fork into its source thread records a `merge_back` context transfer, materializes
Expand Down
102 changes: 102 additions & 0 deletions apps/server/src/provider/CodexDeveloperInstructions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as NodeAssert from "node:assert/strict";

import { describe, it } from "vite-plus/test";

import { buildCodexDeveloperInstructions } from "./CodexDeveloperInstructions.ts";

describe("buildCodexDeveloperInstructions", () => {
it("appends runtime info after the mode instructions", () => {
const instructions = buildCodexDeveloperInstructions("default", {
model: "gpt-5.3-codex",
reasoningEffort: "high",
});

NodeAssert.match(instructions, /^<collaboration_mode># Collaboration Mode: Default/);
NodeAssert.match(instructions, /T3 Code/);
NodeAssert.match(instructions, /Codex harness/);
NodeAssert.match(instructions, /as gpt-5\.3-codex with high reasoning effort/);
});

it("describes Markdown media support in the runtime context in both modes", () => {
for (const mode of ["default", "plan"] as const) {
const instructions = buildCodexDeveloperInstructions(mode, {
model: "gpt-5.3-codex",
reasoningEffort: "high",
});
NodeAssert.match(
instructions,
/<runtime_info>.*embed images and videos.*Markdown.*<\/runtime_info>/,
);
}
});

it("includes runtime info alongside plan mode instructions", () => {
const instructions = buildCodexDeveloperInstructions("plan", {
model: "gpt-5.3-codex",
reasoningEffort: "medium",
});

NodeAssert.match(instructions, /^<collaboration_mode># Plan Mode/);
NodeAssert.match(instructions, /as gpt-5\.3-codex with medium reasoning effort/);
});

it("varies with the model and effort of each turn", () => {
const first = buildCodexDeveloperInstructions("default", {
model: "gpt-5.3-codex",
reasoningEffort: "medium",
});
const second = buildCodexDeveloperInstructions("default", {
model: "gpt-5.4",
reasoningEffort: "high",
});

NodeAssert.notEqual(first, second);
});

it("flattens multiline metadata into single-line runtime info", () => {
const instructions = buildCodexDeveloperInstructions("default", {
model: "gpt\n5.3\ncodex",
reasoningEffort: " high\neffort ",
});

NodeAssert.match(instructions, /as gpt 5\.3 codex with high effort reasoning effort/);
NodeAssert.doesNotMatch(instructions, /<runtime_info>[^<]*\n/);
});
});

describe("T3 browser developer instructions", () => {
const runtime = { model: "gpt-5.3-codex", reasoningEffort: "high" };

it("prefers the product-native preview tools in both collaboration modes", () => {
for (const mode of ["default", "plan"] as const) {
const instructions = buildCodexDeveloperInstructions(mode, runtime, true);
NodeAssert.match(instructions, /t3-code/);
NodeAssert.match(instructions, /preview_status/);
NodeAssert.match(instructions, /preview_open/);
NodeAssert.match(instructions, /Do not switch to global browser skills/);
}
});

it("omits the browser block entirely when the preview tools are not attached", () => {
for (const mode of ["default", "plan"] as const) {
const instructions = buildCodexDeveloperInstructions(mode, runtime, false);
NodeAssert.doesNotMatch(instructions, /preview_status/);
NodeAssert.doesNotMatch(instructions, /preview_open/);
NodeAssert.doesNotMatch(instructions, /T3 Code collaborative browser/);
// Steering away from other browser automation must go with the tools;
// keeping it would leave the model talked out of its only option.
NodeAssert.doesNotMatch(instructions, /Do not switch to global browser skills/);
// The rest of the collaboration mode is untouched.
NodeAssert.match(instructions, /<collaboration_mode>/);
NodeAssert.match(instructions, /<\/collaboration_mode>/);
}
});

it("tracks the turn's MCP configuration rather than defaulting to on", () => {
NodeAssert.match(buildCodexDeveloperInstructions("default", runtime, true), /preview_open/);
NodeAssert.doesNotMatch(
buildCodexDeveloperInstructions("default", runtime, false),
/preview_open/,
);
});
});
208 changes: 208 additions & 0 deletions apps/server/src/provider/CodexMcpElicitation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import * as NodeAssert from "node:assert/strict";

import { describe, it } from "vite-plus/test";
import type * as EffectCodexSchema from "effect-codex-app-server/schema";

import { describeMcpElicitation, toMcpElicitationResponse } from "./CodexMcpElicitation.ts";

describe("Codex MCP elicitation approvals", () => {
const request = {
mode: "form",
message: "Allow ChatGPT to use Safari?",
serverName: "computer-use",
threadId: "provider-thread-1",
turnId: "turn-1",
_meta: {
app_name: "Safari",
persist: ["session", "always"],
},
requestedSchema: {
type: "object",
properties: {
approval: {
type: "string",
oneOf: [
{ const: "once", title: "Allow once" },
{ const: "session", title: "Allow for this session" },
{ const: "always", title: "Always allow Safari" },
],
},
},
required: ["approval"],
},
} satisfies EffectCodexSchema.McpServerElicitationRequestParams;

it("preserves the app name and advertised persistence choices", () => {
NodeAssert.deepStrictEqual(describeMcpElicitation(request), {
appName: "Safari",
options: [
{ decision: "cancel", label: "Cancel" },
{ decision: "decline", label: "Decline" },
{ decision: "acceptForSession", label: "Allow for this session" },
{ decision: "acceptAlways", label: "Always allow Safari" },
{ decision: "accept", label: "Approve" },
],
});
});

it("extracts the app name from a Computer Use request without metadata", () => {
const { _meta, ...requestWithoutMetadata } = request;

NodeAssert.equal(describeMcpElicitation(requestWithoutMetadata).appName, "Safari");
});

it("returns the accepted form option to Codex", () => {
NodeAssert.deepStrictEqual(toMcpElicitationResponse(request, "accept"), {
action: "accept",
content: { approval: "once" },
});
});

it("returns session-scoped approval in the MCP response", () => {
NodeAssert.deepStrictEqual(toMcpElicitationResponse(request, "acceptForSession"), {
action: "accept",
_meta: { persist: "session" },
content: { approval: "session" },
});
});

it("returns persistent approval in the MCP response", () => {
NodeAssert.deepStrictEqual(toMcpElicitationResponse(request, "acceptAlways"), {
action: "accept",
_meta: { persist: "always" },
content: { approval: "always" },
});
});

it("returns rejection without form content", () => {
NodeAssert.deepStrictEqual(toMcpElicitationResponse(request, "decline"), {
action: "decline",
});
});

it("returns cancellation without form content", () => {
NodeAssert.deepStrictEqual(toMcpElicitationResponse(request, "cancel"), {
action: "cancel",
});
});

it("supports boolean permanent-approval fields", () => {
const booleanRequest = {
...request,
_meta: { app_name: "Safari" },
requestedSchema: {
type: "object",
properties: {
always: { type: "boolean", title: "Always allow Safari" },
},
},
} satisfies EffectCodexSchema.McpServerElicitationRequestParams;

NodeAssert.ok(
describeMcpElicitation(booleanRequest).options.some(
(option) => option.decision === "acceptAlways",
),
);
NodeAssert.deepStrictEqual(toMcpElicitationResponse(booleanRequest, "acceptAlways"), {
action: "accept",
_meta: { persist: "always" },
content: { always: true },
});
});

it("preserves valid nullable MCP form fields and persistence choices", () => {
const nullableRequest = {
...request,
_meta: {
app_name: null,
appName: "Safari",
connector_name: null,
persist: null,
target: null,
tool_params: null,
},
requestedSchema: {
type: "object",
properties: {
approval: {
type: "string",
title: null,
description: null,
default: null,
enum: ["once", "always"],
enumNames: null,
},
},
required: ["approval"],
},
} satisfies EffectCodexSchema.McpServerElicitationRequestParams;

NodeAssert.equal(describeMcpElicitation(nullableRequest).appName, "Safari");
NodeAssert.ok(
describeMcpElicitation(nullableRequest).options.some(
(option) => option.decision === "acceptAlways",
),
);
NodeAssert.deepStrictEqual(toMcpElicitationResponse(nullableRequest, "acceptAlways"), {
action: "accept",
_meta: { persist: "always" },
content: { approval: "always" },
});
});

it("declines required form fields that an approval prompt cannot collect", () => {
const inputRequest = {
...request,
requestedSchema: {
type: "object",
properties: {
email: { type: "string", format: "email" },
},
required: ["email"],
},
} satisfies EffectCodexSchema.McpServerElicitationRequestParams;

NodeAssert.deepStrictEqual(toMcpElicitationResponse(inputRequest, "accept"), {
action: "decline",
});
});

it("does not approve URL elicitations without opening their requested URL", () => {
const urlRequest = {
mode: "url",
message: "Finish signing in to continue.",
serverName: "computer-use",
threadId: "provider-thread-1",
turnId: "turn-1",
elicitationId: "sign-in-1",
url: "https://example.com/authorize",
} satisfies EffectCodexSchema.McpServerElicitationRequestParams;

NodeAssert.deepStrictEqual(toMcpElicitationResponse(urlRequest, "accept"), {
action: "decline",
});
});

it("omits persistence choices that cannot satisfy required form fields", () => {
const onceOnlyRequest = {
...request,
_meta: { app_name: "Safari", persist: ["session", "always"] },
requestedSchema: {
type: "object",
properties: {
approval: {
type: "string",
enum: ["once"],
},
},
required: ["approval"],
},
} satisfies EffectCodexSchema.McpServerElicitationRequestParams;

NodeAssert.deepStrictEqual(describeMcpElicitation(onceOnlyRequest).options, [
{ decision: "cancel", label: "Cancel" },
{ decision: "decline", label: "Decline" },
{ decision: "accept", label: "Approve" },
]);
});
});
Loading
Loading