systemprompt_models/subprocess.rs
1//! Environment-marker contract between the supervisor and the detached agent
2//! and MCP children it spawns.
3//!
4//! The supervisor stamps these markers at spawn time; shutdown and
5//! reconciliation read them back from `/proc/<pid>/environ` to confirm a
6//! registry PID still names *this* installation's child before signalling it.
7//! PIDs are recycled, and group-signalling a stale PID (`kill(-pid)`) could
8//! reach an unrelated session leader — so a row is only ever signalled once
9//! both the subprocess marker and the exact `name_key=service_name` pairing
10//! are found.
11
12pub const SUBPROCESS_MARKER_ENV: &str = "SYSTEMPROMPT_SUBPROCESS";
13pub const AGENT_NAME_ENV: &str = "AGENT_NAME";
14pub const MCP_SERVICE_ID_ENV: &str = "MCP_SERVICE_ID";
15
16/// Convert an OS process id into the signed form `kill(2)` expects, rejecting
17/// any value that would target more than that single process.
18///
19/// A `u32` above `i32::MAX` wraps to a negative `i32`, and `kill(2)` reads a
20/// negative pid as a *process group* — `-1` broadcasts to **every** process the
21/// caller may signal, and `0` means the caller's own group. Routing every pid
22/// through this guard turns those cases into a no-op (`None`) instead of
23/// letting a single-PID request escalate into a group or session-wide kill.
24#[must_use]
25pub fn signalable_pid(pid: u32) -> Option<i32> {
26 if pid == 0 {
27 return None;
28 }
29 i32::try_from(pid).ok()
30}
31
32#[must_use]
33pub fn environ_identifies_child(environ: &[u8], name_key: &str, service_name: &str) -> bool {
34 let marker = format!("{SUBPROCESS_MARKER_ENV}=1");
35 let expected_name = format!("{name_key}={service_name}");
36
37 let mut has_marker = false;
38 let mut has_name = false;
39 for entry in environ.split(|&b| b == 0) {
40 if entry == marker.as_bytes() {
41 has_marker = true;
42 } else if entry == expected_name.as_bytes() {
43 has_name = true;
44 }
45 }
46
47 has_marker && has_name
48}