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");