Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6af7cf5
Add Codex launch arguments setting
jamesx0416 Jun 1, 2026
8d4883a
Address Codex launch args review
jamesx0416 Jun 1, 2026
e9608d9
Keep Codex launch args PR small
jamesx0416 Jun 1, 2026
e051516
Fix Codex adapter launch args test expectation
jamesx0416 Jun 1, 2026
3d522fe
Forward shared Codex launch args to exec
jamesx0416 Jun 4, 2026
4f301a6
Merge branch 'main' into t3code/codex-launch-args
jamesx0416 Jun 4, 2026
8939411
Merge branch 'main' into t3code/codex-launch-args
jamesx0416 Jun 4, 2026
c714c38
Handle incomplete Codex exec launch flags
jamesx0416 Jun 4, 2026
15cbb46
Merge branch 'main' into t3code/codex-launch-args
juliusmarminge Jun 5, 2026
518e65d
Change test framework import from vitest to vite-plus
juliusmarminge Jun 5, 2026
737d8f6
Merge upstream/main into pr-2892
jamesx0416 Jun 20, 2026
9fcde3c
Merge branch 'main' into t3code/codex-launch-args
juliusmarminge Jun 20, 2026
0d5061a
Fix Codex app-server args
jamesx0416 Jun 21, 2026
7867442
Merge branch 'main' into t3code/codex-launch-args
jamesx0416 Jun 21, 2026
7b3b6d3
Fix Codex launch args checks
jamesx0416 Jun 21, 2026
006c9f5
Merge branch 'main' into t3code/codex-launch-args
jamesx0416 Jun 29, 2026
ccfabb2
Add codex launch args env override
invalid-email-address Jun 30, 2026
5304b45
Preserve launch args for MCP Codex sessions
invalid-email-address Jun 30, 2026
036a8dd
Parse quoted Codex launch args
invalid-email-address Jul 1, 2026
568ae0e
Preserve backslashes in Codex launch args
invalid-email-address Jul 1, 2026
bfa7715
Resolve catalog store merge conflict
jamesx0416 Jul 10, 2026
9dbbf9e
Merge branch 'main' into t3code/codex-launch-args
jamesx0416 Jul 10, 2026
04cb6cd
Share quote-aware CLI arg tokenization
jamesx0416 Jul 10, 2026
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
32 changes: 32 additions & 0 deletions apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@
NodeAssert.deepStrictEqual(validationRuntimeFactory.factory.mock.calls[0]?.[0], {
binaryPath: "codex",
cwd: process.cwd(),
launchArgs: "",
model: "gpt-5.3-codex",
providerInstanceId: ProviderInstanceId.make("codex"),
serviceTier: "priority",
Expand Down Expand Up @@ -359,6 +360,37 @@
}),
);

it.effect("passes configured launch args into the session runtime", () => {
const runtimeFactory = makeRuntimeFactory();
const layer = Layer.effect(
CodexAdapter,
Effect.gen(function* () {
const codexConfig = decodeCodexSettings({ launchArgs: "--strict-config --enable foo" });
return yield* makeCodexAdapter(codexConfig, {
makeRuntime: runtimeFactory.factory,
});
}),
).pipe(
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(providerSessionDirectoryTestLayer),
Layer.provideMerge(NodeServices.layer),
);

return Effect.gen(function* () {
const adapter = yield* CodexAdapter;
yield* adapter.startSession({
provider: ProviderDriverKind.make("codex"),
threadId: asThreadId("sess-launch-args"),
runtimeMode: "full-access",
});

const runtime = runtimeFactory.lastRuntime;
assert.ok(runtime);

Check failure on line 389 in apps/server/src/provider/Layers/CodexAdapter.test.ts

View workflow job for this annotation

GitHub Actions / Test

src/provider/Layers/CodexAdapter.test.ts > CodexAdapterLive session errors > passes configured launch args into the session runtime

ReferenceError: assert is not defined ❯ Array.<anonymous> src/provider/Layers/CodexAdapter.test.ts:389:7 ❯ next ../../node_modules/.pnpm/effect@4.0.0-beta.78_patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5/node_modules/effect/src/internal/effect.ts:1275:25 ❯ Object.~effect/Effect/evaluate ../../node_modules/.pnpm/effect@4.0.0-beta.78_patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5/node_modules/effect/src/internal/core.ts:513:30 ❯ FiberImpl.runLoop ../../node_modules/.pnpm/effect@4.0.0-beta.78_patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5/node_modules/effect/src/internal/effect.ts:633:39 ❯ runLoop ../../node_modules/.pnpm/effect@4.0.0-beta.78_patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5/node_modules/effect/src/internal/effect.ts:593:22 ❯ evaluate ../../node_modules/.pnpm/effect@4.0.0-beta.78_patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5/node_modules/effect/src/internal/effect.ts:1031:14 ❯ resume ../../node_modules/.pnpm/effect@4.0.0-beta.78_patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5/node_modules/effect/src/internal/effect.ts:973:13
assert.equal(runtime.options.launchArgs, "--strict-config --enable foo");
}).pipe(Effect.provide(layer));
});

