diff --git a/docs/wiki/Configuration.md b/docs/wiki/Configuration.md index 0b3af4d..1d87dda 100644 --- a/docs/wiki/Configuration.md +++ b/docs/wiki/Configuration.md @@ -124,7 +124,7 @@ Every command writes diagnostic logs to `$CODEX_SWITCH_HOME/logs/`, one file per - Linux uses a systemd user service; headless login should use `login --device`. - Windows uses Task Scheduler and requires elevated PowerShell for daemon installation. Windows Terminal or PowerShell is recommended for the TUI. -> **`CODEX_SWITCH_HOME` and installed daemon services:** `daemon install` writes a service definition that forwards only `HOME` and `CODEX_HOME` into the daemon's environment. If you relocate data with `CODEX_SWITCH_HOME`, an installed service still uses the default `~/.codex-switch` unless you add the variable to the generated LaunchAgent plist, systemd unit, or Task Scheduler command yourself. +> **`CODEX_SWITCH_HOME` and installed daemon services:** when `CODEX_SWITCH_HOME` is set in the shell that runs `daemon install`, its value is captured into the generated LaunchAgent plist, systemd unit, or Task Scheduler command so the installed service reads the same relocated store. The variable is read at install time; if you later change or unset it, re-run `daemon install` to update the service definition. ## Next steps diff --git a/docs/wiki/Troubleshooting.md b/docs/wiki/Troubleshooting.md index 423055b..fe2cf99 100644 --- a/docs/wiki/Troubleshooting.md +++ b/docs/wiki/Troubleshooting.md @@ -22,7 +22,7 @@ Start with the complete error message, its file path, and the command that produ | macOS/Linux self-update reports that the install directory is not writable | Rerun the current installer once to migrate a legacy `/usr/local/bin` direct install to `$HOME/.local/bin`; see [Updating](Updating#legacy-direct-installs). Use `sudo codex-switch self-update` only for an intentional `--system` install. | | A dev build should return to stable | Run `codex-switch self-update --stable`. | | Self-update reports that `gh attestation verify` is unavailable | Install or upgrade [GitHub CLI](https://cli.github.com/), then retry. Direct self-update fails closed until it can verify the release provenance bundle. | -| An installed daemon ignores `CODEX_SWITCH_HOME` | The generated service forwards only `HOME` and `CODEX_HOME`; add `CODEX_SWITCH_HOME` to the service definition manually. See [Configuration](Configuration#platform-integration). | +| An installed daemon ignores `CODEX_SWITCH_HOME` | `daemon install` captures `CODEX_SWITCH_HOME` from the shell that runs it; re-run `daemon install` with the variable set so its value lands in the service definition. See [Configuration](Configuration#platform-integration). | | HTTPS fails with `invalid peer certificate: UnknownIssuer` | An intercepting proxy is re-signing traffic. See [HTTPS fails with an unknown issuer](#https-fails-with-invalid-peer-certificate-unknownissuer). | | An account reports `re-login required (refresh_token_reused)` | The stored refresh token was already spent and cannot be recovered. Run `codex-switch login ` for that profile. The verdict is remembered, so the account costs no further requests until you sign in again; `codex-switch list -f` asks the server anyway. | | Import reports a quarantined rotated credential | The server replaced the source file's one-time token before identity or managed-policy validation failed. Keep the named file under `~/.codex-switch/recovery/` private, sign in again, then remove it only after the account works. Recovery files are deliberately not selectable profiles. | diff --git a/src/daemon/service.rs b/src/daemon/service.rs index 141a09d..83dcec0 100644 --- a/src/daemon/service.rs +++ b/src/daemon/service.rs @@ -60,6 +60,21 @@ fn effective_codex_home() -> Result { .ok_or_else(|| anyhow::anyhow!("Codex auth path has no parent directory")) } +/// The `CODEX_SWITCH_HOME` override active in the installing process, if any. +/// +/// The service definition otherwise forwards only `HOME` and `CODEX_HOME`, so a +/// relocated store (`CODEX_SWITCH_HOME`) would be lost and the daemon would read +/// the default `~/.codex-switch` — polling a stale profile set and potentially +/// rewriting the wrong account's live `~/.codex/auth.json`. Baking the override +/// into the unit keeps the daemon pointed at the same store the user installed +/// from. Returns `None` when unset or empty so the default path is untouched. +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] +fn configured_codex_switch_home() -> Option { + std::env::var_os("CODEX_SWITCH_HOME") + .filter(|value| !value.is_empty()) + .map(|value| value.to_string_lossy().into_owned()) +} + #[cfg(any(target_os = "macos", test))] fn xml_escape(value: &str) -> String { value @@ -136,10 +151,23 @@ fn plist_path() -> Result { } #[cfg(any(target_os = "macos", test))] -fn launchd_plist(exe: &str, home: &str, codex_home: &str) -> String { +fn launchd_plist( + exe: &str, + home: &str, + codex_home: &str, + codex_switch_home: Option<&str>, +) -> String { let exe = xml_escape(exe); let home = xml_escape(home); let codex_home = xml_escape(codex_home); + let codex_switch_home = codex_switch_home + .map(|value| { + format!( + "\n CODEX_SWITCH_HOME\n {}", + xml_escape(value) + ) + }) + .unwrap_or_default(); format!( r#" String { HOME {home} CODEX_HOME - {codex_home} + {codex_home}{codex_switch_home} "#, exe = exe, home = home, codex_home = codex_home, + codex_switch_home = codex_switch_home, label = LAUNCHD_LABEL, ) } @@ -183,7 +212,8 @@ fn install_launchd() -> Result<()> { .display() .to_string(); let codex_home = effective_codex_home()?.display().to_string(); - let plist = launchd_plist(&exe, &home, &codex_home); + let codex_switch_home = configured_codex_switch_home(); + let plist = launchd_plist(&exe, &home, &codex_home, codex_switch_home.as_deref()); let path = plist_path()?; if path.exists() { @@ -283,10 +313,23 @@ fn unit_path() -> Result { } #[cfg(any(target_os = "linux", test))] -fn systemd_unit(exe: &str, home: &str, codex_home: &str) -> String { +fn systemd_unit( + exe: &str, + home: &str, + codex_home: &str, + codex_switch_home: Option<&str>, +) -> String { let exe = systemd_quote(exe); let home = systemd_quote(&format!("HOME={home}")); let codex_home = systemd_quote(&format!("CODEX_HOME={codex_home}")); + let codex_switch_home = codex_switch_home + .map(|value| { + format!( + "\nEnvironment={}", + systemd_quote(&format!("CODEX_SWITCH_HOME={value}")) + ) + }) + .unwrap_or_default(); format!( r#"[Unit] Description=codex-switch auto-switching daemon @@ -298,7 +341,7 @@ ExecStart={exe} daemon start --foreground Restart=on-failure RestartSec=10 Environment={home} -Environment={codex_home} +Environment={codex_home}{codex_switch_home} [Install] WantedBy=default.target @@ -306,6 +349,7 @@ WantedBy=default.target exe = exe, home = home, codex_home = codex_home, + codex_switch_home = codex_switch_home, ) } @@ -317,8 +361,9 @@ fn install_systemd() -> Result<()> { .display() .to_string(); let codex_home = effective_codex_home()?.display().to_string(); + let codex_switch_home = configured_codex_switch_home(); - let unit = systemd_unit(&exe, &home, &codex_home); + let unit = systemd_unit(&exe, &home, &codex_home, codex_switch_home.as_deref()); let path = unit_path()?; if path.exists() { @@ -403,10 +448,23 @@ fn uninstall_systemd() -> Result<()> { // -- Windows Task Scheduler -- #[cfg(any(target_os = "windows", test))] -fn task_scheduler_command(exe: &Path, codex_home: &Path) -> String { +fn task_scheduler_command( + exe: &Path, + codex_home: &Path, + codex_switch_home: Option<&Path>, +) -> String { + let codex_switch_home = codex_switch_home + .map(|path| { + format!( + "set \"CODEX_SWITCH_HOME={}\" && ", + path.display().to_string().replace('"', "") + ) + }) + .unwrap_or_default(); format!( - "cmd.exe /D /S /C \"\"set \"CODEX_HOME={}\" && \"{}\" daemon start --foreground\"\"", + "cmd.exe /D /S /C \"\"set \"CODEX_HOME={}\" && {}\"{}\" daemon start --foreground\"\"", codex_home.display().to_string().replace('"', ""), + codex_switch_home, exe.display().to_string().replace('"', "") ) } @@ -449,7 +507,8 @@ fn task_scheduler_failure_message(action: &str, detail: &str) -> String { fn install_task_scheduler() -> Result<()> { let exe = std::env::current_exe()?; let codex_home = effective_codex_home()?; - let task_run = task_scheduler_command(&exe, &codex_home); + let codex_switch_home = configured_codex_switch_home().map(PathBuf::from); + let task_run = task_scheduler_command(&exe, &codex_home, codex_switch_home.as_deref()); schtasks( &[ @@ -507,6 +566,7 @@ mod tests { "/usr/local/bin/codex-switch", "/Users/alice", "/Users/alice/.codex", + None, ); assert!(plist.contains("/usr/local/bin/codex-switch")); assert!(plist.contains("daemon")); @@ -515,6 +575,8 @@ mod tests { assert!(plist.contains("KeepAlive")); assert!(plist.contains("CODEX_HOME")); assert!(plist.contains("/Users/alice/.codex")); + // No override set: the key must be absent so the default path applies. + assert!(!plist.contains("CODEX_SWITCH_HOME")); } #[test] @@ -523,18 +585,32 @@ mod tests { "/Applications/A & B/codex-switch", "/Users/aCODEX_SWITCH_HOME")); + assert!(plist.contains("/Users/alice/relocated & store")); + } + #[test] fn systemd_unit_runs_foreground_daemon() { let unit = systemd_unit( "/usr/local/bin/codex-switch", "/home/alice", "/home/alice/.codex", + None, ); assert!( unit.contains("ExecStart=\"/usr/local/bin/codex-switch\" daemon start --foreground") @@ -542,6 +618,7 @@ mod tests { assert!(unit.contains("Restart=on-failure")); assert!(unit.contains("Environment=\"HOME=/home/alice\"")); assert!(unit.contains("Environment=\"CODEX_HOME=/home/alice/.codex\"")); + assert!(!unit.contains("CODEX_SWITCH_HOME")); } #[test] @@ -550,6 +627,7 @@ mod tests { r#"/opt/Codex & Tools\\codex-switch"#, "/home/a & b", r#"/home/a & b/.codex\\custom"#, + None, ); assert!(unit.contains( r#"ExecStart="/opt/Codex & Tools\\\\codex-switch" daemon start --foreground"# @@ -558,11 +636,23 @@ mod tests { assert!(unit.contains(r#"Environment="CODEX_HOME=/home/a & b/.codex\\\\custom""#)); } + #[test] + fn systemd_unit_forwards_codex_switch_home_when_set() { + let unit = systemd_unit( + "/usr/local/bin/codex-switch", + "/home/alice", + "/home/alice/.codex", + Some("/home/alice/relocated"), + ); + assert!(unit.contains(r#"Environment="CODEX_SWITCH_HOME=/home/alice/relocated""#)); + } + #[test] fn windows_task_scheduler_command_quotes_exe_path() { let cmd = task_scheduler_command( Path::new(r"C:\Program Files\codex-switch.exe"), Path::new(r"C:\Users\A & B\.codex"), + None, ); assert_eq!( cmd, @@ -570,6 +660,19 @@ mod tests { ); } + #[test] + fn windows_task_scheduler_command_forwards_codex_switch_home_when_set() { + let cmd = task_scheduler_command( + Path::new(r"C:\Program Files\codex-switch.exe"), + Path::new(r"C:\Users\A & B\.codex"), + Some(Path::new(r"C:\Users\A & B\relocated")), + ); + assert_eq!( + cmd, + r#"cmd.exe /D /S /C ""set "CODEX_HOME=C:\Users\A & B\.codex" && set "CODEX_SWITCH_HOME=C:\Users\A & B\relocated" && "C:\Program Files\codex-switch.exe" daemon start --foreground"""# + ); + } + #[test] fn windows_task_scheduler_create_error_includes_elevation_guidance() { assert_eq!(