Skip to main content

systemprompt_models/subprocess/
mod.rs

1//! Identity contract for the detached agent and MCP children the supervisor
2//! owns: the environment markers stamped at spawn time and the pure parsers
3//! that read them back off a live process image.
4//!
5//! The supervisor stamps [`SUBPROCESS_MARKER_ENV`] and a `name_key=service`
6//! pair into every child; shutdown, reconciliation, and port reclamation
7//! confirm a registry PID still names *this* installation's child before
8//! signalling it. PIDs are recycled, and group-signalling a stale PID
9//! (`kill(-pid)`) could reach an unrelated session leader — so a row is only
10//! ever signalled once both the marker and the exact pairing are found.
11//!
12//! Spawning and the platform-specific process probes live in
13//! `systemprompt_loader::subprocess`; this module holds only data and pure
14//! functions so the shared layer stays free of process I/O.
15//!
16//! Copyright (c) systemprompt.io — Business Source License 1.1.
17//! See <https://systemprompt.io> for licensing details.
18
19pub const SUBPROCESS_MARKER_ENV: &str = "SYSTEMPROMPT_SUBPROCESS";
20pub const AGENT_NAME_ENV: &str = "AGENT_NAME";
21pub const MCP_SERVICE_ID_ENV: &str = "MCP_SERVICE_ID";
22
23pub const DEPLOYMENT_HOST_ENV: &str = "SYSTEMPROMPT_DEPLOYMENT_HOST";
24
25// Why: Fly injects `FLY_APP_NAME` into deployed machines.
26const FLY_HOST_ENV: &str = "FLY_APP_NAME";
27
28pub fn deployment_host(lookup: impl Fn(&str) -> Option<String>) -> Option<String> {
29    [DEPLOYMENT_HOST_ENV, FLY_HOST_ENV].iter().find_map(|name| {
30        lookup(name)
31            .map(|value| value.trim().to_owned())
32            .filter(|value| !value.is_empty())
33    })
34}
35
36pub fn is_deployment_host(lookup: impl Fn(&str) -> Option<String>) -> bool {
37    deployment_host(lookup).is_some()
38}
39
40pub fn inherited_parent_env(lookup: impl Fn(&str) -> Option<String>) -> Vec<(String, String)> {
41    let mut env: Vec<(String, String)> = [
42        DEPLOYMENT_HOST_ENV,
43        FLY_HOST_ENV,
44        "HOSTNAME",
45        "PATH",
46        "HOME",
47    ]
48    .iter()
49    .filter_map(|name| lookup(name).map(|value| ((*name).to_owned(), value)))
50    .collect();
51
52    if let Some(entry) = crate::net::trusted_hosts_env_entry(&lookup) {
53        env.push(entry);
54    }
55
56    env
57}
58
59#[must_use]
60pub const fn identity_verification_supported() -> bool {
61    cfg!(any(target_os = "linux", target_os = "macos"))
62}
63
64#[must_use]
65pub fn signalable_pid(pid: u32) -> Option<i32> {
66    if pid == 0 {
67        return None;
68    }
69    i32::try_from(pid).ok()
70}
71#[must_use]
72pub fn environ_identifies_child(environ: &[u8], name_key: &str, service_name: &str) -> bool {
73    let marker = format!("{SUBPROCESS_MARKER_ENV}=1");
74    let expected_name = format!("{name_key}={service_name}");
75
76    let mut has_marker = false;
77    let mut has_name = false;
78    for entry in environ.split(|&b| b == 0) {
79        if entry == marker.as_bytes() {
80            has_marker = true;
81        } else if entry == expected_name.as_bytes() {
82            has_name = true;
83        }
84    }
85
86    has_marker && has_name
87}
88
89// Why: macOS `KERN_PROCARGS2` stores argc, exec path, NUL padding, argv, then
90// environ. Skip argv by argc: argument strings can themselves look like
91// environment entries.
92#[must_use]
93pub fn environ_from_procargs2(blob: &[u8]) -> Option<&[u8]> {
94    const ARGC_LEN: usize = size_of::<i32>();
95
96    let argc_bytes: [u8; ARGC_LEN] = blob.get(..ARGC_LEN)?.try_into().ok()?;
97    let argc = usize::try_from(i32::from_ne_bytes(argc_bytes)).ok()?;
98
99    let mut rest = blob.get(ARGC_LEN..)?;
100    let exec_path_end = rest.iter().position(|&b| b == 0)?;
101    rest = rest.get(exec_path_end + 1..)?;
102
103    let argv_start = rest.iter().position(|&b| b != 0)?;
104    rest = rest.get(argv_start..)?;
105
106    for _ in 0..argc {
107        let entry_end = rest.iter().position(|&b| b == 0)?;
108        rest = rest.get(entry_end + 1..)?;
109    }
110
111    Some(rest)
112}