Skip to main content

lilo_rm_core/
spawn_context.rs

1use std::ffi::OsString;
2use std::path::PathBuf;
3
4use crate::LaunchEnv;
5
6/// Exact env-var names dropped when forwarding caller env into a spawned runtime.
7///
8/// These either name the calling process's parent context (so forwarding lies to the spawned
9/// runtime about who its parent is) or are daemon/test internals the daemon re-sets correctly.
10pub const CALLER_ENV_DENYLIST: &[&str] = &[
11    "CLAUDECODE",
12    "TMUX",
13    "TMUX_PANE",
14    "RTM_SOCKET_PATH",
15    "RTM_DB_PATH",
16    "HELIOY_SESSION_ID",
17    "HELIOY_RUNTIME",
18    "RTM_SESSION_ID",
19    "RTM_RUNTIME_KIND",
20];
21
22/// Prefixes dropped when forwarding caller env. Used for variable families
23/// like `CLAUDE_CODE_*` and `CLAUDE_PLUGIN_*` that describe the calling claude
24/// instance, not user state.
25pub const CALLER_ENV_DENYLIST_PREFIXES: &[&str] = &["CLAUDE_CODE_", "CLAUDE_PLUGIN_"];
26
27/// Capture the caller's environment, filtered through the denylist.
28///
29/// Iterates `std::env::vars_os()` so non-UTF-8 keys and values do not panic.
30/// Lossy decoding is applied via [`capture_env_from_os`]: we choose lossy over
31/// reject because env values are an open universe, and refusing on the first
32/// non-UTF-8 byte would break callers on systems with non-UTF-8 locales for
33/// reasons unrelated to anything rtm cares about.
34pub fn capture_caller_env() -> Vec<LaunchEnv> {
35    capture_env_from_os(std::env::vars_os())
36}
37
38/// Variant of [`capture_env_from`] that accepts `OsString` keys and values
39/// (the shape returned by `std::env::vars_os()`). Lossy-converts both before
40/// applying the denylist. Exposed for tests that want to feed `OsString`
41/// directly, exercising the same code path as `capture_caller_env`.
42pub fn capture_env_from_os<I>(iter: I) -> Vec<LaunchEnv>
43where
44    I: IntoIterator<Item = (OsString, OsString)>,
45{
46    capture_env_from(iter.into_iter().map(|(k, v)| {
47        (
48            k.to_string_lossy().into_owned(),
49            v.to_string_lossy().into_owned(),
50        )
51    }))
52}
53
54/// Filter an iterator of `(String, String)` env entries through the denylist.
55/// Use [`capture_caller_env`] or [`capture_env_from_os`] for OS-sourced env;
56/// this lower-level variant is the right choice when env is already UTF-8.
57pub fn capture_env_from<I, K, V>(iter: I) -> Vec<LaunchEnv>
58where
59    I: IntoIterator<Item = (K, V)>,
60    K: Into<String>,
61    V: Into<String>,
62{
63    iter.into_iter()
64        .map(|(k, v)| (k.into(), v.into()))
65        .filter(|(k, _)| !is_denied(k))
66        .map(|(k, v)| LaunchEnv::new(k, v))
67        .collect()
68}
69
70fn is_denied(key: &str) -> bool {
71    if CALLER_ENV_DENYLIST.contains(&key) {
72        return true;
73    }
74    CALLER_ENV_DENYLIST_PREFIXES
75        .iter()
76        .any(|prefix| key.starts_with(prefix))
77}
78
79/// Capture the caller's current working directory.
80pub fn capture_caller_cwd() -> std::io::Result<PathBuf> {
81    std::env::current_dir()
82}
83
84/// Placeholder cwd for call sites that only exercise launcher resolution
85/// (argv lookup, env builders) and never actually spawn a runtime in the
86/// returned directory. Centralized so future audits can grep for it and know
87/// "this cwd was deliberately not load-bearing."
88pub fn launcher_probe_cwd() -> PathBuf {
89    PathBuf::from("/")
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn denylist_drops_parent_markers() {
98        let env = capture_env_from([
99            ("PATH", "/usr/bin"),
100            ("CLAUDECODE", "1"),
101            ("CLAUDE_CODE_SESSION_ID", "abc"),
102            ("CLAUDE_PLUGIN_DATA", "/tmp"),
103            ("TMUX", "/private/tmp/tmux"),
104            ("TMUX_PANE", "%4"),
105            ("RTM_SOCKET_PATH", "/tmp/rtm.sock"),
106            ("RTM_DB_PATH", "/tmp/rtm.db"),
107            ("HELIOY_SESSION_ID", "session"),
108            ("HELIOY_RUNTIME", "claude"),
109            ("RTM_SESSION_ID", "session"),
110            ("RTM_RUNTIME_KIND", "claude"),
111            ("HELIOY_PAT", "ghp_secret"),
112            ("ANTHROPIC_API_KEY", "sk-secret"),
113        ]);
114        let keys: Vec<&str> = env.iter().map(|e| e.key.as_str()).collect();
115        assert_eq!(keys, vec!["PATH", "HELIOY_PAT", "ANTHROPIC_API_KEY"]);
116    }
117
118    #[test]
119    fn denylist_keeps_user_state() {
120        let env = capture_env_from([
121            ("PATH", "/usr/bin"),
122            ("HOME", "/Users/alphab"),
123            ("LANG", "en_US.UTF-8"),
124            ("MISE_SHELL", "zsh"),
125        ]);
126        assert_eq!(env.len(), 4);
127    }
128
129    #[test]
130    fn capture_env_from_os_tolerates_non_utf8() {
131        use std::os::unix::ffi::OsStringExt;
132
133        // 0xFF is invalid as a leading UTF-8 byte. capture_env_from_os runs
134        // the same lossy conversion path as capture_caller_env (the runtime
135        // entry point), so this test protects the actual production path
136        // rather than the already-UTF-8 capture_env_from variant.
137        let raw_value = OsString::from_vec(vec![b'A', 0xFF, b'B']);
138        let env = capture_env_from_os([(OsString::from("RTM_TEST_BAD_BYTES"), raw_value)]);
139        assert_eq!(env.len(), 1);
140        assert_eq!(env[0].key, "RTM_TEST_BAD_BYTES");
141        assert!(env[0].value.contains('\u{FFFD}'), "{:?}", env[0].value);
142    }
143
144    #[test]
145    fn capture_env_from_os_applies_denylist() {
146        // Denylist must also run through the OsString path, not only the
147        // UTF-8 capture_env_from path.
148        let env = capture_env_from_os([
149            (OsString::from("PATH"), OsString::from("/usr/bin")),
150            (OsString::from("CLAUDECODE"), OsString::from("1")),
151            (
152                OsString::from("CLAUDE_CODE_SESSION_ID"),
153                OsString::from("abc"),
154            ),
155        ]);
156        let keys: Vec<&str> = env.iter().map(|e| e.key.as_str()).collect();
157        assert_eq!(keys, vec!["PATH"]);
158    }
159}