diff --git a/openspec/changes/agent-codex-sync-codex-auth-into-cue-runtimes-2026-08-10-11-56/notes.md b/openspec/changes/agent-codex-sync-codex-auth-into-cue-runtimes-2026-08-10-11-56/notes.md new file mode 100644 index 00000000..06269dd6 --- /dev/null +++ b/openspec/changes/agent-codex-sync-codex-auth-into-cue-runtimes-2026-08-10-11-56/notes.md @@ -0,0 +1,21 @@ +# agent-codex-sync-codex-auth-into-cue-runtimes-2026-08-10-11-56 (minimal / T1) + +Branch: `agent//` + +Describe the change in a sentence or two. Commit message is the spec of record. + +## Handoff + +- Handoff: change=`agent-codex-sync-codex-auth-into-cue-runtimes-2026-08-10-11-56`; branch=`agent//`; scope=`TODO`; action=`continue this sandbox or finish cleanup after a usage-limit/manual takeover`. +- Copy prompt: Continue `agent-codex-sync-codex-auth-into-cue-runtimes-2026-08-10-11-56` on branch `agent//`. Work inside the existing sandbox, review `openspec/changes/agent-codex-sync-codex-auth-into-cue-runtimes-2026-08-10-11-56/notes.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent// --base dev --via-pr --wait-for-merge --cleanup`. + +## Cleanup + +- [ ] Run: `gx branch finish --branch agent// --base dev --via-pr --wait-for-merge --cleanup` +- [ ] Record PR URL + `MERGED` state in the completion handoff. +- [ ] Confirm sandbox worktree is gone (`git worktree list`, `git branch -a`). +# Codex auth sync + +- Cause: Cue launches Codex with a profile-isolated `CODEX_HOME`, while AuthMux manages `~/.codex/auth.json`. +- Fix: copy canonical auth into the selected runtime before launch, then copy refreshed runtime auth back after exit. +- Verification: `bun test src/lib/codex-auth-sync.test.ts`; `bunx tsc --noEmit`. diff --git a/src/commands/launch.ts b/src/commands/launch.ts index 58fa422f..9ba0fce6 100644 --- a/src/commands/launch.ts +++ b/src/commands/launch.ts @@ -14,7 +14,7 @@ */ import { spawn } from "node:child_process"; -import { readFile } from "node:fs/promises"; +import { copyFile, readFile } from "node:fs/promises"; import { existsSync, readFileSync, readdirSync } from "node:fs"; import { basename, dirname, join, resolve, sep } from "node:path"; import { homedir } from "node:os"; @@ -205,6 +205,16 @@ function execAgent(bin: string, args: string[], env: NodeJS.ProcessEnv): Promise }); } +/** Keep Cue's isolated CODEX_HOME in sync with Codex/AuthMux's canonical auth. */ +export async function syncCodexAuth(source: string, destination: string): Promise { + try { + await copyFile(source, destination); + return true; + } catch { + return false; + } +} + /** * Whether the interactive MCP toggle should open this launch. Only when stdin * is a TTY, AND either the user forced it (`--cue-pick-mcps`) or there's no @@ -2712,6 +2722,11 @@ export async function run(args: string[]): Promise { // re-login. const stopReconciler = agentKind === "claude-code" ? startCredentialReconciler(runtimeKey) : undefined; + const canonicalCodexAuth = join(homedir(), ".codex", "auth.json"); + const runtimeCodexAuth = join(runtime.runtimeDir, "auth.json"); + if (agentKind === "codex") { + await syncCodexAuth(canonicalCodexAuth, runtimeCodexAuth); + } let exitCode: number; try { exitCode = await execAgent(realBin, [...briefArgs, ...parsed.passthrough], childEnv); @@ -2721,6 +2736,9 @@ export async function run(args: string[]): Promise { // Persist any /login done inside the session to its account dir now — // don't leave the only live rotated token stranded in the per-account runtime. if (agentKind === "claude-code") await rescueRuntimeCredsToOwner(runtimeKey); + if (agentKind === "codex") { + await syncCodexAuth(runtimeCodexAuth, canonicalCodexAuth); + } // Post-session runtime GC: the child has exited, so this costs zero launch // latency. Throttled (~once/day) and never touches the runtime we just used. try { await maybeAutoGc(runtimeKey); } catch { /* GC is best-effort */ } diff --git a/src/lib/codex-auth-sync.test.ts b/src/lib/codex-auth-sync.test.ts new file mode 100644 index 00000000..ada9258b --- /dev/null +++ b/src/lib/codex-auth-sync.test.ts @@ -0,0 +1,31 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { syncCodexAuth } from "../commands/launch"; + +describe("syncCodexAuth", () => { + const dirs: string[] = []; + + afterEach(async () => { + await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); + }); + + test("copies canonical Codex auth into a Cue runtime", async () => { + const dir = await mkdtemp(join(tmpdir(), "cue-codex-auth-")); + dirs.push(dir); + const source = join(dir, "source.json"); + const destination = join(dir, "runtime.json"); + await writeFile(source, '{"tokens":{"access_token":"test"}}\n'); + + expect(await syncCodexAuth(source, destination)).toBe(true); + expect(await readFile(destination, "utf8")).toBe(await readFile(source, "utf8")); + }); + + test("fails open when no canonical login exists", async () => { + const dir = await mkdtemp(join(tmpdir(), "cue-codex-auth-")); + dirs.push(dir); + expect(await syncCodexAuth(join(dir, "missing.json"), join(dir, "runtime.json"))).toBe(false); + }); +});