it.effect("maps codex model options for the adapter's bound custom instance id", () => {
const customInstanceId = ProviderInstanceId.make("codex_personal");
const customRuntimeFactory = makeRuntimeFactory();
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/Layers/CodexAdapter.ts
Comment thread
jamesx0416 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -1392,6 +1392,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
providerInstanceId: boundInstanceId,
cwd: input.cwd ?? process.cwd(),
binaryPath: codexConfig.binaryPath,
launchArgs: codexConfig.launchArgs,
...(options?.environment ? { environment: options.environment } : {}),
...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}),
...(isCodexResumeCursorSchema(input.resumeCursor)
Expand Down
42 changes: 38 additions & 4 deletions apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,37 @@ export function buildCodexInitializeParams(): CodexSchema.V1InitializeParams {
};
}

export const codexAppServerArgs = (launchArgs?: string) =>
launchArgs?.trim() ? ["app-server", ...launchArgs.trim().split(/\s+/)] : ["app-server"];

export const codexExecLaunchArgs = (launchArgs?: string) => {
const args = launchArgs?.trim().split(/\s+/).filter(Boolean) ?? [];
const execArgs: Array<string> = [];
for (let index = 0; index < args.length; index++) {
const arg = args[index];
if (arg === undefined) continue;
if (arg === "--strict-config" || arg.startsWith("--config=") || arg.startsWith("-c=")) {
execArgs.push(arg);
} else if (arg === "--config" || arg === "-c" || arg === "--enable" || arg === "--disable") {
execArgs.push(arg);
const value = args[index + 1];
if (value !== undefined && !value.startsWith("-")) {
execArgs.push(value);
index++;
} else {
execArgs.pop();
}
} else if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) {
execArgs.push(arg);
}
}
return execArgs;
};
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(function* (input: {
readonly binaryPath: string;
readonly homePath?: string;
readonly launchArgs?: string;
readonly cwd: string;
readonly customModels?: ReadonlyArray<string>;
readonly environment?: NodeJS.ProcessEnv;
Expand All @@ -301,10 +329,14 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
...input.environment,
...(resolvedHomePath ? { CODEX_HOME: resolvedHomePath } : {}),
};
const spawnCommand = yield* resolveSpawnCommand(input.binaryPath, ["app-server"], {
env: environment,
extendEnv: true,
});
const spawnCommand = yield* resolveSpawnCommand(
input.binaryPath,
codexAppServerArgs(input.launchArgs),
{
env: environment,
extendEnv: true,
},
);
const child = yield* spawner
.spawn(
ChildProcess.make(spawnCommand.command, spawnCommand.args, {
Expand Down Expand Up @@ -463,6 +495,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
probe: (input: {
readonly binaryPath: string;
readonly homePath?: string;
readonly launchArgs?: string;
readonly cwd: string;
readonly customModels: ReadonlyArray<string>;
readonly environment?: NodeJS.ProcessEnv;
Expand Down Expand Up @@ -501,6 +534,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
const probeResult = yield* probe({
binaryPath: codexSettings.binaryPath,
homePath: codexSettings.homePath,
launchArgs: codexSettings.launchArgs,
cwd: process.cwd(),
customModels: codexSettings.customModels,
environment: resolvedEnvironment,
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
} from "../CodexDeveloperInstructions.ts";
import {
buildTurnStartParams,
codexSessionAppServerArgs,
hasConfiguredMcpServer,
isRecoverableThreadResumeError,
openCodexThread,
Expand Down Expand Up @@ -219,6 +220,16 @@
});
});

describe("codexSessionAppServerArgs", () => {
it("keeps the app-server subcommand when explicit args are provided", () => {
assert.deepStrictEqual(codexSessionAppServerArgs(["-c", "model=gpt-5"], undefined), [

Check failure on line 225 in apps/server/src/provider/Layers/CodexSessionRuntime.test.ts

View workflow job for this annotation

GitHub Actions / Test

src/provider/Layers/CodexSessionRuntime.test.ts > codexSessionAppServerArgs > keeps the app-server subcommand when explicit args are provided

ReferenceError: assert is not defined ❯ src/provider/Layers/CodexSessionRuntime.test.ts:225:5 ❯ ../../node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.1.24_@types+node@24.12.4_@voidzero-dev+vite-plus-core@0._88d14256e6158ed6afe78863bf7c5717/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/chunk-artifact.js:302:14 ❯ ../../node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.1.24_@types+node@24.12.4_@voidzero-dev+vite-plus-core@0._88d14256e6158ed6afe78863bf7c5717/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/chunk-artifact.js:1903:28 ❯ ../../node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.1.24_@types+node@24.12.4_@voidzero-dev+vite-plus-core@0._88d14256e6158ed6afe78863bf7c5717/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/chunk-artifact.js:2326:24 ❯ runWithCancel ../../node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.1.24_@types+node@24.12.4_@voidzero-dev+vite-plus-core@0._88d14256e6158ed6afe78863bf7c5717/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/chunk-artifact.js:2323:12 ❯ ../../node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.1.24_@types+node@24.12.4_@voidzero-dev+vite-plus-core@0._88d14256e6158ed6afe78863bf7c5717/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/chunk-artifact.js:2305:24 ❯ runWithTimeout ../../node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.1.24_@types+node@24.12.4_@voidzero-dev+vite-plus-core@0._88d14256e6158ed6afe78863bf7c5717/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/chunk-artifact.js:2272:12 ❯ ../../node_modules/.pnpm/@voidzero-dev+vite-plus-test@0.1.24_@types+node@24.12.4_@voidzero-dev+vite-plus-core@0._88d14256e6158ed6afe78863bf7c5717/node_modules/@voidzero-dev/vite-plus-test/dist/@vitest/runner/chunk-artifact.js:2955:70
"app-server",
"-c",
"model=gpt-5",
]);
});
});

describe("isRecoverableThreadResumeError", () => {
it("matches missing thread errors", () => {
NodeAssert.equal(
Expand Down
20 changes: 13 additions & 7 deletions apps/server/src/provider/Layers/CodexSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import * as CodexErrors from "effect-codex-app-server/errors";
import * as CodexRpc from "effect-codex-app-server/rpc";
import * as EffectCodexSchema from "effect-codex-app-server/schema";

import { buildCodexInitializeParams } from "./CodexProvider.ts";
import { buildCodexInitializeParams, codexAppServerArgs } from "./CodexProvider.ts";
import { expandHomePath } from "../../pathExpansion.ts";
import {
CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
Expand Down Expand Up @@ -66,6 +66,11 @@ export function hasConfiguredMcpServer(appServerArgs: ReadonlyArray<string> | un
return appServerArgs?.some((argument) => argument.includes("mcp_servers.")) === true;
}

export const codexSessionAppServerArgs = (
appServerArgs: ReadonlyArray<string> | undefined,
launchArgs: string | undefined,
) => (appServerArgs ? ["app-server", ...appServerArgs] : codexAppServerArgs(launchArgs));

export const CodexResumeCursorSchema = Schema.Struct({
threadId: Schema.String,
});
Expand Down Expand Up @@ -100,6 +105,7 @@ export interface CodexSessionRuntimeOptions {
readonly providerInstanceId?: ProviderInstanceId;
readonly binaryPath: string;
readonly homePath?: string;
readonly launchArgs?: string;
readonly environment?: NodeJS.ProcessEnv;
readonly cwd: string;
readonly runtimeMode: RuntimeMode;
Expand Down Expand Up @@ -719,11 +725,11 @@ export const makeCodexSessionRuntime = (
...(resolvedHomePath ? { CODEX_HOME: resolvedHomePath } : {}),
};
const extendEnv = options.environment === undefined;
const spawnCommand = yield* resolveSpawnCommand(
options.binaryPath,
["app-server", ...(options.appServerArgs ?? [])],
{ env, extendEnv },
);
const appServerArgs = codexSessionAppServerArgs(options.appServerArgs, options.launchArgs);
const spawnCommand = yield* resolveSpawnCommand(options.binaryPath, appServerArgs, {
env,
extendEnv,
});
const child = yield* spawner
.spawn(
ChildProcess.make(spawnCommand.command, spawnCommand.args, {
Expand All @@ -739,7 +745,7 @@ export const makeCodexSessionRuntime = (
Effect.mapError(
(cause) =>
new CodexErrors.CodexAppServerSpawnError({
command: `${options.binaryPath} app-server`,
command: `${options.binaryPath} ${appServerArgs.join(" ")}`,
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
cause,
}),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const makeCodexConfig = (overrides: Partial<CodexSettings>): CodexSettings => ({
binaryPath: "codex",
homePath: "",
shadowHomePath: "",
launchArgs: "",
customModels: [],
...overrides,
});
Expand Down
16 changes: 16 additions & 0 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const encodedDefaultServerSettings = encodeServerSettings(DEFAULT_SERVER_SETTING

const defaultClaudeSettings: ClaudeSettings = Schema.decodeSync(ClaudeSettings)({});
const defaultCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({});
const decodeCodexSettings = Schema.decodeSync(CodexSettings);
const disabledCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({
enabled: false,
});
Expand Down Expand Up @@ -346,6 +347,21 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
}),
);

it.effect("passes configured launch args to the Codex provider probe", () =>
Effect.gen(function* () {
let observedLaunchArgs: string | undefined;
const settings = decodeCodexSettings({ launchArgs: "--strict-config --enable foo" });

const status = yield* checkCodexProviderStatus(settings, (input) => {
observedLaunchArgs = input.launchArgs;
return Effect.succeed(makeCodexProbeSnapshot());
});

assert.strictEqual(status.status, "ready");
assert.strictEqual(observedLaunchArgs, "--strict-config --enable foo");
}),
);

it.effect("returns unauthenticated when app-server requires OpenAI auth", () =>
Effect.gen(function* () {
const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>
Expand Down
35 changes: 35 additions & 0 deletions apps/server/src/provider/Layers/codexLaunchArgs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import assert from "node:assert/strict";

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

import { codexAppServerArgs, codexExecLaunchArgs } from "./CodexProvider.ts";

describe("codexAppServerArgs", () => {
it("returns the app-server command for empty launch args", () => {
assert.deepStrictEqual(codexAppServerArgs(""), ["app-server"]);
});

it("appends whitespace-split launch args after app-server", () => {
assert.deepStrictEqual(codexAppServerArgs("--strict-config --enable foo"), [
"app-server",
"--strict-config",
"--enable",
"foo",
]);
});
});

describe("codexExecLaunchArgs", () => {
it("keeps shared codex flags and omits app-server-only flags", () => {
assert.deepStrictEqual(
codexExecLaunchArgs('--strict-config --enable foo --listen off --config model="gpt-5"'),
["--strict-config", "--enable", "foo", "--config", 'model="gpt-5"'],
);
});

it("does not pair value-taking flags with adjacent flags", () => {
assert.deepStrictEqual(codexExecLaunchArgs("--config --strict-config --enable --disable"), [
"--strict-config",
]);
});
});
2 changes: 2 additions & 0 deletions apps/server/src/serverSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
binaryPath: "/opt/homebrew/bin/codex",
homePath: "/Users/julius/.codex",
shadowHomePath: "",
launchArgs: "",
customModels: [],
});
assert.deepEqual(next.providers.claudeAgent, {
Expand Down Expand Up @@ -420,6 +421,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
binaryPath: "/opt/homebrew/bin/codex",
homePath: "",
shadowHomePath: "",
launchArgs: "",
customModels: [],
});
assert.deepEqual(next.providers.claudeAgent, {
Expand Down
46 changes: 45 additions & 1 deletion apps/server/src/textGeneration/CodexTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ function makeFakeCodexBinary(
requireServiceTier?: string;
requireReasoningEffort?: string;
forbidReasoningEffort?: boolean;
requireArg?: string;
forbidArg?: string;
stdinMustContain?: string;
stdinMustNotContain?: string;
},
Expand All @@ -50,6 +52,7 @@ function makeFakeCodexBinary(
codexPath,
[
"#!/bin/sh",
'original_args="$*"',
'output_path=""',
'seen_image="0"',
'seen_service_tier=""',
Expand Down Expand Up @@ -87,6 +90,22 @@ function makeFakeCodexBinary(
" shift",
"done",
'stdin_content="$(cat)"',
...(input.requireArg !== undefined
? [
`case " $original_args " in *" ${input.requireArg} "*) ;; *)`,
` printf "%s\\n" "missing arg: ${input.requireArg}" >&2`,
` exit 8`,
"esac",
]
: []),
...(input.forbidArg !== undefined
? [
`case " $original_args " in *" ${input.forbidArg} "*)`,
` printf "%s\\n" "forbidden arg: ${input.forbidArg}" >&2`,
` exit 9`,
"esac",
]
: []),
...(input.requireImage
? [
'if [ "$seen_image" != "1" ]; then',
Expand Down Expand Up @@ -166,16 +185,19 @@ function withFakeCodexEnv<A, E, R>(
requireServiceTier?: string;
requireReasoningEffort?: string;
forbidReasoningEffort?: boolean;
requireArg?: string;
forbidArg?: string;
stdinMustContain?: string;
stdinMustNotContain?: string;
launchArgs?: string;
},
effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect<A, E, R>,
) {
return Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-codex-text-" });
const codexPath = yield* makeFakeCodexBinary(tempDir, input);
const config = decodeCodexSettings({ binaryPath: codexPath });
const config = decodeCodexSettings({ binaryPath: codexPath, launchArgs: input.launchArgs });
const textGeneration = yield* makeCodexTextGeneration(config);
return yield* effectFn(textGeneration);
}).pipe(Effect.scoped);
Expand Down Expand Up @@ -237,6 +259,28 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
),
);

it.effect("passes exec-safe launch args into codex exec", () =>
withFakeCodexEnv(
{
output: JSON.stringify({
subject: "Add important change",
body: "",
}),
launchArgs: "--strict-config --listen off",
requireArg: "--strict-config",
forbidArg: "--listen",
},
(textGeneration) =>
textGeneration.generateCommitMessage({
cwd: process.cwd(),
branch: "feature/codex-effect",
stagedSummary: "M README.md",
stagedPatch: "diff --git a/README.md b/README.md",
modelSelection: DEFAULT_TEST_MODEL_SELECTION,
}),
),
);

it.effect("defaults git text generation codex effort to low", () =>
withFakeCodexEnv(
{
Expand Down
Loading
Loading