From c7f42eac624c55c5e2569bfd0b179ef8d5794793 Mon Sep 17 00:00:00 2001 From: "Jason (Json)" <263060202+fuller-stack-dev@users.noreply.github.com> Date: Sat, 12 Sep 2026 01:34:10 -0600 Subject: [PATCH] fix: detect native CLI logins in supervised gateways --- src/supervisor/mod.rs | 39 +++++++++++++++++++++++- tests/service_command_tests.rs | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/supervisor/mod.rs b/src/supervisor/mod.rs index 63f1ea6c..b97ba245 100644 --- a/src/supervisor/mod.rs +++ b/src/supervisor/mod.rs @@ -67,6 +67,9 @@ const SERVICE_PROXY_ENV_KEYS: [&str; 8] = [ "all_proxy", ]; const SERVICE_EXTRA_ENV_KEYS: [&str; 2] = ["NODE_EXTRA_CA_CERTS", "NODE_USE_SYSTEM_CA"]; +// Native CLIs use the account name to find their existing login (for example, +// Claude's macOS Keychain account). Keep it across both service boundaries. +const SERVICE_USER_ENV_KEYS: [&str; 2] = ["USER", "LOGNAME"]; const SUPERVISED_CHILD_BASE_ENV_KEYS: [&str; 5] = ["HOME", "PATH", "OCM_HOME", "OCM_SELF", "SHELL"]; const SUPERVISED_CHILD_RUNTIME_ENV_KEYS: [&str; 4] = ["NODE_OPTIONS", "NODE_ENV", "NODE_PATH", "PNPM_HOME"]; @@ -2571,7 +2574,10 @@ fn supervisor_service_environment( service_env.insert(key.to_string(), value.trim().to_string()); } } - for key in SERVICE_EXTRA_ENV_KEYS { + for key in SERVICE_EXTRA_ENV_KEYS + .into_iter() + .chain(SERVICE_USER_ENV_KEYS) + { if let Some(value) = process_env .get(key) .filter(|value| !value.trim().is_empty()) @@ -2830,6 +2836,7 @@ fn stable_supervised_child_env_key(key: &str) -> bool { || key.starts_with("npm_config_") || key.starts_with("COREPACK_") || SUPERVISED_CHILD_BASE_ENV_KEYS.contains(&key) + || SERVICE_USER_ENV_KEYS.contains(&key) || SUPERVISED_CHILD_RUNTIME_ENV_KEYS.contains(&key) || SERVICE_PROXY_ENV_KEYS.contains(&key) || SERVICE_EXTRA_ENV_KEYS.contains(&key) @@ -4093,6 +4100,36 @@ mod tests { assert!(!process_env.contains_key("OPENCLAW_WINDOWS_TASK_NAME")); } + #[test] + fn service_environments_preserve_only_nonempty_user_identity() { + for (user, logname) in [(" fixture-user ", "fixture-login"), (" ", "")] { + let env = BTreeMap::from([ + ("USER".to_string(), user.to_string()), + ("LOGNAME".to_string(), logname.to_string()), + ("GH_TOKEN".to_string(), "fixture-token".to_string()), + ("LD_PRELOAD".to_string(), "fixture.so".to_string()), + ( + "DYLD_INSERT_LIBRARIES".to_string(), + "fixture.dylib".to_string(), + ), + ]); + for actual in [ + supervisor_service_environment(&env, Path::new("/fixture"), Path::new("/bin/ocm")), + build_supervised_openclaw_env(env), + ] { + for (key, value) in [("USER", user), ("LOGNAME", logname)] { + assert_eq!( + actual.get(key).map(String::as_str), + (!value.trim().is_empty()).then_some(value.trim()) + ); + } + for excluded in ["GH_TOKEN", "LD_PRELOAD", "DYLD_INSERT_LIBRARIES"] { + assert!(!actual.contains_key(excluded)); + } + } + } + } + #[test] fn supervised_child_env_drops_volatile_caller_context() { let process_env = build_supervised_openclaw_env(BTreeMap::from([ diff --git a/tests/service_command_tests.rs b/tests/service_command_tests.rs index cadfe1d6..3647dd7b 100644 --- a/tests/service_command_tests.rs +++ b/tests/service_command_tests.rs @@ -350,6 +350,61 @@ fn service_install_enables_the_env_and_installs_the_ocm_service() { ); } +#[test] +fn service_start_preserves_native_cli_identity_across_service_boundaries() { + for systemd in [false, true] { + let root = TestDir::new("service-native-cli-identity"); + let cwd = root.child("workspace"); + fs::create_dir_all(&cwd).unwrap(); + let mut env = if systemd { + systemd_env(&root) + } else { + launchd_env(&root) + }; + env.insert("USER".to_string(), "fixture-user".to_string()); + env.insert("LOGNAME".to_string(), "fixture-user".to_string()); + env.insert("GH_TOKEN".to_string(), "fixture-token".to_string()); + env.insert( + "CODEX_SESSION_ID".to_string(), + "fixture-session".to_string(), + ); + let marker = root.child("native-cli-identity.txt"); + write_executable_script( + &root.child("fake-bin/openclaw"), + &format!( + "#!/bin/sh\nprintf '%s\\n' \"${{USER-unset}}:${{LOGNAME-unset}}:${{GH_TOKEN-unset}}:${{CODEX_SESSION_ID-unset}}\" > '{}'\n", + path_string(&marker) + ), + ); + setup_launcher_env(&cwd, &env); + let started = run_ocm(&cwd, &env, &["service", "start", "demo", "--json"]); + assert!(started.status.success(), "{}", stderr(&started)); + + let definition = + fs::read_to_string(managed_service_definition_path(&env, &cwd, "ocm")).unwrap(); + for key in ["USER", "LOGNAME"] { + let expected = if systemd { + format!("Environment=\"{key}=fixture-user\"") + } else { + format!("{key}\n fixture-user") + }; + assert!(definition.contains(&expected), "{definition}"); + } + assert!(!definition.contains("fixture-token")); + assert!(!definition.contains("fixture-session")); + + // The daemon must launch with the saved identity, not an ambient shell's. + env.remove("USER"); + env.remove("LOGNAME"); + let run = run_ocm(&cwd, &env, &["__daemon", "run", "--once", "--json"]); + assert!(run.status.success(), "{}", stderr(&run)); + assert_eq!( + fs::read_to_string(&marker).unwrap(), + "fixture-user:fixture-user:unset:unset\n" + ); + } +} + #[test] fn service_install_uses_valid_path_ocm_for_dev_artifact() { let root = TestDir::new("service-install-valid-path-ocm");