diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-21 22:15:01 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-21 22:17:37 +0300 |
| commit | 659badf106786f5611e750de0e4936e47c69a3d4 (patch) | |
| tree | 3cad8ed17a4e4d91d5e178e953a2349e6afa4b42 /src | |
| parent | f7fa9486855c824bcd7e783e2d3eb4d12ad85205 (diff) | |
fix: avoid agent detection from runtime arguments
refs #238
Diffstat (limited to 'src')
| -rw-r--r-- | src/detect.rs | 254 | ||||
| -rw-r--r-- | src/pane/osc.rs | 12 | ||||
| -rw-r--r-- | src/platform/linux.rs | 8 | ||||
| -rw-r--r-- | src/platform/macos.rs | 9 | ||||
| -rw-r--r-- | src/platform/mod.rs | 1 |
5 files changed, 210 insertions, 74 deletions
diff --git a/src/detect.rs b/src/detect.rs index 5767a0b6..dd7d21c8 100644 --- a/src/detect.rs +++ b/src/detect.rs @@ -824,7 +824,7 @@ fn normalized_process_name(process: &crate::platform::ForegroundProcess) -> Stri if is_generic_runtime_or_shell(&lower_effective) { if let Some(wrapped_agent) = - wrapped_agent_name_from_cmdline(process.cmdline.as_deref().unwrap_or_default()) + wrapped_agent_name_from_runtime_argv(&lower_effective, process.argv.as_deref()) { return wrapped_agent; } @@ -834,8 +834,8 @@ fn normalized_process_name(process: &crate::platform::ForegroundProcess) -> Stri return effective.to_string(); } - if let Some(wrapped_agent) = - cmdline_argv0_agent_name(process.cmdline.as_deref().unwrap_or_default()) + if let Some(wrapped_agent) = argv0_agent_name(process.argv.as_deref()) + .or_else(|| cmdline_argv0_agent_name(process.cmdline.as_deref().unwrap_or_default())) { return wrapped_agent; } @@ -843,16 +843,89 @@ fn normalized_process_name(process: &crate::platform::ForegroundProcess) -> Stri effective.to_string() } -fn wrapped_agent_name_from_cmdline(cmdline: &str) -> Option<String> { - for token in cmdline.split_whitespace() { - if let Some(agent_name) = agent_name_from_path_token(token) { - return Some(agent_name); +fn wrapped_agent_name_from_runtime_argv(runtime: &str, argv: Option<&[String]>) -> Option<String> { + let argv = argv?; + let runtime = path_basename(runtime).to_lowercase(); + + match runtime.as_str() { + "node" | "bun" => script_arg_agent_name(argv, &["-e", "--eval", "-p", "--print"], &[]), + "python" | "python3" => script_arg_agent_name(argv, &["-c"], &["-m"]), + "sh" | "bash" | "zsh" | "fish" => script_arg_agent_name(argv, &["-c"], &[]), + "tmux" => None, + _ => None, + } +} + +fn script_arg_agent_name( + argv: &[String], + eval_flags: &[&str], + module_flags: &[&str], +) -> Option<String> { + let mut args = argv.iter().skip(1); + while let Some(arg) = args.next() { + if arg == "--" { + return args + .next() + .and_then(|token| agent_name_from_path_token(token)); } + + if flag_matches(arg, eval_flags) || flag_matches(arg, module_flags) { + return None; + } + + if arg.starts_with('-') { + if option_takes_value(arg) { + let _ = args.next(); + } + continue; + } + + return agent_name_from_path_token(arg); } None } +fn flag_matches(arg: &str, flags: &[&str]) -> bool { + flags + .iter() + .any(|flag| arg == *flag || short_flag_payload(arg, flag) || long_flag_value(arg, flag)) +} + +fn short_flag_payload(arg: &str, flag: &str) -> bool { + flag.starts_with('-') + && !flag.starts_with("--") + && arg.starts_with(flag) + && arg.len() > flag.len() +} + +fn long_flag_value(arg: &str, flag: &str) -> bool { + flag.starts_with("--") + && arg + .strip_prefix(flag) + .is_some_and(|rest| rest.starts_with('=')) +} + +fn option_takes_value(arg: &str) -> bool { + matches!( + arg, + "-r" | "--require" + | "--loader" + | "--import" + | "--experimental-loader" + | "--inspect-port" + | "-W" + | "-X" + | "-S" + | "-L" + | "-o" + ) +} + +fn argv0_agent_name(argv: Option<&[String]>) -> Option<String> { + agent_name_from_path_token(argv?.first()?) +} + fn cmdline_argv0_agent_name(cmdline: &str) -> Option<String> { agent_name_from_path_token(cmdline.split_whitespace().next()?) } @@ -863,14 +936,18 @@ fn agent_name_from_path_token(token: &str) -> Option<String> { return None; } - let basename = std::path::Path::new(trimmed) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(trimmed); + let basename = path_basename(trimmed); let agent = parse_agent_label(basename)?; Some(agent_label(agent).to_string()) } +fn path_basename(path: &str) -> &str { + std::path::Path::new(path) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(path) +} + fn process_priority(process: &crate::platform::ForegroundProcess, normalized_name: &str) -> u8 { let lower_name = normalized_name.to_lowercase(); if lower_name != process.name.to_lowercase() { @@ -897,6 +974,20 @@ fn is_generic_runtime_or_shell(name: &str) -> bool { mod tests { use super::*; + fn foreground_process( + pid: u32, + name: &str, + argv: &[&str], + ) -> crate::platform::ForegroundProcess { + crate::platform::ForegroundProcess { + pid, + name: name.to_string(), + argv0: None, + argv: Some(argv.iter().map(|arg| (*arg).to_string()).collect()), + cmdline: Some(argv.join(" ")), + } + } + // ---- Agent identification ---- #[test] @@ -972,18 +1063,8 @@ mod tests { let job = crate::platform::ForegroundJob { process_group_id: 123, processes: vec![ - crate::platform::ForegroundProcess { - pid: 1, - name: "node".to_string(), - argv0: None, - cmdline: Some("node /path/to/bin/codex".to_string()), - }, - crate::platform::ForegroundProcess { - pid: 2, - name: "bash".to_string(), - argv0: None, - cmdline: Some("bash".to_string()), - }, + foreground_process(1, "node", &["node", "/path/to/bin/codex"]), + foreground_process(2, "bash", &["bash"]), ], }; @@ -998,18 +1079,8 @@ mod tests { let job = crate::platform::ForegroundJob { process_group_id: 42, processes: vec![ - crate::platform::ForegroundProcess { - pid: 42, - name: "claude".to_string(), - argv0: None, - cmdline: Some("claude".to_string()), - }, - crate::platform::ForegroundProcess { - pid: 43, - name: "node".to_string(), - argv0: None, - cmdline: Some("node /tmp/mcp/bin/codex".to_string()), - }, + foreground_process(42, "claude", &["claude"]), + foreground_process(43, "node", &["node", "/tmp/mcp/bin/codex"]), ], }; @@ -1024,18 +1095,8 @@ mod tests { let job = crate::platform::ForegroundJob { process_group_id: 42, processes: vec![ - crate::platform::ForegroundProcess { - pid: 42, - name: "bash".to_string(), - argv0: None, - cmdline: Some("bash".to_string()), - }, - crate::platform::ForegroundProcess { - pid: 43, - name: "node".to_string(), - argv0: None, - cmdline: Some("node /tmp/mcp/bin/codex".to_string()), - }, + foreground_process(42, "bash", &["bash"]), + foreground_process(43, "node", &["node", "/tmp/mcp/bin/codex"]), ], }; @@ -1049,12 +1110,11 @@ mod tests { fn identify_agent_in_job_detects_nix_wrapped_codex_from_cmdline_argv0() { let job = crate::platform::ForegroundJob { process_group_id: 123, - processes: vec![crate::platform::ForegroundProcess { - pid: 1, - name: ".codex-wrapped".to_string(), - argv0: None, - cmdline: Some("/etc/profiles/per-user/user/bin/codex --model gpt-5".to_string()), - }], + processes: vec![foreground_process( + 1, + ".codex-wrapped", + &["/etc/profiles/per-user/user/bin/codex", "--model", "gpt-5"], + )], }; assert_eq!( @@ -1067,12 +1127,11 @@ mod tests { fn identify_agent_in_job_canonicalizes_nix_wrapped_aliases_from_cmdline_argv0() { let job = crate::platform::ForegroundJob { process_group_id: 123, - processes: vec![crate::platform::ForegroundProcess { - pid: 1, - name: ".claude-code-wrapped".to_string(), - argv0: None, - cmdline: Some("/nix/store/example/bin/claude-code".to_string()), - }], + processes: vec![foreground_process( + 1, + ".claude-code-wrapped", + &["/nix/store/example/bin/claude-code"], + )], }; assert_eq!( @@ -1085,12 +1144,11 @@ mod tests { fn identify_agent_in_job_detects_shell_wrapped_pi() { let job = crate::platform::ForegroundJob { process_group_id: 123, - processes: vec![crate::platform::ForegroundProcess { - pid: 1, - name: "sh".to_string(), - argv0: None, - cmdline: Some("/bin/sh /tmp/test-bin/pi".to_string()), - }], + processes: vec![foreground_process( + 1, + "sh", + &["/bin/sh", "/tmp/test-bin/pi"], + )], }; assert_eq!( @@ -1100,8 +1158,70 @@ mod tests { } #[test] - fn wrapped_agent_name_from_cmdline_ignores_plain_shell_flags() { - assert_eq!(wrapped_agent_name_from_cmdline("bash -lc"), None); + fn wrapped_agent_name_from_runtime_argv_ignores_plain_shell_flags() { + assert_eq!( + wrapped_agent_name_from_runtime_argv("bash", Some(&["bash".into(), "-lc".into()])), + None + ); + } + + #[test] + fn identify_agent_in_job_ignores_python_c_argument_named_codex() { + let job = crate::platform::ForegroundJob { + process_group_id: 123, + processes: vec![foreground_process( + 1, + "python3", + &["python3", "-c", "import time; time.sleep(60)", "/tmp/codex"], + )], + }; + + assert_eq!(identify_agent_in_job(&job), None); + } + + #[test] + fn identify_agent_in_job_ignores_node_eval_argument_named_codex() { + let job = crate::platform::ForegroundJob { + process_group_id: 123, + processes: vec![foreground_process( + 1, + "node", + &["node", "-e", "setTimeout(() => {}, 60000)", "/tmp/codex"], + )], + }; + + assert_eq!(identify_agent_in_job(&job), None); + } + + #[test] + fn identify_agent_in_job_ignores_shell_c_argument_named_codex() { + let job = crate::platform::ForegroundJob { + process_group_id: 123, + processes: vec![foreground_process( + 1, + "bash", + &["bash", "-c", "sleep 60", "/tmp/codex"], + )], + }; + + assert_eq!(identify_agent_in_job(&job), None); + } + + #[test] + fn identify_agent_in_job_detects_python_script_named_codex() { + let job = crate::platform::ForegroundJob { + process_group_id: 123, + processes: vec![foreground_process( + 1, + "python3", + &["python3", "/tmp/codex", "--model", "gpt-5"], + )], + }; + + assert_eq!( + identify_agent_in_job(&job), + Some((Agent::Codex, "codex".to_string())) + ); } #[test] diff --git a/src/pane/osc.rs b/src/pane/osc.rs index b5278125..0f04b045 100644 --- a/src/pane/osc.rs +++ b/src/pane/osc.rs @@ -376,6 +376,7 @@ mod tests { pid: shell_pid, name: "zsh".to_string(), argv0: Some("zsh".to_string()), + argv: Some(vec!["zsh".to_string()]), cmdline: Some("zsh".to_string()), }], } @@ -550,6 +551,10 @@ mod tests { pid: 42, name: "droid".to_string(), argv0: None, + argv: Some(vec![ + "/opt/factory/droid".to_string(), + "--resume".to_string(), + ]), cmdline: Some("/opt/factory/droid --resume".to_string()), }], }; @@ -561,6 +566,11 @@ mod tests { pid: 42, name: "bun".to_string(), argv0: Some("bun".to_string()), + argv: Some(vec![ + "bun".to_string(), + "/home/can/.local/bin/droid".to_string(), + "--resume".to_string(), + ]), cmdline: Some("/home/can/.local/bin/droid --resume".to_string()), }], }; @@ -584,6 +594,7 @@ mod tests { pid: 42, name: "droid".to_string(), argv0: Some("droid".to_string()), + argv: Some(vec!["droid".to_string()]), cmdline: Some("droid".to_string()), }], }; @@ -626,6 +637,7 @@ mod tests { pid: 42, name: "droid".to_string(), argv0: Some("droid".to_string()), + argv: Some(vec!["droid".to_string()]), cmdline: Some("droid".to_string()), }], }), diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 269e3ad8..73a4f0a5 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -34,11 +34,13 @@ pub fn foreground_job(child_pid: u32) -> Option<ForegroundJob> { continue; } + let argv = process_argv(pid); processes.push(ForegroundProcess { pid, name, argv0: None, - cmdline: process_cmdline(pid), + cmdline: argv.as_ref().map(|parts| parts.join(" ")), + argv, }); } @@ -73,7 +75,7 @@ fn process_pgrp_and_comm(pid: u32) -> Option<(i32, String)> { Some((pgrp, comm)) } -fn process_cmdline(pid: u32) -> Option<String> { +fn process_argv(pid: u32) -> Option<Vec<String>> { let bytes = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?; if bytes.is_empty() { return None; @@ -83,7 +85,7 @@ fn process_cmdline(pid: u32) -> Option<String> { .filter(|part| !part.is_empty()) .map(|part| String::from_utf8_lossy(part).into_owned()) .collect(); - (!parts.is_empty()).then(|| parts.join(" ")) + (!parts.is_empty()).then_some(parts) } /// Get the current working directory of a process. diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 444f458d..add51aa4 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -32,11 +32,13 @@ pub fn foreground_job(child_pid: u32) -> Option<ForegroundJob> { let Some(name) = comm_from_bsdinfo(&info) else { continue; }; + let argv = process_argv(pid); processes.push(ForegroundProcess { pid, name, argv0: process_argv0_name(pid), - cmdline: process_cmdline(pid), + cmdline: argv.as_ref().map(|parts| parts.join(" ")), + argv, }); } @@ -462,10 +464,9 @@ fn comm_from_bsdinfo(info: &libc::proc_bsdinfo) -> Option<String> { String::from_utf8(bytes).ok() } -fn process_cmdline(pid: u32) -> Option<String> { +fn process_argv(pid: u32) -> Option<Vec<String>> { let buf = kern_procargs2(pid)?; - let argv = procargs2_argv(&buf)?; - Some(argv.join(" ")) + procargs2_argv(&buf) } fn procargs2_argv(buf: &[u8]) -> Option<Vec<String>> { diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 8cf2b08d..a836ed81 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -8,6 +8,7 @@ pub struct ForegroundProcess { pub pid: u32, pub name: String, pub argv0: Option<String>, + pub argv: Option<Vec<String>>, pub cmdline: Option<String>, } |
