From 4dce92564dd440902d1a61b21f2ec66f3378acfd Mon Sep 17 00:00:00 2001 From: "S. Ahmed" Date: Sun, 24 May 2026 21:42:31 +0600 Subject: [PATCH 1/3] fix: create scratch CODEX_HOME before login The isolated login flow correctly avoids exposing the real active auth.json to upstream codex login, but current Codex rejects a CODEX_HOME override when the target directory does not already exist. Create the temporary login home before spawning codex login, keep the existing cleanup behavior, and add a strict integration fake that fails unless the scratch CODEX_HOME exists before launch. Validation: zig build run -- list; zig build test --summary all --- src/workflows/login.zig | 3 ++ tests/cli_integration_test.zig | 84 ++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/src/workflows/login.zig b/src/workflows/login.zig index 6da0f62..b242443 100644 --- a/src/workflows/login.zig +++ b/src/workflows/login.zig @@ -26,6 +26,9 @@ pub fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: c try registry.ensureAccountsDir(allocator, codex_home); const login_codex_home = try loginScratchCodexHomeAlloc(allocator, codex_home); defer allocator.free(login_codex_home); + // Current Codex rejects a CODEX_HOME override that does not exist yet. + // Create the isolated home before spawning `codex login`. + try registry.ensurePrivateDir(login_codex_home); defer std.Io.Dir.cwd().deleteTree(app_runtime.io(), login_codex_home) catch {}; try cli.login.runCodexLogin(opts, login_codex_home); diff --git a/tests/cli_integration_test.zig b/tests/cli_integration_test.zig index 0aa832f..07783ba 100644 --- a/tests/cli_integration_test.zig +++ b/tests/cli_integration_test.zig @@ -183,6 +183,35 @@ fn writeSuccessfulFakeCodex(dir: fs.Dir) !void { } } +fn writeStrictExistingCodexHomeFakeCodex(dir: fs.Dir) !void { + const script = + if (builtin.os.tag == .windows) + "@echo off\r\n" ++ + ">\"%HOME%\\fake-codex-argv.txt\" echo %*\r\n" ++ + ">\"%HOME%\\fake-codex-home.txt\" echo %CODEX_HOME%\r\n" ++ + "set \"CODEX_HOME_DIR=%CODEX_HOME%\"\r\n" ++ + "if \"%CODEX_HOME_DIR%\"==\"\" set \"CODEX_HOME_DIR=%HOME%\\.codex\"\r\n" ++ + "if not exist \"%CODEX_HOME_DIR%\" exit /b 42\r\n" ++ + "copy /Y \"%HOME%\\fake-auth.json\" \"%CODEX_HOME_DIR%\\auth.json\" >NUL\r\n" ++ + "exit /b 0\r\n" + else + "#!/bin/sh\n" ++ + "printf '%s\\n' \"$*\" > \"$HOME/fake-codex-argv.txt\"\n" ++ + "printf '%s\\n' \"$CODEX_HOME\" > \"$HOME/fake-codex-home.txt\"\n" ++ + "CODEX_HOME_DIR=\"${CODEX_HOME:-$HOME/.codex}\"\n" ++ + "[ -d \"$CODEX_HOME_DIR\" ] || exit 42\n" ++ + "cp \"$HOME/fake-auth.json\" \"$CODEX_HOME_DIR/auth.json\"\n" ++ + "exit 0\n"; + const sub_path = fakeCodexCommandPath(); + try dir.writeFile(.{ .sub_path = sub_path, .data = script }); + + if (builtin.os.tag != .windows) { + var file = try dir.openFile(sub_path, .{ .mode = .read_write }); + defer file.close(); + try file.chmod(0o755); + } +} + fn fakeNodeCommandPath() []const u8 { return if (builtin.os.tag == .windows) "fake-node-bin/node.cmd" else "fake-node-bin/node"; } @@ -791,6 +820,61 @@ test "Scenario: Given device auth login when running login then it forwards the try std.testing.expectEqualStrings(fake_auth, active_auth); } +test "Scenario: Given strict codex login when running login then scratch CODEX_HOME exists before launch" { + const gpa = std.testing.allocator; + const project_root = try projectRootAlloc(gpa); + defer gpa.free(project_root); + try buildCliBinary(gpa, project_root); + + var tmp = fs.tmpDir(.{}); + defer tmp.cleanup(); + + const home_root = try tmp.dir.realpathAlloc(gpa, "."); + defer gpa.free(home_root); + try tmp.dir.makePath(".codex"); + try tmp.dir.makePath("fake-bin"); + + const expected_email = "strict-login@example.com"; + const fake_auth = try fixtures.authJsonWithEmailPlan(gpa, expected_email, "plus"); + defer gpa.free(fake_auth); + try tmp.dir.writeFile(.{ .sub_path = "fake-auth.json", .data = fake_auth }); + try writeStrictExistingCodexHomeFakeCodex(tmp.dir); + + const fake_bin_path = try fs.path.join(gpa, &[_][]const u8{ home_root, "fake-bin" }); + defer gpa.free(fake_bin_path); + const path_override = try prependPathEntryAlloc(gpa, fake_bin_path); + defer gpa.free(path_override); + + const result = try runCliWithIsolatedHomeAndPath( + gpa, + project_root, + home_root, + path_override, + &[_][]const u8{ "login", "--device-auth" }, + ); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + try expectSuccess(result); + + const codex_home = try codexHomeAlloc(gpa, home_root); + defer gpa.free(codex_home); + + const fake_codex_home_path = try fs.path.join(gpa, &[_][]const u8{ home_root, "fake-codex-home.txt" }); + defer gpa.free(fake_codex_home_path); + const fake_codex_home_data = try fixtures.readFileAlloc(gpa, fake_codex_home_path); + defer gpa.free(fake_codex_home_data); + const fake_codex_home = std.mem.trim(u8, fake_codex_home_data, " \r\n"); + try std.testing.expect(!std.mem.eql(u8, fake_codex_home, codex_home)); + try std.testing.expect(std.mem.indexOf(u8, fake_codex_home, "login-") != null); + try std.testing.expectError(error.FileNotFound, fs.cwd().access(fake_codex_home, .{})); + + var loaded = try registry.loadRegistry(gpa, codex_home); + defer loaded.deinit(gpa); + try std.testing.expectEqual(@as(usize, 1), loaded.accounts.items.len); + try std.testing.expect(std.mem.eql(u8, loaded.accounts.items[0].email, expected_email)); +} + test "Scenario: Given refreshed active auth before login when running login then old account snapshot is synced first" { const gpa = std.testing.allocator; const project_root = try projectRootAlloc(gpa); From 23f8ca7b9a9a7f07204d2d9302169e289c615b46 Mon Sep 17 00:00:00 2001 From: "S. Ahmed" Date: Sun, 24 May 2026 21:53:33 +0600 Subject: [PATCH 2/3] fix: clean scratch login home on hardening failure --- src/workflows/login.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/workflows/login.zig b/src/workflows/login.zig index b242443..4527d2d 100644 --- a/src/workflows/login.zig +++ b/src/workflows/login.zig @@ -26,10 +26,10 @@ pub fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: c try registry.ensureAccountsDir(allocator, codex_home); const login_codex_home = try loginScratchCodexHomeAlloc(allocator, codex_home); defer allocator.free(login_codex_home); + defer std.Io.Dir.cwd().deleteTree(app_runtime.io(), login_codex_home) catch {}; // Current Codex rejects a CODEX_HOME override that does not exist yet. - // Create the isolated home before spawning `codex login`. + // Create it after registering cleanup so hardening failures do not leak it. try registry.ensurePrivateDir(login_codex_home); - defer std.Io.Dir.cwd().deleteTree(app_runtime.io(), login_codex_home) catch {}; try cli.login.runCodexLogin(opts, login_codex_home); const login_auth_path = try registry.activeAuthPath(allocator, login_codex_home); From d8990ca3d141aa847875fdf547d22e9b57dfd762 Mon Sep 17 00:00:00 2001 From: Loongphy Date: Mon, 25 May 2026 10:57:49 +0800 Subject: [PATCH 3/3] style: remove login scratch comment --- src/workflows/login.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/workflows/login.zig b/src/workflows/login.zig index 4527d2d..9c359dd 100644 --- a/src/workflows/login.zig +++ b/src/workflows/login.zig @@ -27,8 +27,6 @@ pub fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: c const login_codex_home = try loginScratchCodexHomeAlloc(allocator, codex_home); defer allocator.free(login_codex_home); defer std.Io.Dir.cwd().deleteTree(app_runtime.io(), login_codex_home) catch {}; - // Current Codex rejects a CODEX_HOME override that does not exist yet. - // Create it after registering cleanup so hardening failures do not leak it. try registry.ensurePrivateDir(login_codex_home); try cli.login.runCodexLogin(opts, login_codex_home);