Skip to main content

confium_sandbox_process/
sandbox.rs

1//! The sandbox abstraction.
2//!
3//! [`Sandbox`] is the trait every plugin-runtime impl satisfies (WASM
4//! in-process via wasmtime in `confium-sandbox-wasm`, out-of-process
5//! via stdin/stdout JSON-RPC in this crate's
6//! [`ProcessSandbox`](crate::ProcessSandbox)). Every instance runs
7//! inside the sandbox's capability envelope: a plugin cannot reach a
8//! host import it has not been granted.
9//!
10//! This trait mirrors `confium_sandbox_wasm::Sandbox` exactly so a
11//! consumer can swap runtimes behind a single trait object.
12
13use std::path::PathBuf;
14
15use crate::Result;
16
17/// A capability a sandboxed plugin may be granted.
18///
19/// Capabilities are explicit, granular, and revocable. The sandbox
20/// runtime checks every host-import invocation against the instance's
21/// current capability set; a denied call traps with
22/// [`Error::CapabilityDenied`](crate::Error).
23///
24/// For the process sandbox the capability set is enforced host-side:
25/// the host refuses to forward a `cfm_*` call to the subprocess unless
26/// the matching capability is present. (A future revision may push the
27/// capability gate into a seccomp/AppSandbox profile on the child.)
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub enum Capability {
30    /// Plugin may call the `cfm_<name>_*` host-import family
31    /// (e.g. `InterfaceAccess { name: "hash" }` enables `cfm_hash_*`).
32    InterfaceAccess { name: String },
33    /// Plugin may talk to this network endpoint.
34    NetworkEndpoint { url: String },
35    /// Plugin may reference this key (read/use, never reveal bytes).
36    KeyAccess { key_id: String },
37    /// Plugin may read/write this filesystem path.
38    FilesystemPath { path: PathBuf, mode: FilesystemMode },
39}
40
41/// Access mode for a [`Capability::FilesystemPath`].
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum FilesystemMode {
44    ReadOnly,
45    ReadWrite,
46}
47
48/// A value passed across the sandbox boundary. Intentionally a small
49/// set — structured data crosses via a length-prefixed byte payload
50/// (`Bytes`) rather than being encoded into the type system.
51#[derive(Debug, Clone, PartialEq)]
52pub enum Value {
53    I32(i32),
54    I64(i64),
55    F32(f32),
56    F64(f64),
57    /// Opaque byte slice. Marshaled as a JSON array of byte values on
58    /// the wire.
59    Bytes(Vec<u8>),
60}
61
62/// A loaded, capability-bound plugin.
63///
64/// Instances are mutable: capabilities can be granted and revoked
65/// between calls. A revoked capability takes effect immediately on
66/// the next call.
67pub trait SandboxInstance: Send + Sync {
68    /// Invoke `function` with `args`. Returns the function's results.
69    fn call(&mut self, function: &str, args: &[Value]) -> Result<Vec<Value>>;
70
71    /// Grant `cap` to this instance. Grants are idempotent.
72    fn grant_capability(&mut self, cap: Capability) -> Result<()>;
73
74    /// Revoke the matching capability. Revokes are idempotent.
75    fn revoke_capability(&mut self, cap: &Capability) -> Result<()>;
76}
77
78/// A plugin runtime.
79///
80/// Implementations are cheap to clone: they share the underlying
81/// engine state. Each [`load_module`](Sandbox::load_module) produces
82/// an independent [`SandboxInstance`] with its own subprocess and an
83/// empty capability set.
84pub trait Sandbox: Send + Sync {
85    /// Spawn a plugin subprocess.
86    ///
87    /// For the process sandbox, `bytes` is interpreted as the UTF-8
88    /// encoded path to the plugin executable. (This keeps the trait
89    /// signature byte-oriented so it matches the WASM sandbox's
90    /// `load_module(&[u8])`; the meaning of those bytes is runtime
91    /// specific.)
92    fn load_module(&self, bytes: &[u8]) -> Result<Box<dyn SandboxInstance>>;
93
94    /// Human-readable runtime name (e.g. `"process"`).
95    fn name(&self) -> &'static str;
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn capability_equality() {
104        let a = Capability::InterfaceAccess {
105            name: "hash".into(),
106        };
107        let b = Capability::InterfaceAccess {
108            name: "hash".into(),
109        };
110        assert_eq!(a, b);
111
112        let c = Capability::InterfaceAccess {
113            name: "sign".into(),
114        };
115        assert_ne!(a, c);
116    }
117
118    #[test]
119    fn capability_clone_preserves_fields() {
120        let cap = Capability::NetworkEndpoint {
121            url: "https://example.com".into(),
122        };
123        let cloned = cap.clone();
124        assert_eq!(cap, cloned);
125    }
126
127    #[test]
128    fn filesystem_mode_distinct() {
129        assert_ne!(FilesystemMode::ReadOnly, FilesystemMode::ReadWrite);
130    }
131}