From 6af7cf54678a1f73d8927909f7d0713f9c3cbc62 Mon Sep 17 00:00:00 2001 From: jamesx0416 Date: Mon, 1 Jun 2026 22:51:24 +1000 Subject: [PATCH 01/15] Add Codex launch arguments setting --- .../src/provider/Layers/CodexAdapter.test.ts | 31 +++++++++++++++++++ .../src/provider/Layers/CodexAdapter.ts | 1 + .../src/provider/Layers/CodexProvider.ts | 10 +++++- .../provider/Layers/CodexSessionRuntime.ts | 6 ++-- .../ProviderInstanceRegistryLive.test.ts | 1 + .../provider/Layers/ProviderRegistry.test.ts | 16 ++++++++++ .../provider/Layers/codexLaunchArgs.test.ts | 20 ++++++++++++ apps/server/src/serverSettings.test.ts | 2 ++ .../components/KeybindingsToast.browser.tsx | 1 + .../settings/ProviderSettingsForm.test.ts | 1 + packages/contracts/src/settings.test.ts | 4 +++ packages/contracts/src/settings.ts | 12 +++++++ 12 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 apps/server/src/provider/Layers/codexLaunchArgs.test.ts diff --git a/apps/server/src/provider/Layers/CodexAdapter.test.ts b/apps/server/src/provider/Layers/CodexAdapter.test.ts index 3ae98ec248c..1e65970e455 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.test.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.test.ts @@ -359,6 +359,37 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { }), ); + 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); + 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(); diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts index 9893cf6c149..6a798b952ab 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -1385,6 +1385,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) diff --git a/apps/server/src/provider/Layers/CodexProvider.ts b/apps/server/src/provider/Layers/CodexProvider.ts index 4d793194c1d..5b5ac569b8a 100644 --- a/apps/server/src/provider/Layers/CodexProvider.ts +++ b/apps/server/src/provider/Layers/CodexProvider.ts @@ -248,9 +248,15 @@ export function buildCodexInitializeParams(): CodexSchema.V1InitializeParams { }; } +export const codexAppServerArgs = (launchArgs: string | undefined): ReadonlyArray => [ + "app-server", + ...(launchArgs ?? "").trim().split(/\s+/).filter(Boolean), +]; + const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(function* (input: { readonly binaryPath: string; readonly homePath?: string; + readonly launchArgs?: string; readonly cwd: string; readonly customModels?: ReadonlyArray; readonly environment?: NodeJS.ProcessEnv; @@ -263,7 +269,7 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun const clientContext = yield* Layer.build( CodexClient.layerCommand({ command: input.binaryPath, - args: ["app-server"], + args: codexAppServerArgs(input.launchArgs), cwd: input.cwd, env: { ...(input.environment ?? process.env), @@ -409,6 +415,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; readonly environment?: NodeJS.ProcessEnv; @@ -446,6 +453,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, diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index f9b9c6ab4fb..6e48573568f 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -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, @@ -97,6 +97,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; @@ -718,9 +719,10 @@ export const makeCodexSessionRuntime = ( ...(options.environment ?? process.env), ...(resolvedHomePath ? { CODEX_HOME: resolvedHomePath } : {}), }; + const appServerArgs = codexAppServerArgs(options.launchArgs); const child = yield* spawner .spawn( - ChildProcess.make(options.binaryPath, ["app-server"], { + ChildProcess.make(options.binaryPath, appServerArgs, { cwd: options.cwd, env, forceKillAfter: CODEX_APP_SERVER_FORCE_KILL_AFTER, diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts index 86f99c97326..0460d8552c6 100644 --- a/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts +++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts @@ -58,6 +58,7 @@ const makeCodexConfig = (overrides: Partial): CodexSettings => ({ binaryPath: "codex", homePath: "", shadowHomePath: "", + launchArgs: "", customModels: [], ...overrides, }); diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index fb6eb3b443d..36efffd62be 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -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, }); @@ -346,6 +347,21 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T }), ); + 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, () => diff --git a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts new file mode 100644 index 00000000000..c14e9708d11 --- /dev/null +++ b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts @@ -0,0 +1,20 @@ +import assert from "node:assert/strict"; + +import { describe, it } from "vitest"; + +import { codexAppServerArgs } 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", + ]); + }); +}); diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index d24f2ee2826..f499185f09b 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -120,6 +120,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, { @@ -361,6 +362,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { binaryPath: "/opt/homebrew/bin/codex", homePath: "", shadowHomePath: "", + launchArgs: "", customModels: [], }); assert.deepEqual(next.providers.claudeAgent, { diff --git a/apps/web/src/components/KeybindingsToast.browser.tsx b/apps/web/src/components/KeybindingsToast.browser.tsx index 63715f201c1..e9b8de31c4e 100644 --- a/apps/web/src/components/KeybindingsToast.browser.tsx +++ b/apps/web/src/components/KeybindingsToast.browser.tsx @@ -139,6 +139,7 @@ function createBaseServerConfig(): ServerConfig { binaryPath: "", homePath: "", shadowHomePath: "", + launchArgs: "", customModels: [], }, claudeAgent: { diff --git a/apps/web/src/components/settings/ProviderSettingsForm.test.ts b/apps/web/src/components/settings/ProviderSettingsForm.test.ts index 0d3bc5ae98a..b00de748e50 100644 --- a/apps/web/src/components/settings/ProviderSettingsForm.test.ts +++ b/apps/web/src/components/settings/ProviderSettingsForm.test.ts @@ -18,6 +18,7 @@ describe("ProviderSettingsForm helpers", () => { "binaryPath", "homePath", "shadowHomePath", + "launchArgs", ]); }); diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index 39695fe3b01..a907cd98a08 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -106,6 +106,7 @@ describe("ServerSettingsPatch string normalization", () => { codex: { binaryPath: " /opt/homebrew/bin/codex ", homePath: " ~/.codex ", + launchArgs: " --strict-config --enable foo ", }, }, providerInstances: { @@ -122,6 +123,7 @@ describe("ServerSettingsPatch string normalization", () => { expect(patch.observability?.otlpTracesUrl).toBe("http://localhost:4318/v1/traces"); expect(patch.providers?.codex?.binaryPath).toBe("/opt/homebrew/bin/codex"); expect(patch.providers?.codex?.homePath).toBe("~/.codex"); + expect(patch.providers?.codex?.launchArgs).toBe("--strict-config --enable foo"); expect(patch.providerInstances?.[ProviderInstanceId.make("codex_personal")]?.driver).toBe( "codex", ); @@ -143,11 +145,13 @@ describe("ServerSettingsPatch string normalization", () => { codex: { ...defaultSettings.providers.codex, binaryPath: " /opt/homebrew/bin/codex ", + launchArgs: " --strict-config ", }, }, }); expect(encoded.addProjectBaseDirectory).toBe("~/Development"); expect(encoded.providers?.codex?.binaryPath).toBe("/opt/homebrew/bin/codex"); + expect(encoded.providers?.codex?.launchArgs).toBe("--strict-config"); }); }); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 2d115eed98e..75d820d2427 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -191,6 +191,17 @@ export const CodexSettings = makeProviderSettingsSchema( }, }), ), + launchArgs: TrimmedString.pipe( + Schema.withDecodingDefault(Effect.succeed("")), + Schema.annotateKey({ + title: "Launch arguments", + description: "Additional CLI arguments passed to codex app-server on session start.", + providerSettingsForm: { + placeholder: "e.g. --strict-config", + clearWhenEmpty: "omit", + }, + }), + ), customModels: Schema.Array(Schema.String).pipe( Schema.withDecodingDefault(Effect.succeed([])), Schema.annotateKey({ providerSettingsForm: { hidden: true } }), @@ -419,6 +430,7 @@ const CodexSettingsPatch = Schema.Struct({ binaryPath: Schema.optionalKey(TrimmedString), homePath: Schema.optionalKey(TrimmedString), shadowHomePath: Schema.optionalKey(TrimmedString), + launchArgs: Schema.optionalKey(TrimmedString), customModels: Schema.optionalKey(Schema.Array(Schema.String)), }); From 8d4883abc610c1663d45da96198bb114a364c5aa Mon Sep 17 00:00:00 2001 From: jamesx0416 Date: Tue, 2 Jun 2026 09:27:22 +1000 Subject: [PATCH 02/15] Address Codex launch args review --- apps/server/src/provider/Layers/CodexSessionRuntime.ts | 2 +- packages/contracts/src/settings.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index 6e48573568f..b37cbac8caf 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -734,7 +734,7 @@ export const makeCodexSessionRuntime = ( Effect.mapError( (cause) => new CodexErrors.CodexAppServerSpawnError({ - command: `${options.binaryPath} app-server`, + command: `${options.binaryPath} ${appServerArgs.join(" ")}`, cause, }), ), diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 75d820d2427..49b8e9bb347 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -208,7 +208,7 @@ export const CodexSettings = makeProviderSettingsSchema( ), }, { - order: ["binaryPath", "homePath", "shadowHomePath"], + order: ["binaryPath", "homePath", "shadowHomePath", "launchArgs"], }, ); export type CodexSettings = typeof CodexSettings.Type; From e9608d9f4043b8abe65fadd2f267e7c5f6d167b2 Mon Sep 17 00:00:00 2001 From: jamesx0416 Date: Tue, 2 Jun 2026 09:39:32 +1000 Subject: [PATCH 03/15] Keep Codex launch args PR small --- apps/server/src/provider/Layers/CodexProvider.ts | 6 ++---- packages/contracts/src/settings.ts | 4 ---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/server/src/provider/Layers/CodexProvider.ts b/apps/server/src/provider/Layers/CodexProvider.ts index 5b5ac569b8a..5a9eaa8d386 100644 --- a/apps/server/src/provider/Layers/CodexProvider.ts +++ b/apps/server/src/provider/Layers/CodexProvider.ts @@ -248,10 +248,8 @@ export function buildCodexInitializeParams(): CodexSchema.V1InitializeParams { }; } -export const codexAppServerArgs = (launchArgs: string | undefined): ReadonlyArray => [ - "app-server", - ...(launchArgs ?? "").trim().split(/\s+/).filter(Boolean), -]; +export const codexAppServerArgs = (launchArgs?: string) => + launchArgs?.trim() ? ["app-server", ...launchArgs.trim().split(/\s+/)] : ["app-server"]; const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(function* (input: { readonly binaryPath: string; diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 49b8e9bb347..e4cc6c50e63 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -196,10 +196,6 @@ export const CodexSettings = makeProviderSettingsSchema( Schema.annotateKey({ title: "Launch arguments", description: "Additional CLI arguments passed to codex app-server on session start.", - providerSettingsForm: { - placeholder: "e.g. --strict-config", - clearWhenEmpty: "omit", - }, }), ), customModels: Schema.Array(Schema.String).pipe( From e0515167547e120165708dc9bb552972046b877f Mon Sep 17 00:00:00 2001 From: jamesx0416 Date: Tue, 2 Jun 2026 09:54:31 +1000 Subject: [PATCH 04/15] Fix Codex adapter launch args test expectation --- apps/server/src/provider/Layers/CodexAdapter.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/provider/Layers/CodexAdapter.test.ts b/apps/server/src/provider/Layers/CodexAdapter.test.ts index 1e65970e455..0f174732e77 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.test.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.test.ts @@ -279,6 +279,7 @@ validationLayer("CodexAdapterLive validation", (it) => { assert.deepStrictEqual(validationRuntimeFactory.factory.mock.calls[0]?.[0], { binaryPath: "codex", cwd: process.cwd(), + launchArgs: "", model: "gpt-5.3-codex", providerInstanceId: ProviderInstanceId.make("codex"), serviceTier: "fast", From 3d522fea9bbc5c7ad197f0529a6693dca9549c51 Mon Sep 17 00:00:00 2001 From: jamesx0416 Date: Thu, 4 Jun 2026 12:06:18 +1000 Subject: [PATCH 05/15] Forward shared Codex launch args to exec --- .../src/provider/Layers/CodexProvider.ts | 22 +++++++++ .../provider/Layers/codexLaunchArgs.test.ts | 11 ++++- .../CodexTextGeneration.test.ts | 46 ++++++++++++++++++- .../src/textGeneration/CodexTextGeneration.ts | 2 + 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/apps/server/src/provider/Layers/CodexProvider.ts b/apps/server/src/provider/Layers/CodexProvider.ts index 5a9eaa8d386..c043adb1d73 100644 --- a/apps/server/src/provider/Layers/CodexProvider.ts +++ b/apps/server/src/provider/Layers/CodexProvider.ts @@ -251,6 +251,28 @@ 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 = []; + 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) { + execArgs.push(value); + index++; + } + } else if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) { + execArgs.push(arg); + } + } + return execArgs; +}; + const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(function* (input: { readonly binaryPath: string; readonly homePath?: string; diff --git a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts index c14e9708d11..024aed8f4fc 100644 --- a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts +++ b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { describe, it } from "vitest"; -import { codexAppServerArgs } from "./CodexProvider.ts"; +import { codexAppServerArgs, codexExecLaunchArgs } from "./CodexProvider.ts"; describe("codexAppServerArgs", () => { it("returns the app-server command for empty launch args", () => { @@ -18,3 +18,12 @@ describe("codexAppServerArgs", () => { ]); }); }); + +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"'], + ); + }); +}); diff --git a/apps/server/src/textGeneration/CodexTextGeneration.test.ts b/apps/server/src/textGeneration/CodexTextGeneration.test.ts index 87a2f95fbf5..1c85bf8470f 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.test.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.test.ts @@ -35,6 +35,8 @@ function makeFakeCodexBinary( requireFastServiceTier?: boolean; requireReasoningEffort?: string; forbidReasoningEffort?: boolean; + requireArg?: string; + forbidArg?: string; stdinMustContain?: string; stdinMustNotContain?: string; }, @@ -50,6 +52,7 @@ function makeFakeCodexBinary( codexPath, [ "#!/bin/sh", + 'original_args="$*"', 'output_path=""', 'seen_image="0"', 'seen_fast_service_tier="0"', @@ -85,6 +88,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', @@ -164,8 +183,11 @@ function withFakeCodexEnv( requireFastServiceTier?: boolean; requireReasoningEffort?: string; forbidReasoningEffort?: boolean; + requireArg?: string; + forbidArg?: string; stdinMustContain?: string; stdinMustNotContain?: string; + launchArgs?: string; }, effectFn: (textGeneration: TextGenerationShape) => Effect.Effect, ) { @@ -173,7 +195,7 @@ function withFakeCodexEnv( 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); @@ -235,6 +257,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( { diff --git a/apps/server/src/textGeneration/CodexTextGeneration.ts b/apps/server/src/textGeneration/CodexTextGeneration.ts index 7f400d9ce50..ccf9ef064fa 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.ts @@ -13,6 +13,7 @@ import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shar import { resolveAttachmentPath } from "../attachmentStore.ts"; import { ServerConfig } from "../config.ts"; import { expandHomePath } from "../pathExpansion.ts"; +import { codexExecLaunchArgs } from "../provider/Layers/CodexProvider.ts"; import { TextGenerationError } from "@t3tools/contracts"; import { type BranchNameGenerationInput, @@ -187,6 +188,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func codexConfig.binaryPath || "codex", [ "exec", + ...codexExecLaunchArgs(codexConfig.launchArgs), "--ephemeral", "--skip-git-repo-check", "-s", From c714c38fc116c0b780d015c59a0eeb6ca814adcb Mon Sep 17 00:00:00 2001 From: jamesx0416 Date: Thu, 4 Jun 2026 14:36:22 +1000 Subject: [PATCH 06/15] Handle incomplete Codex exec launch flags --- apps/server/src/provider/Layers/CodexProvider.ts | 4 +++- apps/server/src/provider/Layers/codexLaunchArgs.test.ts | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/server/src/provider/Layers/CodexProvider.ts b/apps/server/src/provider/Layers/CodexProvider.ts index c043adb1d73..57d207516d5 100644 --- a/apps/server/src/provider/Layers/CodexProvider.ts +++ b/apps/server/src/provider/Layers/CodexProvider.ts @@ -262,9 +262,11 @@ export const codexExecLaunchArgs = (launchArgs?: string) => { } else if (arg === "--config" || arg === "-c" || arg === "--enable" || arg === "--disable") { execArgs.push(arg); const value = args[index + 1]; - if (value !== undefined) { + if (value !== undefined && !value.startsWith("-")) { execArgs.push(value); index++; + } else { + execArgs.pop(); } } else if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) { execArgs.push(arg); diff --git a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts index 024aed8f4fc..766104b2e78 100644 --- a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts +++ b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts @@ -26,4 +26,10 @@ describe("codexExecLaunchArgs", () => { ["--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", + ]); + }); }); From 518e65d1c07900ef2e0264f76e369d96122e6dc5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 5 Jun 2026 11:28:23 -0700 Subject: [PATCH 07/15] Change test framework import from vitest to vite-plus --- apps/server/src/provider/Layers/codexLaunchArgs.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts index 766104b2e78..eb2f2f5af65 100644 --- a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts +++ b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; -import { describe, it } from "vitest"; +import { describe, it } from "vite-plus/test"; import { codexAppServerArgs, codexExecLaunchArgs } from "./CodexProvider.ts"; From 0d5061a6d2e406d2261072af66a291327fcdf1d5 Mon Sep 17 00:00:00 2001 From: jamesx0416 Date: Sun, 21 Jun 2026 21:23:37 +1000 Subject: [PATCH 08/15] Fix Codex app-server args --- .../provider/Layers/CodexSessionRuntime.test.ts | 11 +++++++++++ .../src/provider/Layers/CodexSessionRuntime.ts | 17 ++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts index 2d303039856..ba16884ee02 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -13,6 +13,7 @@ import { } from "../CodexDeveloperInstructions.ts"; import { buildTurnStartParams, + codexSessionAppServerArgs, hasConfiguredMcpServer, isRecoverableThreadResumeError, openCodexThread, @@ -175,6 +176,16 @@ describe("hasConfiguredMcpServer", () => { }); }); +describe("codexSessionAppServerArgs", () => { + it("keeps the app-server subcommand when explicit args are provided", () => { + assert.deepStrictEqual(codexSessionAppServerArgs(["-c", "model=gpt-5"], undefined), [ + "app-server", + "-c", + "model=gpt-5", + ]); + }); +}); + describe("isRecoverableThreadResumeError", () => { it("matches missing thread errors", () => { assert.equal( diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index f908a2c486c..e53547523eb 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -67,6 +67,11 @@ export function hasConfiguredMcpServer(appServerArgs: ReadonlyArray | un return appServerArgs?.some((argument) => argument.includes("mcp_servers.")) === true; } +export const codexSessionAppServerArgs = ( + appServerArgs: ReadonlyArray | undefined, + launchArgs: string | undefined, +) => (appServerArgs ? ["app-server", ...appServerArgs] : codexAppServerArgs(launchArgs)); + export const CodexResumeCursorSchema = Schema.Struct({ threadId: Schema.String, }); @@ -726,13 +731,11 @@ export const makeCodexSessionRuntime = ( ...(resolvedHomePath ? { CODEX_HOME: resolvedHomePath } : {}), }; const extendEnv = options.environment === undefined; - const appServerArgs = - options.appServerArgs ?? codexAppServerArgs(options.launchArgs); - const spawnCommand = yield* resolveSpawnCommand( - options.binaryPath, - 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, { From 7b3b6d315ccd1da7d01b4dc00e2028be09870cb5 Mon Sep 17 00:00:00 2001 From: jamesx0416 Date: Sun, 21 Jun 2026 21:59:20 +1000 Subject: [PATCH 09/15] Fix Codex launch args checks --- apps/mobile/src/connection/catalog-store.ts | 15 +++++++++------ .../src/provider/Layers/CodexAdapter.test.ts | 4 ++-- .../provider/Layers/CodexSessionRuntime.test.ts | 2 +- .../src/provider/Layers/CodexSessionRuntime.ts | 2 +- .../src/provider/Layers/codexLaunchArgs.test.ts | 10 +++++----- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/apps/mobile/src/connection/catalog-store.ts b/apps/mobile/src/connection/catalog-store.ts index b5bda400670..6e9238849ef 100644 --- a/apps/mobile/src/connection/catalog-store.ts +++ b/apps/mobile/src/connection/catalog-store.ts @@ -15,6 +15,9 @@ import { migrateLegacyConnectionCatalog } from "./migration"; export const CONNECTION_CATALOG_KEY = "t3code.connection-catalog.v1"; export const LEGACY_CONNECTIONS_KEY = "t3code.connections"; +const decodeConnectionCatalogDocument = Schema.decodeUnknownResult(ConnectionCatalogDocument); +const encodeConnectionCatalogDocument = Schema.encodeUnknownResult(ConnectionCatalogDocument); + function catalogError(operation: string, cause: unknown) { return new ConnectionTransientError({ reason: "remote-unavailable", @@ -27,17 +30,17 @@ const decodeCatalog = Effect.fn("mobile.connectionStorage.decodeCatalog")(functi try: () => JSON.parse(raw) as unknown, catch: (cause) => catalogError("decode", cause), }); - return yield* Effect.fromResult( - Schema.decodeUnknownResult(ConnectionCatalogDocument)(parsed), - ).pipe(Effect.mapError((cause) => catalogError("decode", cause))); + return yield* Effect.fromResult(decodeConnectionCatalogDocument(parsed)).pipe( + Effect.mapError((cause) => catalogError("decode", cause)), + ); }); const encodeCatalog = Effect.fn("mobile.connectionStorage.encodeCatalog")(function* ( catalog: ConnectionCatalogDocumentType, ) { - const encoded = yield* Effect.fromResult( - Schema.encodeUnknownResult(ConnectionCatalogDocument)(catalog), - ).pipe(Effect.mapError((cause) => catalogError("encode", cause))); + const encoded = yield* Effect.fromResult(encodeConnectionCatalogDocument(catalog)).pipe( + Effect.mapError((cause) => catalogError("encode", cause)), + ); return JSON.stringify(encoded); }); diff --git a/apps/server/src/provider/Layers/CodexAdapter.test.ts b/apps/server/src/provider/Layers/CodexAdapter.test.ts index d02972d9b85..f8e18c137f0 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.test.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.test.ts @@ -386,8 +386,8 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { }); const runtime = runtimeFactory.lastRuntime; - assert.ok(runtime); - assert.equal(runtime.options.launchArgs, "--strict-config --enable foo"); + NodeAssert.ok(runtime); + NodeAssert.equal(runtime.options.launchArgs, "--strict-config --enable foo"); }).pipe(Effect.provide(layer)); }); diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts index 9b051069525..3cd60b40e51 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -222,7 +222,7 @@ describe("hasConfiguredMcpServer", () => { describe("codexSessionAppServerArgs", () => { it("keeps the app-server subcommand when explicit args are provided", () => { - assert.deepStrictEqual(codexSessionAppServerArgs(["-c", "model=gpt-5"], undefined), [ + NodeAssert.deepStrictEqual(codexSessionAppServerArgs(["-c", "model=gpt-5"], undefined), [ "app-server", "-c", "model=gpt-5", diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index ea4ecb67ef8..7ef64856b1b 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -745,7 +745,7 @@ export const makeCodexSessionRuntime = ( Effect.mapError( (cause) => new CodexErrors.CodexAppServerSpawnError({ - command: `${options.binaryPath} ${appServerArgs.join(" ")}`, + command: `${options.binaryPath} app-server`, cause, }), ), diff --git a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts index eb2f2f5af65..b1e45e1bbe0 100644 --- a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts +++ b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts @@ -1,4 +1,4 @@ -import assert from "node:assert/strict"; +import * as NodeAssert from "node:assert/strict"; import { describe, it } from "vite-plus/test"; @@ -6,11 +6,11 @@ import { codexAppServerArgs, codexExecLaunchArgs } from "./CodexProvider.ts"; describe("codexAppServerArgs", () => { it("returns the app-server command for empty launch args", () => { - assert.deepStrictEqual(codexAppServerArgs(""), ["app-server"]); + NodeAssert.deepStrictEqual(codexAppServerArgs(""), ["app-server"]); }); it("appends whitespace-split launch args after app-server", () => { - assert.deepStrictEqual(codexAppServerArgs("--strict-config --enable foo"), [ + NodeAssert.deepStrictEqual(codexAppServerArgs("--strict-config --enable foo"), [ "app-server", "--strict-config", "--enable", @@ -21,14 +21,14 @@ describe("codexAppServerArgs", () => { describe("codexExecLaunchArgs", () => { it("keeps shared codex flags and omits app-server-only flags", () => { - assert.deepStrictEqual( + NodeAssert.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"), [ + NodeAssert.deepStrictEqual(codexExecLaunchArgs("--config --strict-config --enable --disable"), [ "--strict-config", ]); }); From ccfabb27ec68997d2ca23814d6d8393dfde6fdce Mon Sep 17 00:00:00 2001 From: root Date: Tue, 30 Jun 2026 12:08:02 +0000 Subject: [PATCH 10/15] Add codex launch args env override --- .../src/provider/Layers/CodexAdapter.test.ts | 32 +++++++++++++++++++ .../src/provider/Layers/CodexAdapter.ts | 3 +- .../src/provider/Layers/CodexProvider.ts | 9 +++++- .../provider/Layers/codexLaunchArgs.test.ts | 26 ++++++++++++++- .../CodexTextGeneration.test.ts | 26 ++++++++++++++- .../src/textGeneration/CodexTextGeneration.ts | 5 +-- 6 files changed, 95 insertions(+), 6 deletions(-) diff --git a/apps/server/src/provider/Layers/CodexAdapter.test.ts b/apps/server/src/provider/Layers/CodexAdapter.test.ts index f8e18c137f0..4ae654a5187 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.test.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.test.ts @@ -391,6 +391,38 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { }).pipe(Effect.provide(layer)); }); + it.effect("uses T3CODE_CODEX_LAUNCH_ARGS for the session runtime", () => { + const runtimeFactory = makeRuntimeFactory(); + const layer = Layer.effect( + CodexAdapter, + Effect.gen(function* () { + const codexConfig = decodeCodexSettings({ launchArgs: "--enable settings-feature" }); + return yield* makeCodexAdapter(codexConfig, { + environment: { T3CODE_CODEX_LAUNCH_ARGS: " --strict-config --enable env-feature " }, + 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-env"), + runtimeMode: "full-access", + }); + + const runtime = runtimeFactory.lastRuntime; + NodeAssert.ok(runtime); + NodeAssert.equal(runtime.options.launchArgs, "--strict-config --enable env-feature"); + }).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(); diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts index 9b956d461b0..105a88d1439 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -61,6 +61,7 @@ import { type CodexSessionRuntimeShape, } from "./CodexSessionRuntime.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; +import { resolveCodexLaunchArgs } from "./CodexProvider.ts"; const isCodexAppServerProcessExitedError = Schema.is(CodexErrors.CodexAppServerProcessExitedError); const isCodexAppServerTransportError = Schema.is(CodexErrors.CodexAppServerTransportError); const isCodexSessionRuntimeThreadIdMissingError = Schema.is( @@ -1392,7 +1393,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( providerInstanceId: boundInstanceId, cwd: input.cwd ?? process.cwd(), binaryPath: codexConfig.binaryPath, - launchArgs: codexConfig.launchArgs, + launchArgs: resolveCodexLaunchArgs(codexConfig.launchArgs, options?.environment), ...(options?.environment ? { environment: options.environment } : {}), ...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}), ...(isCodexResumeCursorSchema(input.resumeCursor) diff --git a/apps/server/src/provider/Layers/CodexProvider.ts b/apps/server/src/provider/Layers/CodexProvider.ts index 0cab7d89bf2..925b486dfeb 100644 --- a/apps/server/src/provider/Layers/CodexProvider.ts +++ b/apps/server/src/provider/Layers/CodexProvider.ts @@ -284,6 +284,13 @@ export function buildCodexInitializeParams(): CodexSchema.V1InitializeParams { }; } +export const T3CODE_CODEX_LAUNCH_ARGS_ENV = "T3CODE_CODEX_LAUNCH_ARGS"; + +export const resolveCodexLaunchArgs = ( + launchArgs?: string, + environment: NodeJS.ProcessEnv = process.env, +) => environment[T3CODE_CODEX_LAUNCH_ARGS_ENV]?.trim() || launchArgs?.trim() || ""; + export const codexAppServerArgs = (launchArgs?: string) => launchArgs?.trim() ? ["app-server", ...launchArgs.trim().split(/\s+/)] : ["app-server"]; @@ -534,7 +541,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu const probeResult = yield* probe({ binaryPath: codexSettings.binaryPath, homePath: codexSettings.homePath, - launchArgs: codexSettings.launchArgs, + launchArgs: resolveCodexLaunchArgs(codexSettings.launchArgs, resolvedEnvironment), cwd: process.cwd(), customModels: codexSettings.customModels, environment: resolvedEnvironment, diff --git a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts index b1e45e1bbe0..741eb3d6c33 100644 --- a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts +++ b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts @@ -2,7 +2,31 @@ import * as NodeAssert from "node:assert/strict"; import { describe, it } from "vite-plus/test"; -import { codexAppServerArgs, codexExecLaunchArgs } from "./CodexProvider.ts"; +import { + codexAppServerArgs, + codexExecLaunchArgs, + resolveCodexLaunchArgs, +} from "./CodexProvider.ts"; + +describe("resolveCodexLaunchArgs", () => { + it("uses T3CODE_CODEX_LAUNCH_ARGS before configured settings", () => { + NodeAssert.equal( + resolveCodexLaunchArgs(" --strict-config ", { T3CODE_CODEX_LAUNCH_ARGS: "--enable foo" }), + "--enable foo", + ); + }); + + it("uses configured settings when T3CODE_CODEX_LAUNCH_ARGS is empty", () => { + NodeAssert.equal( + resolveCodexLaunchArgs(" --strict-config ", { T3CODE_CODEX_LAUNCH_ARGS: " " }), + "--strict-config", + ); + }); + + it("ignores whitespace-only environment values", () => { + NodeAssert.equal(resolveCodexLaunchArgs("", { T3CODE_CODEX_LAUNCH_ARGS: " " }), ""); + }); +}); describe("codexAppServerArgs", () => { it("returns the app-server command for empty launch args", () => { diff --git a/apps/server/src/textGeneration/CodexTextGeneration.test.ts b/apps/server/src/textGeneration/CodexTextGeneration.test.ts index 7b2fc6ee3ac..657118fff51 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.test.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.test.ts @@ -190,6 +190,7 @@ function withFakeCodexEnv( stdinMustContain?: string; stdinMustNotContain?: string; launchArgs?: string; + environment?: NodeJS.ProcessEnv; }, effectFn: (textGeneration: TextGeneration.TextGeneration["Service"]) => Effect.Effect, ) { @@ -198,7 +199,7 @@ function withFakeCodexEnv( const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-codex-text-" }); const codexPath = yield* makeFakeCodexBinary(tempDir, input); const config = decodeCodexSettings({ binaryPath: codexPath, launchArgs: input.launchArgs }); - const textGeneration = yield* makeCodexTextGeneration(config); + const textGeneration = yield* makeCodexTextGeneration(config, input.environment); return yield* effectFn(textGeneration); }).pipe(Effect.scoped); } @@ -281,6 +282,29 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => { ), ); + it.effect("uses T3CODE_CODEX_LAUNCH_ARGS for codex exec over settings", () => + withFakeCodexEnv( + { + output: JSON.stringify({ + subject: "Add important change", + body: "", + }), + launchArgs: "--enable settings-feature", + environment: { T3CODE_CODEX_LAUNCH_ARGS: " --strict-config --listen off " }, + requireArg: "--strict-config", + forbidArg: "settings-feature", + }, + (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( { diff --git a/apps/server/src/textGeneration/CodexTextGeneration.ts b/apps/server/src/textGeneration/CodexTextGeneration.ts index 9deb9755ec2..1a3e8fdca1a 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.ts @@ -14,7 +14,7 @@ import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { resolveAttachmentPath } from "../attachmentStore.ts"; import * as ServerConfig from "../config.ts"; import { expandHomePath } from "../pathExpansion.ts"; -import { codexExecLaunchArgs } from "../provider/Layers/CodexProvider.ts"; +import { codexExecLaunchArgs, resolveCodexLaunchArgs } from "../provider/Layers/CodexProvider.ts"; import { TextGenerationError } from "@t3tools/contracts"; import * as TextGeneration from "./TextGeneration.ts"; import { @@ -175,6 +175,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func const outputPath = yield* writeTempFile(operation, "codex-output", ""); const runCodexCommand = Effect.fn("runCodexJson.runCodexCommand")(function* () { + const launchArgs = resolveCodexLaunchArgs(codexConfig.launchArgs, resolvedEnvironment); const reasoningEffort = getModelSelectionStringOptionValue(modelSelection, "reasoningEffort") ?? CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT; @@ -183,7 +184,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func codexConfig.binaryPath || "codex", [ "exec", - ...codexExecLaunchArgs(codexConfig.launchArgs), + ...codexExecLaunchArgs(launchArgs), "--ephemeral", "--skip-git-repo-check", "-s", From 5304b45fffbffded5cee9fb805d3ccb8e4f18e77 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 30 Jun 2026 23:04:26 +0000 Subject: [PATCH 11/15] Preserve launch args for MCP Codex sessions --- .../provider/Layers/CodexSessionRuntime.test.ts | 17 +++++++++++++++++ .../src/provider/Layers/CodexSessionRuntime.ts | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts index 3cd60b40e51..a17ad27cbd4 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -228,6 +228,23 @@ describe("codexSessionAppServerArgs", () => { "model=gpt-5", ]); }); + + it("keeps launch args when explicit app-server args are provided", () => { + NodeAssert.deepStrictEqual( + codexSessionAppServerArgs( + ["-c", "mcp_servers.t3-code.url=http://127.0.0.1/mcp"], + "--strict-config --enable foo", + ), + [ + "app-server", + "--strict-config", + "--enable", + "foo", + "-c", + "mcp_servers.t3-code.url=http://127.0.0.1/mcp", + ], + ); + }); }); describe("isRecoverableThreadResumeError", () => { diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index 7ef64856b1b..b22327d73c2 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -69,7 +69,10 @@ export function hasConfiguredMcpServer(appServerArgs: ReadonlyArray | un export const codexSessionAppServerArgs = ( appServerArgs: ReadonlyArray | undefined, launchArgs: string | undefined, -) => (appServerArgs ? ["app-server", ...appServerArgs] : codexAppServerArgs(launchArgs)); +) => { + const launchAppServerArgs = codexAppServerArgs(launchArgs); + return appServerArgs ? [...launchAppServerArgs, ...appServerArgs] : launchAppServerArgs; +}; export const CodexResumeCursorSchema = Schema.Struct({ threadId: Schema.String, From 036a8dd0f761ff760c645376b56f71f971e93d36 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 1 Jul 2026 00:04:52 +0000 Subject: [PATCH 12/15] Parse quoted Codex launch args --- .../src/provider/Layers/CodexProvider.ts | 65 ++++++++++++++++++- .../provider/Layers/codexLaunchArgs.test.ts | 18 ++++- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/apps/server/src/provider/Layers/CodexProvider.ts b/apps/server/src/provider/Layers/CodexProvider.ts index 925b486dfeb..b42f4efc401 100644 --- a/apps/server/src/provider/Layers/CodexProvider.ts +++ b/apps/server/src/provider/Layers/CodexProvider.ts @@ -291,11 +291,70 @@ export const resolveCodexLaunchArgs = ( environment: NodeJS.ProcessEnv = process.env, ) => environment[T3CODE_CODEX_LAUNCH_ARGS_ENV]?.trim() || launchArgs?.trim() || ""; -export const codexAppServerArgs = (launchArgs?: string) => - launchArgs?.trim() ? ["app-server", ...launchArgs.trim().split(/\s+/)] : ["app-server"]; +export const codexLaunchArgv = (launchArgs?: string): ReadonlyArray => { + const input = launchArgs?.trim(); + if (!input) return []; + + const args: string[] = []; + let current = ""; + let quote: "'" | '"' | undefined; + let quoted = false; + + for (let index = 0; index < input.length; index++) { + const char = input[index]; + if (char === undefined) continue; + + if (quote) { + if (char === quote) { + quote = undefined; + quoted = true; + } else if (char === "\\" && quote === '"') { + const next = input[index + 1]; + if (next !== undefined) { + current += next; + index++; + } else { + current += char; + } + } else { + current += char; + } + continue; + } + + if (char === "'" || char === '"') { + quote = char; + quoted = true; + } else if (/\s/.test(char)) { + if (current || quoted) { + args.push(current); + current = ""; + quoted = false; + } + } else if (char === "\\") { + const next = input[index + 1]; + if (next !== undefined) { + current += next; + index++; + } else { + current += char; + } + } else { + current += char; + } + } + + if (current || quoted) args.push(current); + return args; +}; + +export const codexAppServerArgs = (launchArgs?: string) => [ + "app-server", + ...codexLaunchArgv(launchArgs), +]; export const codexExecLaunchArgs = (launchArgs?: string) => { - const args = launchArgs?.trim().split(/\s+/).filter(Boolean) ?? []; + const args = codexLaunchArgv(launchArgs); const execArgs: Array = []; for (let index = 0; index < args.length; index++) { const arg = args[index]; diff --git a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts index 741eb3d6c33..b4913f971c6 100644 --- a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts +++ b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts @@ -5,6 +5,7 @@ import { describe, it } from "vite-plus/test"; import { codexAppServerArgs, codexExecLaunchArgs, + codexLaunchArgv, resolveCodexLaunchArgs, } from "./CodexProvider.ts"; @@ -28,12 +29,23 @@ describe("resolveCodexLaunchArgs", () => { }); }); +describe("codexLaunchArgv", () => { + it("preserves quoted values and escaped spaces", () => { + NodeAssert.deepStrictEqual( + codexLaunchArgv( + String.raw`--config model="gpt 5" --enable foo\ bar --config=profile='work profile'`, + ), + ["--config", "model=gpt 5", "--enable", "foo bar", "--config=profile=work profile"], + ); + }); +}); + describe("codexAppServerArgs", () => { it("returns the app-server command for empty launch args", () => { NodeAssert.deepStrictEqual(codexAppServerArgs(""), ["app-server"]); }); - it("appends whitespace-split launch args after app-server", () => { + it("appends parsed launch args after app-server", () => { NodeAssert.deepStrictEqual(codexAppServerArgs("--strict-config --enable foo"), [ "app-server", "--strict-config", @@ -46,8 +58,8 @@ describe("codexAppServerArgs", () => { describe("codexExecLaunchArgs", () => { it("keeps shared codex flags and omits app-server-only flags", () => { NodeAssert.deepStrictEqual( - codexExecLaunchArgs('--strict-config --enable foo --listen off --config model="gpt-5"'), - ["--strict-config", "--enable", "foo", "--config", 'model="gpt-5"'], + codexExecLaunchArgs('--strict-config --enable foo --listen off --config model="gpt 5"'), + ["--strict-config", "--enable", "foo", "--config", "model=gpt 5"], ); }); From 568ae0eef71934faaea04d5d9a45c3d55e586b80 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 1 Jul 2026 00:21:56 +0000 Subject: [PATCH 13/15] Preserve backslashes in Codex launch args --- apps/server/src/provider/Layers/CodexProvider.ts | 4 ++-- apps/server/src/provider/Layers/codexLaunchArgs.test.ts | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/server/src/provider/Layers/CodexProvider.ts b/apps/server/src/provider/Layers/CodexProvider.ts index b42f4efc401..fe7b1f1b5f4 100644 --- a/apps/server/src/provider/Layers/CodexProvider.ts +++ b/apps/server/src/provider/Layers/CodexProvider.ts @@ -310,7 +310,7 @@ export const codexLaunchArgv = (launchArgs?: string): ReadonlyArray => { quoted = true; } else if (char === "\\" && quote === '"') { const next = input[index + 1]; - if (next !== undefined) { + if (next !== undefined && ['"', "\\", "$", "`"].includes(next)) { current += next; index++; } else { @@ -333,7 +333,7 @@ export const codexLaunchArgv = (launchArgs?: string): ReadonlyArray => { } } else if (char === "\\") { const next = input[index + 1]; - if (next !== undefined) { + if (next !== undefined && /\s/.test(next)) { current += next; index++; } else { diff --git a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts index b4913f971c6..70462e751ed 100644 --- a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts +++ b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts @@ -38,6 +38,13 @@ describe("codexLaunchArgv", () => { ["--config", "model=gpt 5", "--enable", "foo bar", "--config=profile=work profile"], ); }); + + it("preserves literal backslashes in path values", () => { + NodeAssert.deepStrictEqual( + codexLaunchArgv(String.raw`--config cacheDir=C:\Users\me --config "quoted=C:\Users\me"`), + ["--config", String.raw`cacheDir=C:\Users\me`, "--config", String.raw`quoted=C:\Users\me`], + ); + }); }); describe("codexAppServerArgs", () => { From bfa7715c260b6cb07ee572b4b1e26a00254c64c6 Mon Sep 17 00:00:00 2001 From: James <105842516+jamesx0416@users.noreply.github.com> Date: Fri, 10 Jul 2026 04:37:12 +0000 Subject: [PATCH 14/15] Resolve catalog store merge conflict --- apps/mobile/src/connection/catalog-store.ts | 46 ++++++++++----------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/apps/mobile/src/connection/catalog-store.ts b/apps/mobile/src/connection/catalog-store.ts index 6e9238849ef..6a4fcd35f6d 100644 --- a/apps/mobile/src/connection/catalog-store.ts +++ b/apps/mobile/src/connection/catalog-store.ts @@ -10,14 +10,12 @@ import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Semaphore from "effect/Semaphore"; +import * as MobileSecureStorage from "../persistence/mobile-secure-storage"; import { migrateLegacyConnectionCatalog } from "./migration"; export const CONNECTION_CATALOG_KEY = "t3code.connection-catalog.v1"; export const LEGACY_CONNECTIONS_KEY = "t3code.connections"; -const decodeConnectionCatalogDocument = Schema.decodeUnknownResult(ConnectionCatalogDocument); -const encodeConnectionCatalogDocument = Schema.encodeUnknownResult(ConnectionCatalogDocument); - function catalogError(operation: string, cause: unknown) { return new ConnectionTransientError({ reason: "remote-unavailable", @@ -25,12 +23,12 @@ function catalogError(operation: string, cause: unknown) { }); } +const ConnectionCatalogDocumentJson = Schema.fromJsonString(ConnectionCatalogDocument); +const decodeConnectionCatalogDocument = Schema.decodeEffect(ConnectionCatalogDocumentJson); +const encodeConnectionCatalogDocument = Schema.encodeEffect(ConnectionCatalogDocumentJson); + const decodeCatalog = Effect.fn("mobile.connectionStorage.decodeCatalog")(function* (raw: string) { - const parsed = yield* Effect.try({ - try: () => JSON.parse(raw) as unknown, - catch: (cause) => catalogError("decode", cause), - }); - return yield* Effect.fromResult(decodeConnectionCatalogDocument(parsed)).pipe( + return yield* decodeConnectionCatalogDocument(raw).pipe( Effect.mapError((cause) => catalogError("decode", cause)), ); }); @@ -38,10 +36,9 @@ const decodeCatalog = Effect.fn("mobile.connectionStorage.decodeCatalog")(functi const encodeCatalog = Effect.fn("mobile.connectionStorage.encodeCatalog")(function* ( catalog: ConnectionCatalogDocumentType, ) { - const encoded = yield* Effect.fromResult(encodeConnectionCatalogDocument(catalog)).pipe( + return yield* encodeConnectionCatalogDocument(catalog).pipe( Effect.mapError((cause) => catalogError("encode", cause)), ); - return JSON.stringify(encoded); }); interface CatalogStore { @@ -51,20 +48,19 @@ interface CatalogStore { ) => Effect.Effect; } -export interface SecureCatalogStorage { - readonly getItem: (key: string) => Effect.Effect; - readonly setItem: (key: string, value: string) => Effect.Effect; - readonly deleteItem: (key: string) => Effect.Effect; -} - -export const makeCatalogStore = Effect.fn("mobile.connectionStorage.makeCatalogStore")(function* ( - storage: SecureCatalogStorage, -) { +export const make = Effect.fn("mobile.connectionStorage.makeCatalogStore")(function* () { + const storage = yield* MobileSecureStorage.MobileSecureStorage; + const getItem = (key: string) => + storage.getItem(key).pipe(Effect.mapError((cause) => catalogError("load", cause))); + const setItem = (key: string, value: string) => + storage.setItem(key, value).pipe(Effect.mapError((cause) => catalogError("save", cause))); + const deleteItem = (key: string) => + storage.removeItem(key).pipe(Effect.mapError((cause) => catalogError("delete", cause))); const state = yield* Ref.make>(Option.none()); const lock = yield* Semaphore.make(1); const loadLegacyCatalog = Effect.fn("mobile.connectionStorage.loadLegacyCatalog")(function* () { - const legacyRaw = yield* storage.getItem(LEGACY_CONNECTIONS_KEY); + const legacyRaw = yield* getItem(LEGACY_CONNECTIONS_KEY); const catalog = legacyRaw === null || legacyRaw.trim() === "" ? EMPTY_CONNECTION_CATALOG_DOCUMENT @@ -78,8 +74,8 @@ export const makeCatalogStore = Effect.fn("mobile.connectionStorage.makeCatalogS ); if (legacyRaw !== null && legacyRaw.trim() !== "") { const encoded = yield* encodeCatalog(catalog); - yield* storage.setItem(CONNECTION_CATALOG_KEY, encoded); - yield* storage.deleteItem(LEGACY_CONNECTIONS_KEY); + yield* setItem(CONNECTION_CATALOG_KEY, encoded); + yield* deleteItem(LEGACY_CONNECTIONS_KEY); } return catalog; }); @@ -89,13 +85,13 @@ export const makeCatalogStore = Effect.fn("mobile.connectionStorage.makeCatalogS if (Option.isSome(cached)) { return cached.value; } - const raw = yield* storage.getItem(CONNECTION_CATALOG_KEY); + const raw = yield* getItem(CONNECTION_CATALOG_KEY); let catalog: ConnectionCatalogDocumentType; if (raw !== null && raw.trim() !== "") { catalog = yield* decodeCatalog(raw).pipe( Effect.catch((error) => Effect.logWarning("Discarding corrupt mobile connection catalog", error).pipe( - Effect.andThen(storage.deleteItem(CONNECTION_CATALOG_KEY)), + Effect.andThen(deleteItem(CONNECTION_CATALOG_KEY)), Effect.andThen(loadLegacyCatalog()), ), ), @@ -114,7 +110,7 @@ export const makeCatalogStore = Effect.fn("mobile.connectionStorage.makeCatalogS Effect.gen(function* () { const next = transform(yield* loadUnlocked()); const encoded = yield* encodeCatalog(next); - yield* storage.setItem(CONNECTION_CATALOG_KEY, encoded); + yield* setItem(CONNECTION_CATALOG_KEY, encoded); yield* Ref.set(state, Option.some(next)); }), ); From 04cb6cdd68273d2d99f5812686773ee7edca698a Mon Sep 17 00:00:00 2001 From: James <105842516+jamesx0416@users.noreply.github.com> Date: Fri, 10 Jul 2026 07:00:41 +0000 Subject: [PATCH 15/15] Share quote-aware CLI arg tokenization --- .../src/provider/Layers/CodexAdapter.ts | 2 +- .../src/provider/Layers/CodexProvider.ts | 94 +------------------ .../Layers/CodexSessionRuntime.test.ts | 2 +- .../provider/Layers/CodexSessionRuntime.ts | 11 +-- .../provider/Layers/codexLaunchArgs.test.ts | 21 +---- .../src/provider/Layers/codexLaunchArgs.ts | 48 ++++++++++ .../src/textGeneration/CodexTextGeneration.ts | 2 +- packages/shared/src/cliArgs.test.ts | 30 +++++- packages/shared/src/cliArgs.ts | 62 +++++++++++- 9 files changed, 143 insertions(+), 129 deletions(-) create mode 100644 apps/server/src/provider/Layers/codexLaunchArgs.ts diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts index 105a88d1439..a2d84d5ec1a 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -61,7 +61,7 @@ import { type CodexSessionRuntimeShape, } from "./CodexSessionRuntime.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; -import { resolveCodexLaunchArgs } from "./CodexProvider.ts"; +import { resolveCodexLaunchArgs } from "./codexLaunchArgs.ts"; const isCodexAppServerProcessExitedError = Schema.is(CodexErrors.CodexAppServerProcessExitedError); const isCodexAppServerTransportError = Schema.is(CodexErrors.CodexAppServerTransportError); const isCodexSessionRuntimeThreadIdMissingError = Schema.is( diff --git a/apps/server/src/provider/Layers/CodexProvider.ts b/apps/server/src/provider/Layers/CodexProvider.ts index 4a8801810a5..9306087a0bc 100644 --- a/apps/server/src/provider/Layers/CodexProvider.ts +++ b/apps/server/src/provider/Layers/CodexProvider.ts @@ -26,6 +26,7 @@ import { ServerSettingsError } from "@t3tools/contracts"; import { createModelCapabilities } from "@t3tools/shared/model"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; +import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts"; import { AUTH_PROBE_TIMEOUT_MS, buildServerProvider, @@ -286,99 +287,6 @@ export function buildCodexInitializeParams(): CodexSchema.V1InitializeParams { }; } -export const T3CODE_CODEX_LAUNCH_ARGS_ENV = "T3CODE_CODEX_LAUNCH_ARGS"; - -export const resolveCodexLaunchArgs = ( - launchArgs?: string, - environment: NodeJS.ProcessEnv = process.env, -) => environment[T3CODE_CODEX_LAUNCH_ARGS_ENV]?.trim() || launchArgs?.trim() || ""; - -export const codexLaunchArgv = (launchArgs?: string): ReadonlyArray => { - const input = launchArgs?.trim(); - if (!input) return []; - - const args: string[] = []; - let current = ""; - let quote: "'" | '"' | undefined; - let quoted = false; - - for (let index = 0; index < input.length; index++) { - const char = input[index]; - if (char === undefined) continue; - - if (quote) { - if (char === quote) { - quote = undefined; - quoted = true; - } else if (char === "\\" && quote === '"') { - const next = input[index + 1]; - if (next !== undefined && ['"', "\\", "$", "`"].includes(next)) { - current += next; - index++; - } else { - current += char; - } - } else { - current += char; - } - continue; - } - - if (char === "'" || char === '"') { - quote = char; - quoted = true; - } else if (/\s/.test(char)) { - if (current || quoted) { - args.push(current); - current = ""; - quoted = false; - } - } else if (char === "\\") { - const next = input[index + 1]; - if (next !== undefined && /\s/.test(next)) { - current += next; - index++; - } else { - current += char; - } - } else { - current += char; - } - } - - if (current || quoted) args.push(current); - return args; -}; - -export const codexAppServerArgs = (launchArgs?: string) => [ - "app-server", - ...codexLaunchArgv(launchArgs), -]; - -export const codexExecLaunchArgs = (launchArgs?: string) => { - const args = codexLaunchArgv(launchArgs); - const execArgs: Array = []; - 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; -}; - const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(function* (input: { readonly binaryPath: string; readonly homePath?: string; diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts index a17ad27cbd4..4b313794527 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.test.ts @@ -12,9 +12,9 @@ import { CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS, CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS, } from "../CodexDeveloperInstructions.ts"; +import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts"; import { buildTurnStartParams, - codexSessionAppServerArgs, hasConfiguredMcpServer, isRecoverableThreadResumeError, openCodexThread, diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index b22327d73c2..1474966ee12 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -35,7 +35,8 @@ 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, codexAppServerArgs } from "./CodexProvider.ts"; +import { buildCodexInitializeParams } from "./CodexProvider.ts"; +import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts"; import { expandHomePath } from "../../pathExpansion.ts"; import { CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS, @@ -66,14 +67,6 @@ export function hasConfiguredMcpServer(appServerArgs: ReadonlyArray | un return appServerArgs?.some((argument) => argument.includes("mcp_servers.")) === true; } -export const codexSessionAppServerArgs = ( - appServerArgs: ReadonlyArray | undefined, - launchArgs: string | undefined, -) => { - const launchAppServerArgs = codexAppServerArgs(launchArgs); - return appServerArgs ? [...launchAppServerArgs, ...appServerArgs] : launchAppServerArgs; -}; - export const CodexResumeCursorSchema = Schema.Struct({ threadId: Schema.String, }); diff --git a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts index 70462e751ed..115ac28eaf9 100644 --- a/apps/server/src/provider/Layers/codexLaunchArgs.test.ts +++ b/apps/server/src/provider/Layers/codexLaunchArgs.test.ts @@ -5,9 +5,8 @@ import { describe, it } from "vite-plus/test"; import { codexAppServerArgs, codexExecLaunchArgs, - codexLaunchArgv, resolveCodexLaunchArgs, -} from "./CodexProvider.ts"; +} from "./codexLaunchArgs.ts"; describe("resolveCodexLaunchArgs", () => { it("uses T3CODE_CODEX_LAUNCH_ARGS before configured settings", () => { @@ -29,24 +28,6 @@ describe("resolveCodexLaunchArgs", () => { }); }); -describe("codexLaunchArgv", () => { - it("preserves quoted values and escaped spaces", () => { - NodeAssert.deepStrictEqual( - codexLaunchArgv( - String.raw`--config model="gpt 5" --enable foo\ bar --config=profile='work profile'`, - ), - ["--config", "model=gpt 5", "--enable", "foo bar", "--config=profile=work profile"], - ); - }); - - it("preserves literal backslashes in path values", () => { - NodeAssert.deepStrictEqual( - codexLaunchArgv(String.raw`--config cacheDir=C:\Users\me --config "quoted=C:\Users\me"`), - ["--config", String.raw`cacheDir=C:\Users\me`, "--config", String.raw`quoted=C:\Users\me`], - ); - }); -}); - describe("codexAppServerArgs", () => { it("returns the app-server command for empty launch args", () => { NodeAssert.deepStrictEqual(codexAppServerArgs(""), ["app-server"]); diff --git a/apps/server/src/provider/Layers/codexLaunchArgs.ts b/apps/server/src/provider/Layers/codexLaunchArgs.ts new file mode 100644 index 00000000000..771a4f0b6ed --- /dev/null +++ b/apps/server/src/provider/Layers/codexLaunchArgs.ts @@ -0,0 +1,48 @@ +import { tokenizeCliArgs } from "@t3tools/shared/cliArgs"; + +export const T3CODE_CODEX_LAUNCH_ARGS_ENV = "T3CODE_CODEX_LAUNCH_ARGS"; + +export const resolveCodexLaunchArgs = ( + launchArgs?: string, + environment: NodeJS.ProcessEnv = process.env, +) => environment[T3CODE_CODEX_LAUNCH_ARGS_ENV]?.trim() || launchArgs?.trim() || ""; + +export const codexLaunchArgv = (launchArgs?: string): ReadonlyArray => + tokenizeCliArgs(launchArgs); + +export const codexAppServerArgs = (launchArgs?: string) => [ + "app-server", + ...codexLaunchArgv(launchArgs), +]; + +export const codexExecLaunchArgs = (launchArgs?: string) => { + const args = codexLaunchArgv(launchArgs); + const execArgs: Array = []; + + 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") { + const value = args[index + 1]; + if (value !== undefined && !value.startsWith("-")) { + execArgs.push(arg, value); + index++; + } + } else if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) { + execArgs.push(arg); + } + } + + return execArgs; +}; + +export const codexSessionAppServerArgs = ( + appServerArgs: ReadonlyArray | undefined, + launchArgs: string | undefined, +) => { + const launchAppServerArgs = codexAppServerArgs(launchArgs); + return appServerArgs ? [...launchAppServerArgs, ...appServerArgs] : launchAppServerArgs; +}; diff --git a/apps/server/src/textGeneration/CodexTextGeneration.ts b/apps/server/src/textGeneration/CodexTextGeneration.ts index 1a3e8fdca1a..6a5c0df43c7 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.ts @@ -14,7 +14,7 @@ import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { resolveAttachmentPath } from "../attachmentStore.ts"; import * as ServerConfig from "../config.ts"; import { expandHomePath } from "../pathExpansion.ts"; -import { codexExecLaunchArgs, resolveCodexLaunchArgs } from "../provider/Layers/CodexProvider.ts"; +import { codexExecLaunchArgs, resolveCodexLaunchArgs } from "../provider/Layers/codexLaunchArgs.ts"; import { TextGenerationError } from "@t3tools/contracts"; import * as TextGeneration from "./TextGeneration.ts"; import { diff --git a/packages/shared/src/cliArgs.test.ts b/packages/shared/src/cliArgs.test.ts index adf2a3f03c0..1fc6ab0215e 100644 --- a/packages/shared/src/cliArgs.test.ts +++ b/packages/shared/src/cliArgs.test.ts @@ -1,6 +1,27 @@ import { describe, expect, it } from "vite-plus/test"; -import { parseCliArgs } from "./cliArgs.ts"; +import { parseCliArgs, tokenizeCliArgs } from "./cliArgs.ts"; + +describe("tokenizeCliArgs", () => { + it("preserves quoted values and escaped spaces", () => { + expect( + tokenizeCliArgs( + String.raw`--config model="gpt 5" --enable foo\ bar --config=profile='work profile'`, + ), + ).toEqual(["--config", "model=gpt 5", "--enable", "foo bar", "--config=profile=work profile"]); + }); + + it("preserves literal backslashes in path values", () => { + expect( + tokenizeCliArgs(String.raw`--config cacheDir=C:\Users\me --config "quoted=C:\Users\me"`), + ).toEqual([ + "--config", + String.raw`cacheDir=C:\Users\me`, + "--config", + String.raw`quoted=C:\Users\me`, + ]); + }); +}); describe("parseCliArgs", () => { it("returns empty result for empty string", () => { @@ -57,6 +78,13 @@ describe("parseCliArgs", () => { }); }); + it("parses quoted --append-system-prompt with value and --chrome", () => { + expect(parseCliArgs(`--append-system-prompt "always think step by step" --chrome`)).toEqual({ + flags: { "append-system-prompt": "always think step by step", chrome: null }, + positionals: [], + }); + }); + it("parses --max-budget-usd with numeric value", () => { expect(parseCliArgs("--chrome --max-budget-usd 5.00")).toEqual({ flags: { chrome: null, "max-budget-usd": "5.00" }, diff --git a/packages/shared/src/cliArgs.ts b/packages/shared/src/cliArgs.ts index 20920093302..69851f2f3fb 100644 --- a/packages/shared/src/cliArgs.ts +++ b/packages/shared/src/cliArgs.ts @@ -7,10 +7,67 @@ export interface ParseCliArgsOptions { readonly booleanFlags?: readonly string[]; } +export function tokenizeCliArgs(args?: string): ReadonlyArray { + const input = args?.trim(); + if (!input) return []; + + const tokens: string[] = []; + let current = ""; + let quote: "'" | '"' | undefined; + let quoted = false; + + for (let index = 0; index < input.length; index++) { + const char = input[index]; + if (char === undefined) continue; + + if (quote) { + if (char === quote) { + quote = undefined; + quoted = true; + } else if (char === "\\" && quote === '"') { + const next = input[index + 1]; + if (next !== undefined && ['"', "\\", "$", "`"].includes(next)) { + current += next; + index++; + } else { + current += char; + } + } else { + current += char; + } + continue; + } + + if (char === "'" || char === '"') { + quote = char; + quoted = true; + } else if (/\s/.test(char)) { + if (current || quoted) { + tokens.push(current); + current = ""; + quoted = false; + } + } else if (char === "\\") { + const next = input[index + 1]; + if (next !== undefined && /\s/.test(next)) { + current += next; + index++; + } else { + current += char; + } + } else { + current += char; + } + } + + if (current || quoted) tokens.push(current); + return tokens; +} + /** * Parse CLI-style arguments into flags and positionals. * - * Accepts a string (split by whitespace) or a pre-split argv array. + * Accepts a string (quote-aware tokenized) or a pre-split argv array. * Supports `--key value`, `--key=value`, and `--flag` (boolean) syntax. * * parseCliArgs("") @@ -32,8 +89,7 @@ export function parseCliArgs( args: string | readonly string[], options?: ParseCliArgsOptions, ): ParsedCliArgs { - const tokens = - typeof args === "string" ? args.trim().split(/\s+/).filter(Boolean) : Array.from(args); + const tokens = typeof args === "string" ? tokenizeCliArgs(args) : Array.from(args); const booleanSet = options?.booleanFlags ? new Set(options.booleanFlags) : undefined; const flags: Record = {};