agentmux_srv\backend\process_tracker/
mod.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Agent-spawned process tracking.
5//!
6//! Gives the host a complete, authoritative view of what each agent CLI
7//! has forked — backgrounded shells, dev servers, Docker containers,
8//! file watchers, nested bash/python/node children, etc. The goal is
9//! end-user visibility: a user running multiple agents can see in one
10//! place what's still running on their machine, and kill it reliably
11//! when they're done.
12//!
13//! The API is a platform-agnostic trait. Per-platform impls use the
14//! strongest available mechanism:
15//!
16//! | Platform | Impl            | Mechanism                                  | Confidence |
17//! |----------|-----------------|--------------------------------------------|------------|
18//! | Windows  | `JobObjectTracker` | `CreateJobObject` + `AssignProcessToJobObject` + `TerminateJobObject` | high       |
19//! | Linux    | `Cgroupv2Tracker`  | `systemd-run --user --scope` + `cgroup.procs` / `cgroup.kill`      | high       |
20//! | macOS    | `ProcessGroupTracker` | `POSIX_SPAWN_SETPGROUP` + `killpg`                               | best-effort |
21//! | other    | `StubTracker`   | no-op                                                          | none       |
22//!
23//! The frontend's swarm panel surfaces the confidence level so users know
24//! when tracking may miss escaped descendants.
25//!
26//! See `agentmux-ai/AGENT_SPAWNED_PROCESSES_SPEC.md` for the design.
27
28use std::sync::Arc;
29
30pub mod registry;
31
32#[cfg(windows)]
33pub mod windows;
34
35// `pub mod stub;` (file-form) was here. Removed: `stub.rs` doesn't exist
36// in the tree — only the two inline `pub mod stub { ... }` definitions
37// below (cfg(not(windows)) and cfg(windows)) define the module. On Linux
38// the file-form line collided with the inline non-Windows definition →
39// E0428 "the name `stub` is defined multiple times" → broke `task dev`.
40
41/// A single process tracked by the host — PID + metadata enriched
42/// per-platform. The frontend renders one row per entry.
43#[derive(Debug, Clone, serde::Serialize)]
44pub struct TrackedProcess {
45    pub pid: u32,
46    /// Full command line, or best approximation. May be empty if the
47    /// platform doesn't expose it cheaply (macOS without `libproc`).
48    pub command: String,
49    /// Working-set / RSS in bytes. 0 if unavailable.
50    pub rss_bytes: u64,
51    /// Unix ms of process creation, 0 if unavailable.
52    pub started_at_ms: u64,
53}
54
55/// Opaque per-agent handle returned by the tracker when we wrap a spawn.
56/// Held inside `AgentProcessRegistry` for the lifetime of the pane;
57/// dropped when the pane closes or the agent exits.
58#[allow(dead_code)]
59pub trait TrackerHandle: Send + Sync {
60    /// Add a freshly-spawned process to the tracked tree. Called by the
61    /// controller immediately after `tokio::process::Command::spawn`.
62    /// Descendants created AFTER this call are caught automatically;
63    /// descendants created BEFORE (in the ~1ms race window) escape.
64    /// No-op in the stub impl — platforms without a real tracker
65    /// silently accept the PID and move on.
66    fn assign_process(&self, pid: u32) -> Result<(), String>;
67
68    /// Enumerate the current members of this tracked tree.
69    ///
70    /// Must be cheap enough to poll every ~2s. On Windows this is a
71    /// single Job Object query; on Linux it's a read of `cgroup.procs`;
72    /// on macOS it's a sysctl scan.
73    fn list_members(&self) -> Vec<TrackedProcess>;
74
75    /// Forcibly terminate every process in this tracked tree.
76    fn kill_tree(&self);
77
78    /// Terminate a single process by PID, if it's a member of this tree.
79    /// Returns `true` if the PID was known and the kill was attempted.
80    fn kill_pid(&self, pid: u32) -> bool;
81
82    /// Describes how confidently this platform tracks descendants.
83    /// Surfaced to the UI so the user can tell when tracking is
84    /// best-effort and escape-prone.
85    fn confidence(&self) -> TrackingConfidence;
86}
87
88/// How reliable this platform's tracker is.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
90#[serde(rename_all = "snake_case")]
91pub enum TrackingConfidence {
92    /// Descendants can't escape the tracker. Windows Job Objects +
93    /// Linux cgroups v2.
94    High,
95    /// Descendants can escape via `setsid`, launchd, etc. macOS.
96    BestEffort,
97    /// Platform has no tracker. No guarantees.
98    None,
99}
100
101/// Factory: returns a platform-appropriate tracker handle that will
102/// accept the next-spawned process and everything it forks.
103///
104/// Call once per agent pane; reuse the handle across multiple turns of
105/// the `SubprocessController` so children from any turn are all tracked
106/// under the same umbrella.
107pub fn new_tracker(block_id: &str) -> Arc<dyn TrackerHandle> {
108    #[cfg(windows)]
109    {
110        match windows::JobObjectTracker::new(block_id) {
111            Ok(t) => Arc::new(t),
112            Err(e) => {
113                tracing::warn!(
114                    block_id = %block_id,
115                    error = %e,
116                    "[process-tracker] JobObjectTracker init failed — falling back to stub"
117                );
118                Arc::new(stub::StubTracker)
119            }
120        }
121    }
122    #[cfg(not(windows))]
123    {
124        let _ = block_id;
125        Arc::new(stub::StubTracker)
126    }
127}
128
129#[cfg(not(windows))]
130pub mod stub {
131    //! No-op tracker used on unsupported platforms or when init fails.
132    //! All operations succeed silently; `list_members` always returns
133    //! empty. Confidence reports `None` so the UI can inform the user
134    //! that tracking is disabled.
135
136    use super::{TrackedProcess, TrackerHandle, TrackingConfidence};
137
138    pub struct StubTracker;
139
140    impl TrackerHandle for StubTracker {
141        fn assign_process(&self, _pid: u32) -> Result<(), String> {
142            Ok(())
143        }
144        fn list_members(&self) -> Vec<TrackedProcess> {
145            Vec::new()
146        }
147        fn kill_tree(&self) {}
148        fn kill_pid(&self, _pid: u32) -> bool {
149            false
150        }
151        fn confidence(&self) -> TrackingConfidence {
152            TrackingConfidence::None
153        }
154    }
155}
156
157#[cfg(windows)]
158pub mod stub {
159    //! Windows fallback if `JobObjectTracker::new` fails (e.g. the
160    //! process is not elevated enough to create a job object). The real
161    //! impl lives in `windows`; this is only used for the init-fail
162    //! recovery path.
163
164    use super::{TrackedProcess, TrackerHandle, TrackingConfidence};
165
166    pub struct StubTracker;
167
168    impl TrackerHandle for StubTracker {
169        fn assign_process(&self, _pid: u32) -> Result<(), String> {
170            Ok(())
171        }
172        fn list_members(&self) -> Vec<TrackedProcess> {
173            Vec::new()
174        }
175        fn kill_tree(&self) {}
176        fn kill_pid(&self, _pid: u32) -> bool {
177            false
178        }
179        fn confidence(&self) -> TrackingConfidence {
180            TrackingConfidence::None
181        }
182    }
183}