diff --git a/src/cli.codex_smoke.test.ts b/src/cli.codex_smoke.test.ts index cf5aa7c0..096de176 100644 --- a/src/cli.codex_smoke.test.ts +++ b/src/cli.codex_smoke.test.ts @@ -222,6 +222,7 @@ async function runDeck(input: { cwd?: string; command?: "run" | "repl"; extraArgs?: Array; + env?: Record; }): Promise<{ code: number; stdout: string; @@ -244,6 +245,7 @@ async function runDeck(input: { GAMBIT_CODEX_DISABLE_MCP: "1", CODEX_ARGS_LOG: input.argsLogPath, CODEX_REQUESTS_LOG: input.requestLogPath, + ...(input.env ?? {}), }, stdout: "piped", stderr: "piped", @@ -503,6 +505,48 @@ Deno.test({ ), true, ); + + await Deno.remove(mock.requestLogPath).catch((err) => { + if (err instanceof Deno.errors.NotFound) return; + throw err; + }); + const nestedDeckDir = path.join( + dir, + "coworkers", + "agents", + "chief-of-staff", + ); + await Deno.mkdir(nestedDeckDir, { recursive: true }); + const nestedDeck = await writeDeck( + nestedDeckDir, + "codex-cli/default", + undefined, + "Nested Chief deck.", + ); + const nestedRun = await runDeck({ + deckPath: nestedDeck, + codexBinPath: mock.binPath, + argsLogPath: mock.argsLogPath, + requestLogPath: mock.requestLogPath, + cwd: dir, + env: { GAMBIT_BOT_ROOT: dir }, + }); + assertEquals( + nestedRun.code, + 0, + formatCommandDiagnostics( + "run nested codex-cli/default under GAMBIT_BOT_ROOT", + nestedRun, + ), + ); + assertEquals( + nestedRun.requestLog.includes(`"cwd":"${dir}"`), + true, + ); + assertEquals( + nestedRun.requestLog.includes(`"writableRoots":["${dir}"]`), + true, + ); } finally { await Deno.remove(dir, { recursive: true }).catch((err) => { if (err instanceof Deno.errors.NotFound) return; diff --git a/src/providers/codex.ts b/src/providers/codex.ts index e5d4c0bb..394c3690 100644 --- a/src/providers/codex.ts +++ b/src/providers/codex.ts @@ -170,9 +170,30 @@ function codexDeckDir(deckPath?: string): string | undefined { ); } +function codexBotRootCwd(deckPath?: string): string | undefined { + const botRootRaw = Deno.env.get(BOT_ROOT_ENV)?.trim(); + if (!botRootRaw) return undefined; + const botRoot = path.resolve(botRootRaw); + const trimmedDeckPath = deckPath?.trim(); + if (!trimmedDeckPath) return botRoot; + const deckAbsolutePath = path.isAbsolute(trimmedDeckPath) + ? path.resolve(trimmedDeckPath) + : path.resolve(botRoot, trimmedDeckPath); + const relative = path.relative(botRoot, deckAbsolutePath); + if ( + relative === "" || + (!relative.startsWith("..") && !path.isAbsolute(relative)) + ) { + return botRoot; + } + return undefined; +} + function codexRunCwd(input: { cwd?: string; deckPath?: string }): string { const explicitCwd = input.cwd?.trim(); if (explicitCwd) return explicitCwd; + const botRoot = codexBotRootCwd(input.deckPath); + if (botRoot) return botRoot; const deckDir = codexDeckDir(input.deckPath); if (deckDir) return deckDir; return runCwd();