Skip to content

Commit efcc04c

Browse files
committed
fix: isolate codex login auth import
1 parent 842e82d commit efcc04c

3 files changed

Lines changed: 156 additions & 2 deletions

File tree

‎src/cli.zig‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,23 @@ pub fn runCodexLogin(opts: LoginOptions) !void {
11761176
try ensureCodexLoginSucceeded(term);
11771177
}
11781178

1179+
pub fn runCodexLoginWithCodexHome(allocator: std.mem.Allocator, opts: LoginOptions, codex_home: []const u8) !void {
1180+
var env_map = try std.process.getEnvMap(allocator);
1181+
defer env_map.deinit();
1182+
try env_map.put("CODEX_HOME", codex_home);
1183+
1184+
var child = std.process.Child.init(codexLoginArgs(opts), allocator);
1185+
child.env_map = &env_map;
1186+
child.stdin_behavior = .Inherit;
1187+
child.stdout_behavior = .Inherit;
1188+
child.stderr_behavior = .Inherit;
1189+
const term = child.spawnAndWait() catch |err| {
1190+
writeCodexLoginLaunchFailureHint(@errorName(err), stderrColorEnabled()) catch {};
1191+
return err;
1192+
};
1193+
try ensureCodexLoginSucceeded(term);
1194+
}
1195+
11791196
pub fn selectAccount(allocator: std.mem.Allocator, reg: *registry.Registry) !?[]const u8 {
11801197
return selectAccountWithUsageOverrides(allocator, reg, null);
11811198
}

‎src/main.zig‎

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const std = @import("std");
2+
const builtin = @import("builtin");
23
const account_api = @import("account_api.zig");
34
const account_name_refresh = @import("account_name_refresh.zig");
45
const cli = @import("cli.zig");
@@ -1530,8 +1531,16 @@ fn handleList(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.Li
15301531
}
15311532

