systemprompt_loader/subprocess/mod.rs
1//! Process supervision for the agent and MCP children this installation owns,
2//! and for its API server: the one place that spawns, identifies, probes and
3//! stops them.
4//!
5//! # Spawning
6//!
7//! [`spawn_supervised`] is the only sanctioned way to start a child, and
8//! [`mark_child`] stamps it with the environment markers that later prove it
9//! is ours. Every spawn runs on one dedicated thread, which on Linux also arms
10//! the parent-death signal.
11//!
12//! # Control
13//!
14//! [`is_running`], [`owns`], [`pids_listening_on`], [`terminate_gracefully`],
15//! [`terminate_group_gracefully`] and [`stop_owned`] are async and never block
16//! a runtime worker. A stop signals only a pid that is provably ours: the pid
17//! the registry recorded *and* a matching [`ChildKind`] marker read back from
18//! the live process. Outcomes are typed ([`Termination`], [`StopOutcome`]);
19//! failures are [`SupervisionError`].
20//!
21//! # Identity
22//!
23//! The environment markers and the pure parsers that read them back live in
24//! [`systemprompt_models::subprocess`]; the platform probes here
25//! ([`live_pid_is_subprocess`], [`is_zombie`]) execute them against `/proc`
26//! or `sysctl`. They are blocking primitives; async callers use [`owns`] and
27//! [`is_running`].
28//!
29//! # Platform support
30//!
31//! The two halves of supervision have different reach, and conflating them is
32//! what stranded ports on macOS:
33//!
34//! - **Identity and reap checks** ([`live_pid_is_subprocess`], [`is_zombie`])
35//! work on Linux, via `/proc`, and on macOS, via `sysctl(KERN_PROCARGS2)` and
36//! `proc_pidinfo`. Report the platform's coverage with
37//! [`identity_verification_supported`](systemprompt_models::subprocess::identity_verification_supported);
38//! where it is absent the checks are
39//! fail-closed stubs that never confirm an identity, so no process is ever
40//! signalled on a guess.
41//! - **Parent-death prevention** is `prctl(PR_SET_PDEATHSIG)` and therefore
42//! Linux-only. macOS has no equivalent that survives `execve`, and the kqueue
43//! and pipe-EOF alternatives all require cooperation from the child binary —
44//! which is an arbitrary MCP server or agent executable here. A `SIGKILL`ed
45//! supervisor on macOS therefore leaves its children reparented to `launchd`
46//! and still holding their ports; the identity check above is what lets the
47//! next start reclaim them instead of erroring out.
48//!
49//! Copyright (c) systemprompt.io — Business Source License 1.1.
50//! See <https://systemprompt.io> for licensing details.
51
52use systemprompt_identifiers::ServiceName;
53
54mod control;
55mod error;
56mod ports;
57mod spawn;
58
59#[cfg(unix)]
60mod posix;
61#[cfg(windows)]
62mod winnt;
63
64#[cfg(target_os = "linux")]
65mod linux;
66#[cfg(target_os = "linux")]
67use linux::live_environ;
68#[cfg(target_os = "linux")]
69pub use linux::{is_zombie, live_pid_is_subprocess};
70
71#[cfg(target_os = "macos")]
72mod darwin;
73#[cfg(target_os = "macos")]
74use darwin::live_environ;
75#[cfg(target_os = "macos")]
76pub use darwin::{is_zombie, live_pid_is_subprocess};
77
78#[cfg(not(any(target_os = "linux", target_os = "macos")))]
79mod unsupported;
80#[cfg(not(any(target_os = "linux", target_os = "macos")))]
81use unsupported::live_environ;
82#[cfg(not(any(target_os = "linux", target_os = "macos")))]
83pub use unsupported::{is_zombie, live_pid_is_subprocess};
84
85pub use control::{
86 StopOutcome, Termination, is_running, owns, pids_listening_on, process_group, stop_owned,
87 terminate_gracefully, terminate_group_gracefully,
88};
89pub use error::SupervisionError;
90pub use ports::{parse_lsof_pids, parse_netstat_listeners};
91pub use spawn::{
92 ApiServerStamp, mark_child, place_in_own_process_group, spawn_owned_supervised,
93 spawn_supervised, stamp_api_server,
94};
95
96/// Which kind of supervised process a pid is claimed to be; selects the
97/// marker variable that names it. `Api` is the API server, stamped by
98/// [`stamp_api_server`] rather than spawned.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100pub enum ChildKind {
101 Agent,
102 Mcp,
103 Api,
104}
105
106impl ChildKind {
107 #[must_use]
108 pub const fn marker_env(self) -> &'static str {
109 match self {
110 Self::Agent => systemprompt_models::subprocess::AGENT_NAME_ENV,
111 Self::Mcp => systemprompt_models::subprocess::MCP_SERVICE_ID_ENV,
112 Self::Api => systemprompt_models::subprocess::API_SERVER_ENV,
113 }
114 }
115
116 #[must_use]
117 pub fn identifies(self, environ: &[u8], service: &ServiceName) -> bool {
118 match self {
119 Self::Agent | Self::Mcp => systemprompt_models::subprocess::environ_identifies_child(
120 environ,
121 self.marker_env(),
122 service,
123 ),
124 Self::Api => {
125 systemprompt_models::subprocess::environ_identifies_api_server(environ, service)
126 },
127 }
128 }
129}
130
131#[must_use]
132pub fn api_server_service() -> ServiceName {
133 ServiceName::new(systemprompt_models::subprocess::API_SERVER_SERVICE)
134}