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}