synwire_sandbox/plugin/process_plugin.rs
1//! `ProcessPlugin` — exposes process management and command execution as LLM tools.
2//!
3//! Implements the synwire-core [`Plugin`] trait. Contributes two sets of tools:
4//!
5//! **Management tools** (always available):
6//! `list_processes`, `kill_process`, `process_stats`, `wait_for_process`,
7//! `read_process_output`.
8//!
9//! **Command tools** (when [`SandboxContext`] is provided):
10//! `run_command`, `open_shell`, `shell_write`, `shell_read`.
11//!
12//! # Parent-child visibility
13//!
14//! Use [`ProcessVisibilityScope::add_child_registry`] to grant a parent agent
15//! read access to a sub-agent's processes. Read tools (`list`, `stats`,
16//! `wait`, `read_output`) see all visible registries; write tools (`kill`)
17//! are restricted to the agent's own registry.
18
19use std::sync::Arc;
20
21use serde::{Deserialize, Serialize};
22use tokio::sync::RwLock;
23
24use synwire_core::agents::plugin::{Plugin, PluginStateKey};
25use synwire_core::tools::Tool;
26
27use crate::plugin::command_tools::{
28 OpenShellTool, RunCommandTool, ShellBatchTool, ShellExpectCasesTool, ShellExpectTool,
29 ShellReadTool, ShellSignalTool, ShellWriteTool,
30};
31use crate::plugin::context::SandboxContext;
32use crate::plugin::tools::{
33 KillProcessTool, ListProcessesTool, ProcessStatsTool, ReadProcessOutputTool, WaitForProcessTool,
34};
35use crate::process_registry::ProcessRegistry;
36use crate::visibility::ProcessVisibilityScope;
37
38// ── Plugin state key ──────────────────────────────────────────────────────────
39
40/// Shared state owned by `ProcessPlugin`.
41#[derive(Debug)]
42pub struct ProcessPluginState {
43 /// Thread-safe process registry.
44 pub registry: Arc<RwLock<ProcessRegistry>>,
45}
46
47// Minimal serialization for `PluginStateMap::serialize_all` (returns registry size).
48impl Serialize for ProcessPluginState {
49 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
50 use serde::ser::SerializeMap;
51 let mut map = serializer.serialize_map(Some(1))?;
52 map.serialize_entry("active", &true)?;
53 map.end()
54 }
55}
56
57impl<'de> Deserialize<'de> for ProcessPluginState {
58 fn deserialize<D: serde::Deserializer<'de>>(_deserializer: D) -> Result<Self, D::Error> {
59 // Deserialization creates an empty registry — full state cannot be
60 // reconstructed from JSON (PIDs are ephemeral).
61 Ok(Self {
62 registry: Arc::new(RwLock::new(ProcessRegistry::new(None))),
63 })
64 }
65}
66
67/// [`PluginStateKey`] for `ProcessPlugin`.
68pub struct ProcessPluginKey;
69
70impl PluginStateKey for ProcessPluginKey {
71 type State = ProcessPluginState;
72 const KEY: &'static str = "synwire.process";
73}
74
75// ── ProcessPlugin ─────────────────────────────────────────────────────────────
76
77/// Plugin that tracks spawned processes and provides LLM tool access.
78///
79/// # Management-only (no command execution)
80///
81/// ```rust,ignore
82/// let plugin = ProcessPlugin::with_scope(scope);
83/// // Provides: list_processes, kill_process, process_stats,
84/// // wait_for_process, read_process_output
85/// ```
86///
87/// # Full command execution
88///
89/// ```rust,ignore
90/// let ctx = Arc::new(SandboxContext::new(config, registry, scope, container));
91/// let plugin = ProcessPlugin::with_context(ctx);
92/// // Provides all management tools PLUS:
93/// // run_command, open_shell, shell_write, shell_read
94/// ```
95pub struct ProcessPlugin {
96 tools: Vec<Arc<dyn Tool>>,
97}
98
99impl ProcessPlugin {
100 /// Create a plugin with management tools only.
101 ///
102 /// Convenience constructor that wraps the registry in a
103 /// [`ProcessVisibilityScope`] with no child registries.
104 #[must_use]
105 pub fn new(registry: Arc<RwLock<ProcessRegistry>>) -> Self {
106 Self::with_scope(ProcessVisibilityScope::new(registry))
107 }
108
109 /// Create a plugin with management tools backed by a visibility scope.
110 ///
111 /// Use this when the agent has sub-agents whose process registries should
112 /// be visible to read-only tools (`list`, `stats`, `wait`, `read_output`).
113 #[must_use]
114 pub fn with_scope(scope: ProcessVisibilityScope) -> Self {
115 let tools: Vec<Arc<dyn Tool>> = vec![
116 Arc::new(ListProcessesTool::new(scope.clone())),
117 Arc::new(KillProcessTool::new(scope.clone())),
118 Arc::new(ProcessStatsTool::new(scope.clone())),
119 Arc::new(WaitForProcessTool::new(scope.clone())),
120 Arc::new(ReadProcessOutputTool::new(scope)),
121 ];
122 Self { tools }
123 }
124
125 /// Create a plugin with **all** tools: management + command execution.
126 ///
127 /// The [`SandboxContext`] provides the OCI runtime, sandbox config, and
128 /// process registry needed by `run_command`, `open_shell`, `shell_write`,
129 /// and `shell_read`.
130 #[must_use]
131 pub fn with_context(ctx: Arc<SandboxContext>) -> Self {
132 let scope = ctx.scope.clone();
133 let mut tools: Vec<Arc<dyn Tool>> = vec![
134 // Management tools
135 Arc::new(ListProcessesTool::new(scope.clone())),
136 Arc::new(KillProcessTool::new(scope.clone())),
137 Arc::new(ProcessStatsTool::new(scope.clone())),
138 Arc::new(WaitForProcessTool::new(scope.clone())),
139 Arc::new(ReadProcessOutputTool::new(scope)),
140 // Command execution tools
141 Arc::new(RunCommandTool::new(Arc::clone(&ctx))),
142 Arc::new(OpenShellTool::new(Arc::clone(&ctx))),
143 Arc::new(ShellWriteTool::new(Arc::clone(&ctx))),
144 Arc::new(ShellReadTool::new(Arc::clone(&ctx))),
145 Arc::new(ShellExpectTool::new(Arc::clone(&ctx))),
146 Arc::new(ShellExpectCasesTool::new(Arc::clone(&ctx))),
147 Arc::new(ShellBatchTool::new(Arc::clone(&ctx))),
148 Arc::new(ShellSignalTool::new(ctx)),
149 ];
150 // Sort for deterministic tool ordering in schema.
151 tools.sort_by(|a, b| a.name().cmp(b.name()));
152 Self { tools }
153 }
154}
155
156impl Plugin for ProcessPlugin {
157 fn name(&self) -> &'static str {
158 "synwire-process"
159 }
160
161 fn tools(&self) -> Vec<Arc<dyn Tool>> {
162 self.tools.clone()
163 }
164}