Skip to content

Commit 1cce42c

Browse files
Cmochanceclaude
andauthored
fix(login): resolve real codex CLI from live home, not sandbox (#18)
* fix(login): resolve real codex CLI from live home, not sandbox User report (Windows): clicking the per-card Login button surfaced \`C:\\Python314\\python.exe: can't open file 'C:\\Users\\32162\\.codex\\ account_backup\\windows\\login_runtime\\account_backup\\windows\\ codex_switch.py': [Errno 2] No such file or directory\` Root cause: \`build_login_command\` was anchoring \`resolve_real_codex_cli\` on the *runtime* CODEX_HOME (the sandbox). On Windows the user's \`codex\` on PATH is a managed cmd shim that does \`%CODEX_HOME%\\account_backup\\windows\\codex_switch_cli\`. The managed-shim filter only excluded the shim that lives under the *runtime* CODEX_HOME — it didn't filter the live shim, so the resolver returned the live shim. Once invoked with \`CODEX_HOME=runtime_home\`, the shim looked for \`codex_switch_cli\` (or the legacy \`codex_switch.py\`) under the empty sandbox and failed. `run_codex_auth_refresh` already had this split right (\`cli_codex_home\` for resolution, \`runtime_codex_home\` for the env). Mirror that on \`run_codex_login\`: - `PlatformHooks::run_codex_login` now takes \`(cli_codex_home, runtime_codex_home)\`. The legacy \`login_current_profile\` flow passes the same path for both. The per-card login passes the live \`~/.codex\` for resolution and the sandbox for the env. - Both mac and win impls split the args and use \`cli_codex_home\` only for \`resolve_real_codex_cli\`. - \`shared/runtime/login_runtime.rs\` now passes the live \`codex_home\` to the hook for resolution. - Tests updated; 40/40 still pass on mac (the previously-flaky \`discover_real_codex_cli_path_falls_back_to_app_bundle_cli\` continues to be parallel-only flaky due to a pre-existing PATH env race between mac tests and win tests, unrelated to this change). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(login): also update win/switch.rs FakeHooks signature Linux CI compiles the win runtime path (it's the `not(target_os = "macos")` branch), so the win switch tests' FakeHooks needed the same trait signature update as the rest of the impls. Caught by CI on PR #18 only — the local cargo check passed because mac uses the macos runtime path. 40/40 still pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 9b5e8c6 commit 1cce42c

9 files changed

Lines changed: 79 additions & 27 deletions

File tree

src-tauri/mac/runtime/process.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,14 @@ fn build_auth_refresh_command(real_codex_path: &Path, runtime_codex_home: &Path)
266266
command
267267
}
268268

269-
fn build_login_command(codex_home: &Path) -> Command {
270-
let mut command = if let Some(real_codex_path) = resolve_real_codex_cli(Some(codex_home)) {
269+
/// Build the `codex login` command. Resolution of the real codex
270+
/// binary is anchored on `cli_codex_home` (the live `~/.codex`) so the
271+
/// managed-shim filter works correctly even when `runtime_codex_home`
272+
/// is a sandboxed sibling that doesn't have its own install state.
273+
/// `runtime_codex_home` is what the spawned process sees as
274+
/// `CODEX_HOME` and is where it will write `auth.json`.
275+
fn build_login_command(cli_codex_home: &Path, runtime_codex_home: &Path) -> Command {
276+
let mut command = if let Some(real_codex_path) = resolve_real_codex_cli(Some(cli_codex_home)) {
271277
let mut command = Command::new(real_codex_path);
272278
command.arg("login");
273279
command
@@ -277,8 +283,8 @@ fn build_login_command(codex_home: &Path) -> Command {
277283
command
278284
};
279285

280-
command.current_dir(codex_home);
281-
command.env("CODEX_HOME", codex_home);
286+
command.current_dir(runtime_codex_home);
287+
command.env("CODEX_HOME", runtime_codex_home);
282288
command
283289
}
284290

@@ -339,8 +345,10 @@ pub fn run_codex_auth_refresh(cli_codex_home: &Path, runtime_codex_home: &Path)
339345
Err(AppError::new("AUTH_REFRESH_FAILED", message))
340346
}
341347

342-
pub fn run_codex_login(codex_home: &Path) -> AppResult<()> {
343-
let output = build_login_command(codex_home).output().map_err(|error| {
348+
pub fn run_codex_login(cli_codex_home: &Path, runtime_codex_home: &Path) -> AppResult<()> {
349+
let output = build_login_command(cli_codex_home, runtime_codex_home)
350+
.output()
351+
.map_err(|error| {
344352
AppError::new(
345353
"LOGIN_COMMAND_FAILED",
346354
format!("Failed to start `codex login`: {error}"),
@@ -427,8 +435,12 @@ impl PlatformHooks for MacosPlatformHooks {
427435
reopen_codex_app_if_needed(app_was_running, codex_home)
428436
}
429437

430-
fn run_codex_login(&self, codex_home: &Path) -> AppResult<()> {
431-
run_codex_login(codex_home)
438+
fn run_codex_login(
439+
&self,
440+
cli_codex_home: &Path,
441+
runtime_codex_home: &Path,
442+
) -> AppResult<()> {
443+
run_codex_login(cli_codex_home, runtime_codex_home)
432444
}
433445

434446
fn run_codex_auth_refresh(

src-tauri/mac/runtime/profile_actions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ pub fn login_current_profile() -> AppResult<String> {
6969
));
7070
}
7171

72-
platform::run_codex_login(&codex_home)?;
72+
platform::run_codex_login(&codex_home, &codex_home)?;
7373

7474
if !codex_home.join("auth.json").is_file() {
7575
return Err(AppError::new(

src-tauri/shared/platform/hooks.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,17 @@ pub trait PlatformHooks: Send + Sync {
1010
app_was_running: bool,
1111
codex_home: Option<&Path>,
1212
) -> Vec<String>;
13-
fn run_codex_login(&self, codex_home: &Path) -> AppResult<()>;
13+
/// Run `codex login`. `cli_codex_home` is the live `~/.codex` and
14+
/// drives codex-binary resolution (so the user's managed shim is
15+
/// filtered out correctly). `runtime_codex_home` is what the spawned
16+
/// codex sees as `CODEX_HOME` — for the legacy "log in as the
17+
/// active profile" flow they are the same, but the per-card login
18+
/// flow points it at a sandboxed sibling.
19+
fn run_codex_login(
20+
&self,
21+
cli_codex_home: &Path,
22+
runtime_codex_home: &Path,
23+
) -> AppResult<()>;
1424
fn run_codex_auth_refresh(
1525
&self,
1626
cli_codex_home: &Path,

src-tauri/shared/platform/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ pub fn open_or_activate_codex_app(codex_home: Option<&Path>) -> AppResult<String
4444
current_hooks().open_or_activate_codex_app(codex_home)
4545
}
4646

47-
pub fn run_codex_login(codex_home: &Path) -> AppResult<()> {
48-
current_hooks().run_codex_login(codex_home)
47+
pub fn run_codex_login(cli_codex_home: &Path, runtime_codex_home: &Path) -> AppResult<()> {
48+
current_hooks().run_codex_login(cli_codex_home, runtime_codex_home)
4949
}
5050

5151
pub fn run_codex_auth_refresh(cli_codex_home: &Path, runtime_codex_home: &Path) -> AppResult<()> {

src-tauri/shared/runtime/login_runtime.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,13 @@ pub fn login_profile_with_home<H: PlatformHooks + ?Sized>(
205205
)
206206
})?;
207207

208-
hooks.run_codex_login(runtime_home)?;
208+
// Resolve the real codex binary against the live `~/.codex` (so the
209+
// user's managed-shim filter and install-state cache work), but
210+
// point the spawned process's CODEX_HOME at our sandboxed runtime
211+
// dir. Without this split, a Windows codex shim that does
212+
// `%CODEX_HOME%\\account_backup\\windows\\codex_switch_cli` would
213+
// fail to find the script under the empty runtime home.
214+
hooks.run_codex_login(&codex_home, runtime_home)?;
209215

210216
let runtime_auth = runtime_home.join(RUNTIME_AUTH_FILENAME);
211217
if !runtime_auth.is_file() {
@@ -300,9 +306,13 @@ mod tests {
300306
) -> Vec<String> {
301307
unreachable!("not used in login_runtime tests")
302308
}
303-
fn run_codex_login(&self, codex_home: &Path) -> AppResult<()> {
309+
fn run_codex_login(
310+
&self,
311+
_cli_codex_home: &Path,
312+
runtime_codex_home: &Path,
313+
) -> AppResult<()> {
304314
*self.call_count.lock().unwrap() += 1;
305-
fs::write(codex_home.join("auth.json"), &self.auth_payload).unwrap();
315+
fs::write(runtime_codex_home.join("auth.json"), &self.auth_payload).unwrap();
306316
Ok(())
307317
}
308318
fn run_codex_auth_refresh(
@@ -440,7 +450,7 @@ mod tests {
440450
fn reopen_codex_app_if_needed(&self, _: bool, _: Option<&Path>) -> Vec<String> {
441451
unreachable!()
442452
}
443-
fn run_codex_login(&self, _: &Path) -> AppResult<()> {
453+
fn run_codex_login(&self, _: &Path, _: &Path) -> AppResult<()> {
444454
Ok(())
445455
}
446456
fn run_codex_auth_refresh(&self, _: &Path, _: &Path) -> AppResult<()> {

src-tauri/shared/runtime/switch_core.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,11 @@ mod tests {
149149
Vec::new()
150150
}
151151

152-
fn run_codex_login(&self, _codex_home: &Path) -> AppResult<()> {
152+
fn run_codex_login(
153+
&self,
154+
_cli_codex_home: &Path,
155+
_runtime_codex_home: &Path,
156+
) -> AppResult<()> {
153157
unreachable!("not used in switch_core tests")
154158
}
155159

src-tauri/win/runtime/process.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -351,8 +351,14 @@ fn build_auth_refresh_command(real_codex_path: &Path, runtime_codex_home: &Path)
351351
command
352352
}
353353

354-
fn build_login_command(codex_home: &Path) -> Command {
355-
let mut command = if let Some(real_codex_path) = resolve_real_codex_cli(Some(codex_home)) {
354+
/// Build the `codex login` command. Resolution of the real codex
355+
/// binary is anchored on `cli_codex_home` (the live `~/.codex`) so the
356+
/// managed-shim filter works correctly even when `runtime_codex_home`
357+
/// is a sandboxed sibling that doesn't have its own install state.
358+
/// `runtime_codex_home` is what the spawned process sees as
359+
/// `CODEX_HOME` and is where it will write `auth.json`.
360+
fn build_login_command(cli_codex_home: &Path, runtime_codex_home: &Path) -> Command {
361+
let mut command = if let Some(real_codex_path) = resolve_real_codex_cli(Some(cli_codex_home)) {
356362
let mut command = Command::new(real_codex_path);
357363
command.arg("login");
358364
command
@@ -367,8 +373,8 @@ fn build_login_command(codex_home: &Path) -> Command {
367373
};
368374

369375
hide_console_window(&mut command);
370-
command.current_dir(codex_home);
371-
command.env("CODEX_HOME", codex_home);
376+
command.current_dir(runtime_codex_home);
377+
command.env("CODEX_HOME", runtime_codex_home);
372378
command
373379
}
374380

@@ -439,8 +445,10 @@ pub fn run_codex_auth_refresh(cli_codex_home: &Path, runtime_codex_home: &Path)
439445
Err(AppError::new("AUTH_REFRESH_FAILED", message))
440446
}
441447

442-
pub fn run_codex_login(codex_home: &Path) -> AppResult<()> {
443-
let output = build_login_command(codex_home).output().map_err(|error| {
448+
pub fn run_codex_login(cli_codex_home: &Path, runtime_codex_home: &Path) -> AppResult<()> {
449+
let output = build_login_command(cli_codex_home, runtime_codex_home)
450+
.output()
451+
.map_err(|error| {
444452
AppError::new(
445453
"LOGIN_COMMAND_FAILED",
446454
format!("Failed to start `codex login`: {error}"),
@@ -537,8 +545,12 @@ impl PlatformHooks for WindowsPlatformHooks {
537545
reopen_codex_app_if_needed(app_was_running, codex_home)
538546
}
539547

540-
fn run_codex_login(&self, codex_home: &Path) -> AppResult<()> {
541-
run_codex_login(codex_home)
548+
fn run_codex_login(
549+
&self,
550+
cli_codex_home: &Path,
551+
runtime_codex_home: &Path,
552+
) -> AppResult<()> {
553+
run_codex_login(cli_codex_home, runtime_codex_home)
542554
}
543555

544556
fn run_codex_auth_refresh(

src-tauri/win/runtime/profile_actions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ pub fn login_current_profile() -> AppResult<String> {
6767
));
6868
}
6969

70-
platform::run_codex_login(&codex_home)?;
70+
platform::run_codex_login(&codex_home, &codex_home)?;
7171

7272
if !codex_home.join("auth.json").is_file() {
7373
return Err(AppError::new(

src-tauri/win/runtime/switch.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,11 @@ mod tests {
153153
Vec::new()
154154
}
155155

156-
fn run_codex_login(&self, _codex_home: &Path) -> AppResult<()> {
156+
fn run_codex_login(
157+
&self,
158+
_cli_codex_home: &Path,
159+
_runtime_codex_home: &Path,
160+
) -> AppResult<()> {
157161
unreachable!("not used in switch tests")
158162
}
159163

0 commit comments

Comments
 (0)