Skip to main content

recursive/
tool_set_provider.rs

1//! Tool set provider trait for pluggable, swappable tool implementations.
2//!
3//! Implementations:
4//! - [`LocalToolSetProvider`]: standard registry, no sandboxing
5//! - [`PolicyToolSetProvider`]: standard registry + L1 policy checks
6//!
7//! The [`ToolSetProvider`] trait decouples `AgentKernel` from the concrete
8//! tool set. The local default builds the standard registry with no sandboxing;
9//! cloud deployments can substitute a sandboxed registry without touching the
10//! kernel loop.
11
12use crate::tools::{policy_sandbox::PolicyConfig, ToolRegistry};
13
14/// Controls how aggressively the runtime restricts tool side-effects.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum SandboxMode {
17    /// No sandbox — tools execute directly in the agent process (local default).
18    #[default]
19    None,
20    /// Policy-based: filesystem path and network access is validated at the
21    /// Rust layer before the tool executes.
22    Policy,
23    /// Container-based: tools execute inside an isolated Docker/gVisor container.
24    Container,
25    /// MicroVM-based: tools execute inside a hardware-virtualised VM
26    /// (e.g. Firecracker or E2B).
27    MicroVm,
28}
29
30/// Provides the [`ToolRegistry`] for a given runtime mode.
31///
32/// The local implementation returns the standard registry with direct execution.
33/// Cloud implementations wrap tools with sandbox adapters, injecting
34/// container/microVM transport layers transparently.
35pub trait ToolSetProvider: Send + Sync + 'static {
36    /// Build and return the tool registry for this provider.
37    fn build_registry(&self) -> ToolRegistry;
38    /// Report the sandbox level this provider operates at.
39    fn sandbox_mode(&self) -> SandboxMode;
40}
41
42/// Standard local tool set with no sandboxing.
43///
44/// Calls [`crate::tools::build_standard_tools`] with default workspace
45/// and shell timeout settings. AgentKernelBuilder overrides these at build
46/// time if more context is available.
47pub struct LocalToolSetProvider {
48    workspace: std::path::PathBuf,
49    shell_timeout_secs: u64,
50    skills: Vec<crate::skills::Skill>,
51}
52
53impl LocalToolSetProvider {
54    /// Create a provider for `workspace` with the given shell timeout.
55    pub fn new(
56        workspace: std::path::PathBuf,
57        shell_timeout_secs: u64,
58        skills: Vec<crate::skills::Skill>,
59    ) -> Self {
60        Self {
61            workspace,
62            shell_timeout_secs,
63            skills,
64        }
65    }
66}
67
68impl ToolSetProvider for LocalToolSetProvider {
69    fn build_registry(&self) -> ToolRegistry {
70        crate::tools::build_standard_tools(&self.workspace, &self.skills, self.shell_timeout_secs)
71    }
72
73    fn sandbox_mode(&self) -> SandboxMode {
74        SandboxMode::None
75    }
76}
77
78/// [`ToolSetProvider`] that wraps the default registry with an L1 policy config.
79///
80/// The policy is stored in the registry for tools to query at call time via
81/// `registry.policy()`. It does **not** automatically enforce anything by itself;
82/// individual tools (e.g. `run_shell`) are responsible for calling
83/// `registry.policy().check_shell(...)` before executing.
84pub struct PolicyToolSetProvider {
85    workspace: std::path::PathBuf,
86    shell_timeout_secs: u64,
87    skills: Vec<crate::skills::Skill>,
88    /// The L1 policy to attach to the built registry.
89    pub policy: PolicyConfig,
90}
91
92impl PolicyToolSetProvider {
93    pub fn new(
94        workspace: std::path::PathBuf,
95        shell_timeout_secs: u64,
96        skills: Vec<crate::skills::Skill>,
97        policy: PolicyConfig,
98    ) -> Self {
99        Self {
100            workspace,
101            shell_timeout_secs,
102            skills,
103            policy,
104        }
105    }
106
107    /// Create with [`PolicyConfig::default_restrictive`] policy.
108    pub fn restrictive(
109        workspace: std::path::PathBuf,
110        shell_timeout_secs: u64,
111        skills: Vec<crate::skills::Skill>,
112    ) -> Self {
113        Self::new(
114            workspace,
115            shell_timeout_secs,
116            skills,
117            PolicyConfig::default_restrictive(),
118        )
119    }
120}
121
122impl ToolSetProvider for PolicyToolSetProvider {
123    fn build_registry(&self) -> ToolRegistry {
124        crate::tools::build_standard_tools(&self.workspace, &self.skills, self.shell_timeout_secs)
125            .with_policy(self.policy.clone())
126    }
127
128    fn sandbox_mode(&self) -> SandboxMode {
129        SandboxMode::Policy
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use tempfile::TempDir;
137
138    fn provider() -> (LocalToolSetProvider, TempDir) {
139        let dir = TempDir::new().unwrap();
140        let p = LocalToolSetProvider::new(dir.path().to_path_buf(), 30, vec![]);
141        (p, dir)
142    }
143
144    #[test]
145    fn local_provider_sandbox_mode_is_none() {
146        let (p, _dir) = provider();
147        assert_eq!(p.sandbox_mode(), SandboxMode::None);
148    }
149
150    #[test]
151    fn local_provider_registry_non_empty() {
152        let (p, _dir) = provider();
153        let reg = p.build_registry();
154        let names = reg.names();
155        assert!(
156            names.iter().any(|n| n == "run_shell"),
157            "expected run_shell in registry, got: {names:?}"
158        );
159    }
160
161    #[test]
162    fn policy_provider_sandbox_mode_is_policy() {
163        let dir = TempDir::new().unwrap();
164        let p = PolicyToolSetProvider::restrictive(dir.path().to_path_buf(), 30, vec![]);
165        assert_eq!(p.sandbox_mode(), SandboxMode::Policy);
166    }
167
168    #[test]
169    fn policy_provider_attaches_policy_to_registry() {
170        let dir = TempDir::new().unwrap();
171        let p = PolicyToolSetProvider::restrictive(dir.path().to_path_buf(), 30, vec![]);
172        let reg = p.build_registry();
173        // The policy is attached and check_shell works through it.
174        let policy = reg.policy().expect("policy should be attached");
175        assert!(policy.check_shell("ls").is_ok());
176        assert!(policy.check_shell("rm -rf /").is_err());
177    }
178}