diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 02:39:58 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-16 02:40:39 +0300 |
| commit | 947328fa79fcefd2fa8087de014168d46a641312 (patch) | |
| tree | 15d89163b821dcbd11a05460047e0ccd52e1b66c /src/platform/macos.rs | |
| parent | a22454f27ce096585e19d1787dba43f56d1505cf (diff) | |
feat(macos): support agent environment hints
refs https://github.com/ogulcancelik/herdr/discussions/679
Diffstat (limited to 'src/platform/macos.rs')
| -rw-r--r-- | src/platform/macos.rs | 83 |
1 files changed, 72 insertions, 11 deletions
diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 70eea7ec..f35fd1eb 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -842,6 +842,33 @@ fn process_argv(pid: u32) -> Option<Vec<String>> { procargs2_argv(&buf) } +/// Read a Herdr agent identity hint from a process environment. +pub fn process_agent_hint(pid: u32) -> Option<crate::detect::Agent> { + if pid == 0 { + return None; + } + let buf = kern_procargs2(pid)?; + super::parse_agent_env_hint(procargs2_env(&buf)?) +} + +fn procargs2_argv_start(rest: &[u8]) -> Option<usize> { + let exec_end = rest.iter().position(|&byte| byte == 0)?; + let mut pos = exec_end; + while pos < rest.len() && rest[pos] == 0 { + pos += 1; + } + (pos < rest.len()).then_some(pos) +} + +fn skip_nul_strings(bytes: &[u8], start: usize, count: usize) -> Option<usize> { + let mut current = start; + for _ in 0..count { + let end = bytes.get(current..)?.iter().position(|&byte| byte == 0)?; + current = current.checked_add(end)?.checked_add(1)?; + } + Some(current) +} + fn procargs2_argv(buf: &[u8]) -> Option<Vec<String>> { if buf.len() < 4 { return None; @@ -852,19 +879,10 @@ fn procargs2_argv(buf: &[u8]) -> Option<Vec<String>> { return None; } - // Layout: [argc: i32] [exec_path\0] [padding\0...] [argv[0]\0] [argv[1]\0] ... [env\0] ... + // Layout: [argc: i32] [exec_path\0] [padding\0...] [argv[0]\0] ... [env\0] ... let rest = &buf[4..]; - let exec_end = rest.iter().position(|&b| b == 0)?; - let mut pos = exec_end; - while pos < rest.len() && rest[pos] == 0 { - pos += 1; - } - if pos >= rest.len() { - return None; - } - + let mut current = procargs2_argv_start(rest)?; let mut argv = Vec::with_capacity(argc as usize); - let mut current = pos; for _ in 0..argc { if current >= rest.len() { return None; @@ -884,6 +902,22 @@ fn procargs2_argv(buf: &[u8]) -> Option<Vec<String>> { Some(argv) } +fn procargs2_env(buf: &[u8]) -> Option<&[u8]> { + if buf.len() < 4 { + return None; + } + + let argc = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]); + if argc < 1 { + return None; + } + + let rest = &buf[4..]; + let argv_start = procargs2_argv_start(rest)?; + let env_start = skip_nul_strings(rest, argv_start, argc as usize)?; + rest.get(env_start..) +} + /// Get the current working directory of a process. /// /// Uses `proc_pidinfo(PROC_PIDVNODEPATHINFO)` to read `pvi_cdir.vip_path`. @@ -1068,6 +1102,33 @@ mod tests { } #[test] + fn procargs2_env_reads_agent_hint_after_argv() { + let buf = build_procargs2( + "/opt/homebrew/bin/nono", + &["nono", "run", "HERDR_AGENT=codex", "--", "claude"], + &["PATH=/usr/bin", "HERDR_AGENT=claude", "TERM=xterm-256color"], + ); + + let env = procargs2_env(&buf).expect("expected env block"); + assert_eq!( + crate::platform::parse_agent_env_hint(env), + Some(crate::detect::Agent::Claude) + ); + } + + #[test] + fn procargs2_env_does_not_treat_argv_as_environment() { + let buf = build_procargs2( + "/opt/homebrew/bin/nono", + &["nono", "run", "HERDR_AGENT=claude"], + &["PATH=/usr/bin"], + ); + + let env = procargs2_env(&buf).expect("expected env block"); + assert_eq!(crate::platform::parse_agent_env_hint(env), None); + } + + #[test] fn terminal_bundle_identifier_maps_known_terminal_env() { assert_eq!( terminal_bundle_identifier_from_env(Some("ghostty"), None, false, false), |
