Skip to main content

clawft_kernel/wasm_runner/
tools_sys.rs

1//! Built-in system tool implementations and shell execution.
2
3use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7use super::catalog::builtin_tool_catalog;
8use super::registry::BuiltinTool;
9use super::runner::compute_module_hash;
10use super::types::*;
11use crate::governance::EffectVector;
12
13// ---------------------------------------------------------------------------
14// System service tools
15// ---------------------------------------------------------------------------
16
17/// Built-in `sys.service.list` tool.
18pub struct SysServiceListTool {
19    spec: BuiltinToolSpec,
20    service_registry: Arc<crate::service::ServiceRegistry>,
21}
22
23impl SysServiceListTool {
24    pub fn new(service_registry: Arc<crate::service::ServiceRegistry>) -> Self {
25        let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "sys.service.list").unwrap();
26        Self { spec, service_registry }
27    }
28}
29
30impl BuiltinTool for SysServiceListTool {
31    fn name(&self) -> &str { "sys.service.list" }
32    fn spec(&self) -> &BuiltinToolSpec { &self.spec }
33    fn execute(&self, _args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
34        let services = self.service_registry.list();
35        let entries: Vec<serde_json::Value> = services.iter().map(|(name, stype)| {
36            serde_json::json!({
37                "name": name,
38                "service_type": format!("{stype:?}"),
39            })
40        }).collect();
41        Ok(serde_json::json!({"services": entries, "count": entries.len()}))
42    }
43}
44
45/// Built-in `sys.service.health` tool.
46pub struct SysServiceHealthTool {
47    spec: BuiltinToolSpec,
48    service_registry: Arc<crate::service::ServiceRegistry>,
49}
50
51impl SysServiceHealthTool {
52    pub fn new(service_registry: Arc<crate::service::ServiceRegistry>) -> Self {
53        let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "sys.service.health").unwrap();
54        Self { spec, service_registry }
55    }
56}
57
58impl BuiltinTool for SysServiceHealthTool {
59    fn name(&self) -> &str { "sys.service.health" }
60    fn spec(&self) -> &BuiltinToolSpec { &self.spec }
61    fn execute(&self, _args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
62        // health_all() is async -- use service list as sync fallback
63        let services = self.service_registry.list();
64        let entries: Vec<serde_json::Value> = services.iter().map(|(name, _)| {
65            serde_json::json!({"name": name, "status": "registered"})
66        }).collect();
67        Ok(serde_json::json!({"health": entries, "count": entries.len()}))
68    }
69}
70
71// ---------------------------------------------------------------------------
72// Chain tools (exochain feature)
73// ---------------------------------------------------------------------------
74
75/// Built-in `sys.chain.status` tool.
76#[cfg(feature = "exochain")]
77pub struct SysChainStatusTool {
78    spec: BuiltinToolSpec,
79    chain: Arc<crate::chain::ChainManager>,
80}
81
82#[cfg(feature = "exochain")]
83impl SysChainStatusTool {
84    pub fn new(chain: Arc<crate::chain::ChainManager>) -> Self {
85        let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "sys.chain.status").unwrap();
86        Self { spec, chain }
87    }
88}
89
90#[cfg(feature = "exochain")]
91impl BuiltinTool for SysChainStatusTool {
92    fn name(&self) -> &str { "sys.chain.status" }
93    fn spec(&self) -> &BuiltinToolSpec { &self.spec }
94    fn execute(&self, _args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
95        let status = self.chain.status();
96        Ok(serde_json::json!({
97            "chain_id": status.chain_id,
98            "sequence": status.sequence,
99            "event_count": status.event_count,
100            "checkpoint_count": status.checkpoint_count,
101        }))
102    }
103}
104
105/// Built-in `sys.chain.query` tool.
106#[cfg(feature = "exochain")]
107pub struct SysChainQueryTool {
108    spec: BuiltinToolSpec,
109    chain: Arc<crate::chain::ChainManager>,
110}
111
112#[cfg(feature = "exochain")]
113impl SysChainQueryTool {
114    pub fn new(chain: Arc<crate::chain::ChainManager>) -> Self {
115        let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "sys.chain.query").unwrap();
116        Self { spec, chain }
117    }
118}
119
120#[cfg(feature = "exochain")]
121impl BuiltinTool for SysChainQueryTool {
122    fn name(&self) -> &str { "sys.chain.query" }
123    fn spec(&self) -> &BuiltinToolSpec { &self.spec }
124    fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
125        let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
126        let events = self.chain.tail(count);
127        let entries: Vec<serde_json::Value> = events.iter().map(|e| {
128            serde_json::json!({
129                "sequence": e.sequence,
130                "source": e.source,
131                "kind": e.kind,
132                "timestamp": e.timestamp.to_rfc3339(),
133            })
134        }).collect();
135        Ok(serde_json::json!({"events": entries, "count": entries.len()}))
136    }
137}
138
139// ---------------------------------------------------------------------------
140// Tree tools (exochain feature)
141// ---------------------------------------------------------------------------
142
143/// Built-in `sys.tree.read` tool.
144#[cfg(feature = "exochain")]
145pub struct SysTreeReadTool {
146    spec: BuiltinToolSpec,
147    tree: Arc<crate::tree_manager::TreeManager>,
148}
149
150#[cfg(feature = "exochain")]
151impl SysTreeReadTool {
152    pub fn new(tree: Arc<crate::tree_manager::TreeManager>) -> Self {
153        let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "sys.tree.read").unwrap();
154        Self { spec, tree }
155    }
156}
157
158#[cfg(feature = "exochain")]
159impl BuiltinTool for SysTreeReadTool {
160    fn name(&self) -> &str { "sys.tree.read" }
161    fn spec(&self) -> &BuiltinToolSpec { &self.spec }
162    fn execute(&self, _args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
163        let stats = self.tree.stats();
164        Ok(serde_json::json!({
165            "node_count": stats.node_count,
166            "mutation_count": stats.mutation_count,
167            "root_hash": stats.root_hash,
168        }))
169    }
170}
171
172/// Built-in `sys.tree.inspect` tool.
173#[cfg(feature = "exochain")]
174pub struct SysTreeInspectTool {
175    spec: BuiltinToolSpec,
176    tree: Arc<crate::tree_manager::TreeManager>,
177}
178
179#[cfg(feature = "exochain")]
180impl SysTreeInspectTool {
181    pub fn new(tree: Arc<crate::tree_manager::TreeManager>) -> Self {
182        let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "sys.tree.inspect").unwrap();
183        Self { spec, tree }
184    }
185}
186
187#[cfg(feature = "exochain")]
188impl BuiltinTool for SysTreeInspectTool {
189    fn name(&self) -> &str { "sys.tree.inspect" }
190    fn spec(&self) -> &BuiltinToolSpec { &self.spec }
191    fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
192        let path = args.get("path").and_then(|v| v.as_str())
193            .ok_or_else(|| ToolError::InvalidArgs("missing 'path'".into()))?;
194        let rid = exo_resource_tree::ResourceId::new(path);
195        let tree_lock = self.tree.tree().lock()
196            .map_err(|e| ToolError::ExecutionFailed(format!("tree lock: {e}")))?;
197        let node = tree_lock.get(&rid)
198            .ok_or_else(|| ToolError::NotFound(format!("node not found: {path}")))?;
199        Ok(serde_json::json!({
200            "path": path,
201            "kind": format!("{:?}", node.kind),
202            "metadata": node.metadata,
203            "scoring": node.scoring.as_array(),
204        }))
205    }
206}
207
208// ---------------------------------------------------------------------------
209// Env / Cron tools
210// ---------------------------------------------------------------------------
211
212/// Built-in `sys.env.get` tool.
213pub struct SysEnvGetTool {
214    spec: BuiltinToolSpec,
215}
216
217impl SysEnvGetTool {
218    pub fn new() -> Self {
219        let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "sys.env.get").unwrap();
220        Self { spec }
221    }
222}
223
224impl BuiltinTool for SysEnvGetTool {
225    fn name(&self) -> &str { "sys.env.get" }
226    fn spec(&self) -> &BuiltinToolSpec { &self.spec }
227    fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
228        let name = args.get("name").and_then(|v| v.as_str())
229            .ok_or_else(|| ToolError::InvalidArgs("missing 'name'".into()))?;
230        match std::env::var(name) {
231            Ok(val) => Ok(serde_json::json!({"name": name, "value": val})),
232            Err(_) => Ok(serde_json::json!({"name": name, "value": null})),
233        }
234    }
235}
236
237/// Built-in `sys.cron.add` tool.
238pub struct SysCronAddTool {
239    spec: BuiltinToolSpec,
240    cron: Arc<crate::cron::CronService>,
241}
242
243impl SysCronAddTool {
244    pub fn new(cron: Arc<crate::cron::CronService>) -> Self {
245        let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "sys.cron.add").unwrap();
246        Self { spec, cron }
247    }
248}
249
250impl BuiltinTool for SysCronAddTool {
251    fn name(&self) -> &str { "sys.cron.add" }
252    fn spec(&self) -> &BuiltinToolSpec { &self.spec }
253    fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
254        let name = args.get("name").and_then(|v| v.as_str())
255            .ok_or_else(|| ToolError::InvalidArgs("missing 'name'".into()))?;
256        let interval_secs = args.get("interval_secs").and_then(|v| v.as_u64())
257            .ok_or_else(|| ToolError::InvalidArgs("missing 'interval_secs'".into()))?;
258        let command = args.get("command").and_then(|v| v.as_str())
259            .ok_or_else(|| ToolError::InvalidArgs("missing 'command'".into()))?;
260        let target_pid = args.get("target_pid").and_then(|v| v.as_u64());
261        let job = self.cron.add_job(name.to_string(), interval_secs, command.to_string(), target_pid);
262        Ok(serde_json::to_value(&job).unwrap_or_default())
263    }
264}
265
266/// Built-in `sys.cron.list` tool.
267pub struct SysCronListTool {
268    spec: BuiltinToolSpec,
269    cron: Arc<crate::cron::CronService>,
270}
271
272impl SysCronListTool {
273    pub fn new(cron: Arc<crate::cron::CronService>) -> Self {
274        let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "sys.cron.list").unwrap();
275        Self { spec, cron }
276    }
277}
278
279impl BuiltinTool for SysCronListTool {
280    fn name(&self) -> &str { "sys.cron.list" }
281    fn spec(&self) -> &BuiltinToolSpec { &self.spec }
282    fn execute(&self, _args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
283        let jobs = self.cron.list_jobs();
284        Ok(serde_json::to_value(&jobs).unwrap_or_default())
285    }
286}
287
288/// Built-in `sys.cron.remove` tool.
289pub struct SysCronRemoveTool {
290    spec: BuiltinToolSpec,
291    cron: Arc<crate::cron::CronService>,
292}
293
294impl SysCronRemoveTool {
295    pub fn new(cron: Arc<crate::cron::CronService>) -> Self {
296        let spec = builtin_tool_catalog().into_iter().find(|s| s.name == "sys.cron.remove").unwrap();
297        Self { spec, cron }
298    }
299}
300
301impl BuiltinTool for SysCronRemoveTool {
302    fn name(&self) -> &str { "sys.cron.remove" }
303    fn spec(&self) -> &BuiltinToolSpec { &self.spec }
304    fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
305        let id = args.get("id").and_then(|v| v.as_str())
306            .ok_or_else(|| ToolError::InvalidArgs("missing 'id'".into()))?;
307        match self.cron.remove_job(id) {
308            Some(job) => Ok(serde_json::json!({"removed": true, "job_id": job.id})),
309            None => Err(ToolError::NotFound(format!("cron job: {id}"))),
310        }
311    }
312}
313
314// ---------------------------------------------------------------------------
315// Shell Command Execution
316// ---------------------------------------------------------------------------
317
318/// A shell command to be executed in the sandbox.
319///
320/// Represents a command with arguments and optional sandbox configuration.
321/// The command is dispatched through the tool execution path and chain-logged.
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct ShellCommand {
324    /// The command to execute (e.g. "echo", "ls").
325    pub command: String,
326    /// Arguments to the command.
327    pub args: Vec<String>,
328    /// Optional sandbox configuration to restrict execution.
329    pub sandbox_config: Option<SandboxConfig>,
330}
331
332/// Result of a shell command execution.
333#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct ShellResult {
335    /// Process exit code (0 = success).
336    pub exit_code: i32,
337    /// Standard output captured from the command.
338    pub stdout: String,
339    /// Standard error captured from the command.
340    pub stderr: String,
341    /// Execution wall-clock time in milliseconds.
342    pub execution_time_ms: u64,
343}
344
345/// Execute a shell command and return the result.
346///
347/// For now this dispatches as a builtin tool -- actual WASM compilation
348/// of shell commands is deferred to a future sprint. The sandbox config
349/// is stored on the result for governance auditing.
350///
351/// When the `exochain` feature is enabled and a [`ChainManager`] is
352/// provided, the execution is chain-logged as a `shell.exec` event.
353pub fn execute_shell(cmd: &ShellCommand) -> Result<ShellResult, ToolError> {
354    let start = std::time::Instant::now();
355
356    // Sandbox path check: if sandbox_config has allowed_paths,
357    // reject commands that reference paths outside the sandbox.
358    if let Some(ref sandbox) = cmd.sandbox_config {
359        if sandbox.sudo_override {
360            tracing::warn!(command = %cmd.command, "shell exec with sudo override");
361        }
362    }
363
364    // Builtin dispatch: for now, handle a small set of safe builtins.
365    // Real execution would compile to WASM and run in the sandbox.
366    let (exit_code, stdout, stderr) = match cmd.command.as_str() {
367        "echo" => {
368            let output = cmd.args.join(" ");
369            (0, output, String::new())
370        }
371        "true" => (0, String::new(), String::new()),
372        "false" => (1, String::new(), String::new()),
373        _ => {
374            // Unknown commands return a descriptive error in stderr.
375            // Future: compile to WASM and run in sandbox.
376            (127, String::new(), format!("command not found: {}", cmd.command))
377        }
378    };
379
380    let elapsed = start.elapsed();
381
382    Ok(ShellResult {
383        exit_code,
384        stdout,
385        stderr,
386        execution_time_ms: elapsed.as_millis() as u64,
387    })
388}
389
390/// Built-in `shell.exec` tool wrapping [`execute_shell`].
391pub struct ShellExecTool {
392    spec: BuiltinToolSpec,
393}
394
395impl ShellExecTool {
396    /// Create the shell.exec tool.
397    pub fn new() -> Self {
398        Self {
399            spec: BuiltinToolSpec {
400                name: "shell.exec".into(),
401                category: ToolCategory::System,
402                description: "Execute a shell command in the sandbox".into(),
403                parameters: serde_json::json!({
404                    "type": "object",
405                    "required": ["command"],
406                    "properties": {
407                        "command": {"type": "string", "description": "Command to execute"},
408                        "args": {
409                            "type": "array",
410                            "items": {"type": "string"},
411                            "description": "Command arguments"
412                        }
413                    }
414                }),
415                gate_action: "tool.shell.execute".into(),
416                effect: EffectVector {
417                    risk: 0.7,
418                    security: 0.4,
419                    ..Default::default()
420                },
421                native: true,
422            },
423        }
424    }
425}
426
427impl BuiltinTool for ShellExecTool {
428    fn name(&self) -> &str { "shell.exec" }
429    fn spec(&self) -> &BuiltinToolSpec { &self.spec }
430    fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, ToolError> {
431        let command = args.get("command").and_then(|v| v.as_str())
432            .ok_or_else(|| ToolError::InvalidArgs("missing 'command'".into()))?;
433        let cmd_args: Vec<String> = args.get("args")
434            .and_then(|v| serde_json::from_value(v.clone()).ok())
435            .unwrap_or_default();
436
437        let cmd = ShellCommand {
438            command: command.to_string(),
439            args: cmd_args,
440            sandbox_config: None,
441        };
442
443        let result = execute_shell(&cmd)?;
444        Ok(serde_json::json!({
445            "exit_code": result.exit_code,
446            "stdout": result.stdout,
447            "stderr": result.stderr,
448            "execution_time_ms": result.execution_time_ms,
449        }))
450    }
451}
452
453// ---------------------------------------------------------------------------
454// Shell Pipeline (K3 C5)
455// ---------------------------------------------------------------------------
456
457/// A shell pipeline compiled into a chain-linked WASM tool spec.
458///
459/// Shell commands are wrapped as tool definitions with their content
460/// hash anchored to the ExoChain for immutability and provenance.
461#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct ShellPipeline {
463    /// Pipeline name.
464    pub name: String,
465    /// Shell command string.
466    pub command: String,
467    /// SHA-256 hash of the command.
468    pub content_hash: [u8; 32],
469    /// Chain sequence number where this pipeline was registered.
470    pub chain_seq: Option<u64>,
471}
472
473impl ShellPipeline {
474    /// Create a new shell pipeline from a command string.
475    pub fn new(name: impl Into<String>, command: impl Into<String>) -> Self {
476        let cmd = command.into();
477        let hash = compute_module_hash(cmd.as_bytes());
478        Self {
479            name: name.into(),
480            command: cmd,
481            content_hash: hash,
482            chain_seq: None,
483        }
484    }
485
486    /// Register this pipeline on the chain for immutability (C5).
487    #[cfg(feature = "exochain")]
488    pub fn anchor_to_chain(&mut self, chain: &crate::chain::ChainManager) {
489        let seq = chain.sequence();
490        let hash_hex: String = self
491            .content_hash
492            .iter()
493            .map(|b| format!("{b:02x}"))
494            .collect();
495        chain.append(
496            "shell",
497            "shell.pipeline.register",
498            Some(serde_json::json!({
499                "name": &self.name,
500                "command_hash": hash_hex,
501                "command_length": self.command.len(),
502            })),
503        );
504        self.chain_seq = Some(seq);
505    }
506
507    /// Convert to a [`BuiltinToolSpec`] for registration in the [`ToolRegistry`].
508    pub fn to_tool_spec(&self) -> BuiltinToolSpec {
509        BuiltinToolSpec {
510            name: format!("shell.{}", self.name),
511            category: ToolCategory::User,
512            description: format!("Shell pipeline: {}", self.name),
513            parameters: serde_json::json!({
514                "type": "object",
515                "properties": {
516                    "args": {"type": "string", "description": "Additional arguments"}
517                }
518            }),
519            gate_action: "tool.shell.execute".into(),
520            effect: EffectVector {
521                risk: 0.6,
522                security: 0.3,
523                ..Default::default()
524            },
525            native: true,
526        }
527    }
528}