15321533
fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.LoginOptions) !void {
1533-
try cli.runCodexLogin(opts);
1534-
const auth_path = try registry.activeAuthPath(allocator, codex_home);
1534+
const login_home = try createTempLoginCodexHome(allocator);
1535+
defer {
1536+
std.fs.cwd().deleteTree(login_home) catch |err| {
1537+
std.log.warn("failed to remove temporary Codex login home `{s}`: {s}", .{ login_home, @errorName(err) });
1538+
};
1539+
allocator.free(login_home);
1540+
}
1541+
1542+
try cli.runCodexLoginWithCodexHome(allocator, opts, login_home);
1543+
const auth_path = try registry.activeAuthPath(allocator, login_home);
15351544
defer allocator.free(auth_path);
15361545

15371546
const info = try auth.parseAuthInfo(allocator, auth_path);
@@ -1540,6 +1549,8 @@ fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.L
15401549
var reg = try registry.loadRegistry(allocator, codex_home);
15411550
defer reg.deinit(allocator);
15421551

1552+
_ = try registry.syncActiveAccountFromAuth(allocator, codex_home, &reg);
1553+
15431554
const email = info.email orelse return error.MissingEmail;
15441555
_ = email;
15451556
const record_key = info.record_key orelse return error.MissingChatgptUserId;
@@ -1548,6 +1559,9 @@ fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.L
15481559

15491560
try registry.ensureAccountsDir(allocator, codex_home);
15501561
try registry.copyFile(auth_path, dest);
1562+
const active_auth_path = try registry.activeAuthPath(allocator, codex_home);
1563+
defer allocator.free(active_auth_path);
1564+
try registry.copyFile(auth_path, active_auth_path);
15511565

15521566
const record = try registry.accountFromAuth(allocator, "", &info);
15531567
try registry.upsertAccount(allocator, &reg, record);
@@ -1556,6 +1570,56 @@ fn handleLogin(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.L
15561570
try registry.saveRegistry(allocator, codex_home, &reg);
15571571
}
15581572

1573+
fn createTempLoginCodexHome(allocator: std.mem.Allocator) ![]u8 {
1574+
const base = try tempBasePathAlloc(allocator);
1575+
defer allocator.free(base);
1576+
var counter: usize = 0;
1577+
while (counter < 100) : (counter += 1) {
1578+
const path = try std.fmt.allocPrint(
1579+
allocator,
1580+
"{s}{c}codex-auth-login-{d}-{d}",
1581+
.{ base, std.fs.path.sep, std.time.nanoTimestamp(), counter },
1582+
);
1583+
std.fs.cwd().makePath(path) catch |err| switch (err) {
1584+
error.PathAlreadyExists => {
1585+
allocator.free(path);
1586+
continue;
1587+
},
1588+
else => {
1589+
allocator.free(path);
1590+
return err;
1591+
},
1592+
};
1593+
return path;
1594+
}
1595+
return error.PathAlreadyExists;
1596+
}
1597+
1598+
fn tempBasePathAlloc(allocator: std.mem.Allocator) ![]u8 {
1599+
if (builtin.os.tag == .windows) {
1600+
if (try getNonEmptyEnvVarOwned(allocator, "TEMP")) |path| return path;
1601+
if (try getNonEmptyEnvVarOwned(allocator, "TMP")) |path| return path;
1602+
if (try getNonEmptyEnvVarOwned(allocator, "TMPDIR")) |path| return path;
1603+
return allocator.dupe(u8, "C:\\Temp");
1604+
}
1605+
if (try getNonEmptyEnvVarOwned(allocator, "TMPDIR")) |path| return path;
1606+
if (try getNonEmptyEnvVarOwned(allocator, "TMP")) |path| return path;
1607+
if (try getNonEmptyEnvVarOwned(allocator, "TEMP")) |path| return path;
1608+
return allocator.dupe(u8, "/tmp");
1609+
}
1610+
1611+
fn getNonEmptyEnvVarOwned(allocator: std.mem.Allocator, name: []const u8) !?[]u8 {
1612+
const value = std.process.getEnvVarOwned(allocator, name) catch |err| switch (err) {
1613+
error.EnvironmentVariableNotFound => return null,
1614+
else => return err,
1615+
};
1616+
if (value.len == 0) {
1617+
allocator.free(value);
1618+
return null;
1619+
}
1620+
return value;
1621+
}
1622+
15591623
fn handleImport(allocator: std.mem.Allocator, codex_home: []const u8, opts: cli.ImportOptions) !void {
15601624
if (opts.purge) {
15611625
var report = try registry.purgeRegistryFromImportSource(allocator, codex_home, opts.auth_path, opts.alias);

‎src/tests/e2e_cli_test.zig‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,79 @@ test "Scenario: Given CODEX_HOME override when running login then it stores auth
540540
try std.testing.expect(std.mem.eql(u8, loaded.accounts.items[0].email, expected_email));
541541
}
542542

543+
test "Scenario: Given existing active auth when login succeeds then upstream login uses a temporary Codex home" {
544+
const gpa = std.testing.allocator;
545+
const project_root = try projectRootAlloc(gpa);
546+
defer gpa.free(project_root);
547+
try buildCliBinary(gpa, project_root);
548+
549+
var tmp = std.testing.tmpDir(.{});
550+
defer tmp.cleanup();
551+
552+
const home_root = try tmp.dir.realpathAlloc(gpa, ".");
553+
defer gpa.free(home_root);
554+
try tmp.dir.makePath(".codex");
555+
try tmp.dir.makePath("fake-bin");
556+
557+
const existing_auth = try bdd.authJsonWithEmailPlan(gpa, "existing@example.com", "plus");
558+
defer gpa.free(existing_auth);
559+
try tmp.dir.writeFile(.{ .sub_path = ".codex/auth.json", .data = existing_auth });
560+
561+
const expected_email = "new-login@example.com";
562+
const fake_auth = try bdd.authJsonWithEmailPlan(gpa, expected_email, "pro");
563+
defer gpa.free(fake_auth);
564+
try tmp.dir.writeFile(.{ .sub_path = "fake-auth.json", .data = fake_auth });
565+
try writeSuccessfulFakeCodex(tmp.dir);
566+
567+
const fake_bin_path = try std.fs.path.join(gpa, &[_][]const u8{ home_root, "fake-bin" });
568+
defer gpa.free(fake_bin_path);
569+
const path_override = try prependPathEntryAlloc(gpa, fake_bin_path);
570+
defer gpa.free(path_override);
571+
572+
const result = try runCliWithIsolatedHomeAndPath(
573+
gpa,
574+
project_root,
575+
home_root,
576+
path_override,
577+
&[_][]const u8{ "login", "--device-auth" },
578+
);
579+
defer gpa.free(result.stdout);
580+
defer gpa.free(result.stderr);
581+
582+
try expectSuccess(result);
583+
584+
const active_auth_path = try authJsonPathAlloc(gpa, home_root);
585+
defer gpa.free(active_auth_path);
586+
const active_auth = try bdd.readFileAlloc(gpa, active_auth_path);
587+
defer gpa.free(active_auth);
588+
try std.testing.expectEqualStrings(fake_auth, active_auth);
589+
590+
const codex_home = try codexHomeAlloc(gpa, home_root);
591+
defer gpa.free(codex_home);
592+
var loaded = try registry.loadRegistry(gpa, codex_home);
593+
defer loaded.deinit(gpa);
594+
try std.testing.expectEqual(@as(usize, 2), loaded.accounts.items.len);
595+
try std.testing.expect(loaded.active_account_key != null);
596+
597+
const expected_account_key = try bdd.accountKeyForEmailAlloc(gpa, expected_email);
598+
defer gpa.free(expected_account_key);
599+
try std.testing.expectEqualStrings(expected_account_key, loaded.active_account_key.?);
600+
601+
const snapshot_path = try registry.accountAuthPath(gpa, codex_home, expected_account_key);
602+
defer gpa.free(snapshot_path);
603+
const snapshot_data = try bdd.readFileAlloc(gpa, snapshot_path);
604+
defer gpa.free(snapshot_data);
605+
try std.testing.expectEqualStrings(fake_auth, snapshot_data);
606+
607+
const existing_account_key = try bdd.accountKeyForEmailAlloc(gpa, "existing@example.com");
608+
defer gpa.free(existing_account_key);
609+
const existing_snapshot_path = try registry.accountAuthPath(gpa, codex_home, existing_account_key);
610+
defer gpa.free(existing_snapshot_path);
611+
const existing_snapshot_data = try bdd.readFileAlloc(gpa, existing_snapshot_path);
612+
defer gpa.free(existing_snapshot_data);
613+
try std.testing.expectEqualStrings(existing_auth, existing_snapshot_data);
614+
}
615+
543616
test "Scenario: Given failed device auth login with existing auth json when running login then it forwards the flag and does not mutate the registry" {
544617
const gpa = std.testing.allocator;
545618
const project_root = try projectRootAlloc(gpa);

0 commit comments

Comments
 (0)