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}