Skip to main content

agentd/supervisor/
spawn.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Spawning a subagent process. RFC 0009 §re-exec, RFC 0003 §process-group.
3//!
4//! A subagent is the **same binary re-exec'd** with `AGENT_SUBAGENT` set, so
5//! the one artifact is CLI, supervisor, and subagent. Each child is put in its
6//! own **process group** (`setpgid` in `pre_exec`) so the kill ladder can
7//! `killpg` a whole subtree (RFC 0003). The supervisor delivers the
8//! [`SpawnPayload`] as the first control frame; the child's upward
9//! [`AgentMsg`]s are read on a dedicated thread and forwarded — tagged with
10//! the child's [`NodeId`] — onto the reactor's single **merged channel**
11//! (RFC 0002 §reactor).
12//!
13//! Teardown is **reap-safe**: once the reactor has reaped a child via
14//! `waitpid(-1)` it calls [`Subagent::mark_reaped`], so `Drop` will not signal
15//! a possibly-reused pid.
16
17use crate::json::frame;
18use crate::subagent::protocol::{AgentMsg, ControlMsg, SUBAGENT_ENV, SpawnPayload};
19use crate::supervisor::kill::kill_group;
20use crate::supervisor::tree::NodeId;
21use std::io;
22use std::path::Path;
23use std::process::{Child, ChildStdin, Command, Stdio};
24use std::sync::mpsc::Sender;
25use std::thread::JoinHandle;
26use std::time::Duration;
27
28/// A handle to a running subagent process and the down side of its control
29/// channel. Upward messages arrive on the reactor's merged channel, not here.
30pub struct Subagent {
31    pub node: NodeId,
32    child: Child,
33    writer: ChildStdin,
34    /// Process-group id for `killpg` (== child pid; the child is its own group
35    /// leader after `setpgid(0, 0)`).
36    pgid: i32,
37    /// Set once the reactor has reaped this child — suppresses Drop signalling.
38    reaped: bool,
39    _reader: JoinHandle<()>,
40    /// The child's own cgroup leaf (`security.cgroup`), held for its lifetime —
41    /// its Drop writes `cgroup.kill` + removes the leaf (the atomic teardown
42    /// backstop). `None` when cgroups are not configured. RFC 0009 §cgroup.
43    _cgroup: Option<crate::supervisor::cgroup::CgroupGuard>,
44}
45
46/// Spawn a subagent that re-execs `exe` (normally `std::env::current_exe()`),
47/// delivering `payload`. Upward messages are forwarded to `events` tagged with
48/// `node`.
49pub fn spawn(
50    exe: &Path,
51    payload: &SpawnPayload,
52    node: NodeId,
53    events: Sender<(NodeId, AgentMsg)>,
54) -> io::Result<Subagent> {
55    let mut cmd = Command::new(exe);
56    cmd.env(SUBAGENT_ENV, "1")
57        .stdin(Stdio::piped())
58        .stdout(Stdio::piped())
59        // Child telemetry (JSON to its stderr) is inherited into ours; the
60        // control channel is stdout (binary frames).
61        .stderr(Stdio::inherit());
62
63    #[cfg(unix)]
64    {
65        use std::os::unix::process::CommandExt;
66        // SAFETY: only async-signal-safe calls between fork and exec.
67        unsafe {
68            cmd.pre_exec(|| {
69                // Own process group → the kill ladder can target the subtree.
70                libc::setpgid(0, 0);
71                Ok(())
72            });
73        }
74    }
75
76    // Spawn, retrying a transient `EAGAIN` — the kernel refusing a `fork` under
77    // process/memory pressure (a wide fan-out starting many subagents at once, or
78    // a CPU-starved host). Bounded (~1s total, short backoff); a genuine error
79    // (ENOENT, EMFILE-persisted, …) still surfaces. Real robustness, not just a
80    // test artifact: a busy agent tree hits the same refusal. RFC 0003.
81    let mut child = {
82        let mut attempt = 0u32;
83        loop {
84            match cmd.spawn() {
85                Ok(c) => break c,
86                Err(e)
87                    if attempt < 10
88                        && (e.raw_os_error() == Some(libc::EAGAIN)
89                            || e.kind() == io::ErrorKind::WouldBlock) =>
90                {
91                    attempt += 1;
92                    std::thread::sleep(Duration::from_millis(u64::from(20 * attempt)));
93                }
94                Err(e) => return Err(e),
95            }
96        }
97    };
98    let pgid = child.id() as i32;
99    // Place the child in its own cgroup leaf (best-effort; `None` unless
100    // `security.cgroup` armed the parent). The guard is held on the Subagent so
101    // teardown (`cgroup.kill` + rmdir) fires when the child is reaped.
102    let cgroup = crate::supervisor::cgroup::CgroupGuard::for_run().inspect(|g| {
103        g.place(pgid);
104    });
105    let mut writer = child
106        .stdin
107        .take()
108        .ok_or_else(|| io::Error::other("no child stdin"))?;
109    let stdout = child
110        .stdout
111        .take()
112        .ok_or_else(|| io::Error::other("no child stdout"))?;
113
114    // Deliver the spawn payload as the first control frame.
115    frame::write_frame(&mut writer, &ControlMsg::Spawn(Box::new(payload.clone())))?;
116
117    let reader = std::thread::Builder::new()
118        .name(format!("subagent-events:{}", node.0))
119        .spawn(move || {
120            let mut r = io::BufReader::new(stdout);
121            // Exits on Ok(None) (clean EOF) or Err (child closed stdout/exited).
122            while let Ok(Some(bytes)) = frame::read_frame(&mut r) {
123                match serde_json::from_slice::<AgentMsg>(&bytes) {
124                    Ok(msg) => {
125                        if events.send((node, msg)).is_err() {
126                            break; // reactor dropped the channel
127                        }
128                    }
129                    Err(_) => { /* skip an unparseable frame */ }
130                }
131            }
132        })?;
133
134    Ok(Subagent {
135        node,
136        child,
137        writer,
138        pgid,
139        reaped: false,
140        _reader: reader,
141        _cgroup: cgroup,
142    })
143}
144
145impl Subagent {
146    pub fn pid(&self) -> i32 {
147        self.child.id() as i32
148    }
149    pub fn pgid(&self) -> i32 {
150        self.pgid
151    }
152
153    /// Send a control message down (Ping / Cancel / Inject).
154    pub fn send(&mut self, msg: &ControlMsg) -> io::Result<()> {
155        frame::write_frame(&mut self.writer, msg)
156    }
157
158    /// Mark that the reactor already reaped this child (via `waitpid(-1)`), so
159    /// teardown won't signal a possibly-reused pid.
160    pub fn mark_reaped(&mut self) {
161        self.reaped = true;
162    }
163
164    /// Immediate, unconditional teardown of the whole process group. The
165    /// graceful ladder (cancel → SIGTERM → SIGKILL over time) is driven by the
166    /// reactor via `kill.rs`; this is the backstop.
167    pub fn kill(&mut self) {
168        if !self.reaped {
169            crate::supervisor::reaper::deregister(self.pid());
170            kill_group(self.pgid);
171            let _ = self.child.kill();
172            let _ = self.child.wait();
173            self.reaped = true;
174        }
175    }
176}
177
178impl Drop for Subagent {
179    fn drop(&mut self) {
180        if !self.reaped {
181            // Drop a never-dispatched route (an abandoned run), then tear down +
182            // reap the child ourselves. `child.wait()` tolerates ECHILD if the
183            // global reaper collected it first; deregistering first means it sees
184            // this pid as foreign rather than routing a stale exit.
185            crate::supervisor::reaper::deregister(self.pid());
186            kill_group(self.pgid);
187            let _ = self.child.kill();
188            let _ = self.child.wait();
189        }
190    }
191}