Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# agent-codex-sync-codex-auth-into-cue-runtimes-2026-08-10-11-56 (minimal / T1)

Branch: `agent/<your-name>/<branch-slug>`

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/<your-name>/<branch-slug>`; 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/<your-name>/<branch-slug>`. 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/<your-name>/<branch-slug> --base dev --via-pr --wait-for-merge --cleanup`.

## Cleanup

- [ ] Run: `gx branch finish --branch agent/<your-name>/<branch-slug> --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`.
20 changes: 19 additions & 1 deletion src/commands/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<boolean> {
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
Expand Down Expand Up @@ -2712,6 +2722,11 @@ export async function run(args: string[]): Promise<number> {
// 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);
Expand All @@ -2721,6 +2736,9 @@ export async function run(args: string[]): Promise<number> {
// 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 */ }
Expand Down
31 changes: 31 additions & 0 deletions src/lib/codex-auth-sync.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading