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