Skip to main content

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}