Skip to main content

forge_sandbox/
lib.rs

1#![warn(missing_docs)]
2
3//! # forge-sandbox
4//!
5//! V8 sandbox for the Forgemax Code Mode Gateway.
6//!
7//! Executes LLM-generated JavaScript in a deno_core isolate with no filesystem,
8//! network, or environment access. The only bridge to the host is through
9//! explicitly registered ops that dispatch to a [`ToolDispatcher`].
10//!
11//! ## Security model
12//!
13//! - **V8 isolate**: Same process-level isolation as Chrome tabs
14//! - **No ambient capabilities**: No fs, net, env, or child_process access
15//! - **Fresh runtime per call**: No state leakage between executions
16//! - **Pre-execution validation**: Banned patterns caught before reaching V8
17//! - **Timeout enforcement**: Execution killed after configurable deadline
18//! - **Output size limits**: Prevents exfiltration of large data sets
19//! - **Opaque bindings**: Credentials never exposed to sandbox code
20
21#[cfg(feature = "ast-validator")]
22pub mod ast_validator;
23pub mod audit;
24pub mod error;
25pub mod executor;
26pub mod groups;
27pub mod host;
28pub mod ipc;
29#[cfg(feature = "metrics")]
30pub mod metrics;
31pub mod ops;
32pub mod pool;
33pub mod redact;
34pub mod stash;
35pub mod validator;
36
37pub use error::SandboxError;
38pub use executor::{ExecutionMode, SandboxConfig, SandboxExecutor};
39
40/// Trait for dispatching tool calls from the sandbox to downstream MCP servers.
41///
42/// Implementations hold credentials and manage connections to backend servers.
43/// The sandbox code never sees tokens, file paths, or internal state — it calls
44/// through opaque proxy objects that route here.
45#[async_trait::async_trait]
46pub trait ToolDispatcher: Send + Sync {
47    /// Call a tool on a downstream server.
48    ///
49    /// - `server`: The server name (e.g., "github", "narsil")
50    /// - `tool`: The tool identifier (e.g., "symbols.find", "issues.list")
51    /// - `args`: The tool arguments as a JSON value
52    async fn call_tool(
53        &self,
54        server: &str,
55        tool: &str,
56        args: serde_json::Value,
57    ) -> Result<serde_json::Value, forge_error::DispatchError>;
58}
59
60/// Trait for dispatching resource reads from the sandbox to downstream MCP servers.
61///
62/// Resources are data objects (logs, files, database rows) exposed via MCP's
63/// resources/read protocol. Unlike tool calls, resources are read-only.
64#[async_trait::async_trait]
65pub trait ResourceDispatcher: Send + Sync {
66    /// Read a resource by URI from a downstream server.
67    ///
68    /// - `server`: The server name (e.g., "postgres", "github")
69    /// - `uri`: The resource URI (e.g., "file:///logs/app.log")
70    ///
71    /// Returns the resource content as a JSON value.
72    async fn read_resource(
73        &self,
74        server: &str,
75        uri: &str,
76    ) -> Result<serde_json::Value, forge_error::DispatchError>;
77}
78
79/// Trait for dispatching stash operations from the sandbox.
80///
81/// The stash is a per-session key/value store that persists across sandbox
82/// executions within the same session. Entries are scoped by server group
83/// for isolation.
84#[async_trait::async_trait]
85pub trait StashDispatcher: Send + Sync {
86    /// Store a value under a key with an optional TTL.
87    ///
88    /// - `key`: Alphanumeric key (plus `_`, `-`, `.`, `:`) up to 256 chars
89    /// - `value`: The JSON value to store
90    /// - `ttl_secs`: TTL in seconds (0 = use default)
91    /// - `current_group`: The server group of the current execution, if any
92    async fn put(
93        &self,
94        key: &str,
95        value: serde_json::Value,
96        ttl_secs: Option<u32>,
97        current_group: Option<String>,
98    ) -> Result<serde_json::Value, forge_error::DispatchError>;
99
100    /// Retrieve the value stored under a key.
101    ///
102    /// Returns `null` if the key does not exist or has expired.
103    async fn get(
104        &self,
105        key: &str,
106        current_group: Option<String>,
107    ) -> Result<serde_json::Value, forge_error::DispatchError>;
108
109    /// Delete the entry stored under a key.
110    ///
111    /// Returns `{"deleted": true}` if the entry was removed, `{"deleted": false}` otherwise.
112    async fn delete(
113        &self,
114        key: &str,
115        current_group: Option<String>,
116    ) -> Result<serde_json::Value, forge_error::DispatchError>;
117
118    /// List all keys visible to the current group.
119    async fn keys(
120        &self,
121        current_group: Option<String>,
122    ) -> Result<serde_json::Value, forge_error::DispatchError>;
123}
124
125#[cfg(test)]
126mod feature_tests {
127    /// Verify that the default feature set includes ast-validator.
128    #[test]
129    #[cfg(feature = "ast-validator")]
130    fn ff_01_default_features_include_ast_validator() {
131        // This test only compiles when ast-validator is enabled (the default).
132        // If it disappears from default features, the test count will drop — caught by CI.
133        let result = crate::ast_validator::validate_ast("async () => { return 1; }");
134        assert!(result.is_ok());
135    }
136
137    /// Verify that `worker-pool` feature is on by default (v0.4.0+).
138    #[test]
139    #[cfg(feature = "worker-pool")]
140    fn ff_02_worker_pool_is_default() {
141        // worker-pool is default-on since v0.4.0.
142        // Verify the pool module types are accessible.
143        let _ = std::any::type_name::<crate::pool::WorkerPool>();
144        let _ = std::any::type_name::<crate::pool::PoolConfig>();
145    }
146
147    /// Verify that `metrics` feature is on by default (v0.4.0+).
148    #[test]
149    #[cfg(feature = "metrics")]
150    fn ff_03_metrics_is_default() {
151        // metrics is default-on since v0.4.0.
152        // Verify the metrics module types are accessible.
153        let _ = std::any::type_name::<crate::metrics::ForgeMetrics>();
154    }
155
156    /// Verify that the crate has the expected module layout regardless of features.
157    #[test]
158    fn ff_04_core_modules_always_available() {
159        // These types must be available regardless of feature flags.
160        let _ = std::any::type_name::<crate::SandboxError>();
161        let _ = std::any::type_name::<crate::SandboxConfig>();
162        let _ = std::any::type_name::<crate::ExecutionMode>();
163    }
164
165    /// Verify that minimal feature set disables worker-pool.
166    /// Only compiles under `--no-default-features`.
167    #[test]
168    #[cfg(not(feature = "worker-pool"))]
169    fn ff_05_minimal_disables_pool() {
170        // Under --no-default-features, worker-pool should be off.
171    }
172
173    /// Verify that minimal feature set disables metrics.
174    /// Only compiles under `--no-default-features`.
175    #[test]
176    #[cfg(not(feature = "metrics"))]
177    fn ff_06_minimal_disables_metrics() {
178        // Under --no-default-features, metrics should be off.
179    }
180}