aegis_wasm/
lib.rs

1//! # Aegis - WebAssembly Sandbox Runtime
2//!
3//! Aegis is a local-first runtime that allows users and applications to execute
4//! untrusted WebAssembly code safely within a tightly controlled sandbox.
5//!
6//! ## Features
7//!
8//! - **Security**: Capability-based security with no ambient authority
9//! - **Resource Control**: Memory limits, CPU limits (fuel), and timeouts
10//! - **Observability**: Metrics collection and event subscription
11//! - **Embeddable**: Library-first design for easy integration
12//!
13//! ## Quick Start
14//!
15//! ```ignore
16//! use aegis::prelude::*;
17//!
18//! // Create a runtime
19//! let runtime = Aegis::builder()
20//!     .with_memory_limit(64 * 1024 * 1024)  // 64MB
21//!     .with_fuel_limit(1_000_000_000)        // 1B fuel units
22//!     .with_timeout(Duration::from_secs(30))
23//!     .build()?;
24//!
25//! // Load a module
26//! let module = runtime.load_file("plugin.wasm")?;
27//!
28//! // Execute in a sandbox
29//! let mut sandbox = runtime.sandbox().build()?;
30//! sandbox.load_module(&module)?;
31//!
32//! let result: i32 = sandbox.call("add", (2i32, 3i32))?;
33//! assert_eq!(result, 5);
34//! ```
35//!
36//! ## Security Model
37//!
38//! Aegis follows the principle of least privilege:
39//!
40//! 1. **No Ambient Authority**: All permissions must be explicitly granted
41//! 2. **Capability-Based**: Each capability explicitly defines allowed actions
42//! 3. **Resource Limits**: Memory, CPU, and time are bounded
43//! 4. **Isolation**: Each sandbox runs in its own isolated environment
44//!
45//! ## Architecture
46//!
47//! ```text
48//! ┌─────────────────────────────────────────────────────────┐
49//! │                    Your Application                     │
50//! ├─────────────────────────────────────────────────────────┤
51//! │                      aegis (facade)                     │
52//! │                    ┌─────────────────┐                  │
53//! │                    │  Aegis Builder  │                  │
54//! │                    └────────┬────────┘                  │
55//! │                             │                           │
56//! │  ┌──────────────┬──────────┴───────┬───────────────┐   │
57//! │  │ aegis-core   │ aegis-capability │ aegis-observe │   │
58//! │  │ (engine,     │ (permissions)    │ (metrics,     │   │
59//! │  │  sandbox)    │                  │  events)      │   │
60//! │  └──────────────┴──────────────────┴───────────────┘   │
61//! ├─────────────────────────────────────────────────────────┤
62//! │                       Wasmtime                          │
63//! └─────────────────────────────────────────────────────────┘
64//! ```
65
66use std::path::Path;
67use std::sync::Arc;
68use std::time::Duration;
69
70use aegis_capability::{
71    CapabilitySet, CapabilitySetBuilder, ClockCapability, FilesystemCapability, LoggingCapability,
72    NetworkCapability,
73};
74use aegis_core::{
75    AegisEngine, EngineConfig, ExecutionError, ModuleLoader, ResourceLimits, Sandbox, SandboxConfig, SharedEngine, ValidatedModule,
76};
77use aegis_observe::{EventDispatcher, EventSubscriber};
78
79// Re-export from sub-crates
80pub use aegis_capability;
81pub use aegis_core;
82pub use aegis_host;
83pub use aegis_observe;
84pub use aegis_resource;
85
86/// Main entry point for Aegis.
87pub struct Aegis;
88
89impl Aegis {
90    /// Create a new Aegis runtime builder.
91    pub fn builder() -> AegisBuilder {
92        AegisBuilder::new()
93    }
94
95    /// Create a runtime with default configuration.
96    pub fn default() -> Result<AegisRuntime, AegisError> {
97        AegisBuilder::new().build()
98    }
99}
100
101/// Builder for configuring the Aegis runtime.
102pub struct AegisBuilder {
103    engine_config: EngineConfig,
104    resource_limits: ResourceLimits,
105    capabilities: CapabilitySetBuilder,
106    event_subscribers: Vec<Arc<dyn EventSubscriber>>,
107}
108
109impl AegisBuilder {
110    /// Create a new builder with default configuration.
111    pub fn new() -> Self {
112        Self {
113            engine_config: EngineConfig::default(),
114            resource_limits: ResourceLimits::default(),
115            capabilities: CapabilitySetBuilder::new(),
116            event_subscribers: Vec::new(),
117        }
118    }
119
120    // Engine configuration
121
122    /// Enable or disable async execution support.
123    pub fn with_async_support(mut self, enabled: bool) -> Self {
124        self.engine_config.async_support = enabled;
125        self
126    }
127
128    /// Enable or disable the Component Model.
129    pub fn with_component_model(mut self, enabled: bool) -> Self {
130        self.engine_config.component_model = enabled;
131        self
132    }
133
134    /// Enable or disable debug info.
135    pub fn with_debug_info(mut self, enabled: bool) -> Self {
136        self.engine_config.debug_info = enabled;
137        self
138    }
139
140    // Resource limits
141
142    /// Set the maximum memory limit in bytes.
143    pub fn with_memory_limit(mut self, bytes: usize) -> Self {
144        self.resource_limits.max_memory_bytes = bytes;
145        self
146    }
147
148    /// Set the initial fuel limit.
149    pub fn with_fuel_limit(mut self, fuel: u64) -> Self {
150        self.resource_limits.initial_fuel = fuel;
151        self
152    }
153
154    /// Set the execution timeout.
155    pub fn with_timeout(mut self, timeout: Duration) -> Self {
156        self.resource_limits.timeout = timeout;
157        self
158    }
159
160    /// Set custom resource limits.
161    pub fn with_resource_limits(mut self, limits: ResourceLimits) -> Self {
162        self.resource_limits = limits;
163        self
164    }
165
166    // Capabilities
167
168    /// Add the filesystem capability.
169    pub fn with_filesystem(mut self, config: FilesystemCapability) -> Self {
170        self.capabilities = self.capabilities.with(config);
171        self
172    }
173
174    /// Add the network capability.
175    pub fn with_network(mut self, config: NetworkCapability) -> Self {
176        self.capabilities = self.capabilities.with(config);
177        self
178    }
179
180    /// Add the logging capability.
181    pub fn with_logging(mut self, config: LoggingCapability) -> Self {
182        self.capabilities = self.capabilities.with(config);
183        self
184    }
185
186    /// Add the clock capability.
187    pub fn with_clock(mut self, config: ClockCapability) -> Self {
188        self.capabilities = self.capabilities.with(config);
189        self
190    }
191
192    /// Add a custom capability.
193    pub fn with_capability<C: aegis_capability::Capability + 'static>(mut self, cap: C) -> Self {
194        self.capabilities = self.capabilities.with(cap);
195        self
196    }
197
198    // Observability
199
200    /// Add an event subscriber.
201    pub fn with_event_subscriber(mut self, subscriber: Arc<dyn EventSubscriber>) -> Self {
202        self.event_subscribers.push(subscriber);
203        self
204    }
205
206    /// Build the runtime.
207    pub fn build(self) -> Result<AegisRuntime, AegisError> {
208        let engine = AegisEngine::new(self.engine_config).map_err(AegisError::Engine)?;
209        let shared_engine = Arc::new(engine);
210
211        let capabilities = self.capabilities.build().map_err(AegisError::Capability)?;
212
213        let event_dispatcher = EventDispatcher::new();
214        for subscriber in self.event_subscribers {
215            event_dispatcher.subscribe(subscriber);
216        }
217
218        Ok(AegisRuntime {
219            engine: shared_engine,
220            default_limits: self.resource_limits,
221            default_capabilities: Arc::new(capabilities),
222            event_dispatcher: Arc::new(event_dispatcher),
223        })
224    }
225}
226
227impl Default for AegisBuilder {
228    fn default() -> Self {
229        Self::new()
230    }
231}
232
233/// A configured Aegis runtime.
234pub struct AegisRuntime {
235    engine: SharedEngine,
236    default_limits: ResourceLimits,
237    default_capabilities: Arc<CapabilitySet>,
238    event_dispatcher: Arc<EventDispatcher>,
239}
240
241impl AegisRuntime {
242    /// Get a reference to the engine.
243    pub fn engine(&self) -> &SharedEngine {
244        &self.engine
245    }
246
247    /// Get the default resource limits.
248    pub fn default_limits(&self) -> &ResourceLimits {
249        &self.default_limits
250    }
251
252    /// Get the default capabilities.
253    pub fn default_capabilities(&self) -> &Arc<CapabilitySet> {
254        &self.default_capabilities
255    }
256
257    /// Get the event dispatcher.
258    pub fn event_dispatcher(&self) -> &Arc<EventDispatcher> {
259        &self.event_dispatcher
260    }
261
262    /// Create a module loader.
263    pub fn loader(&self) -> ModuleLoader {
264        ModuleLoader::new(Arc::clone(&self.engine))
265    }
266
267    /// Load a module from bytes.
268    pub fn load_bytes(&self, bytes: &[u8]) -> Result<ValidatedModule, AegisError> {
269        self.loader().load_bytes(bytes).map_err(AegisError::Module)
270    }
271
272    /// Load a module from a file.
273    pub fn load_file(&self, path: impl AsRef<Path>) -> Result<ValidatedModule, AegisError> {
274        self.loader()
275            .load_file(path.as_ref())
276            .map_err(AegisError::Module)
277    }
278
279    /// Load a module from WAT text format.
280    pub fn load_wat(&self, wat: &str) -> Result<ValidatedModule, AegisError> {
281        self.loader().load_wat(wat).map_err(AegisError::Module)
282    }
283
284    /// Create a sandbox builder with default configuration.
285    pub fn sandbox(&self) -> RuntimeSandboxBuilder<'_> {
286        RuntimeSandboxBuilder::new(self)
287    }
288
289    /// Execute a module quickly with default settings.
290    ///
291    /// This is a convenience method for simple use cases.
292    pub fn execute<R: wasmtime::WasmResults>(
293        &self,
294        module: &ValidatedModule,
295        function: &str,
296    ) -> Result<R, AegisError> {
297        let mut sandbox = self.sandbox().build()?;
298        sandbox.load_module(module).map_err(AegisError::Execution)?;
299        sandbox.call(function, ()).map_err(AegisError::Execution)
300    }
301}
302
303impl std::fmt::Debug for AegisRuntime {
304    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305        f.debug_struct("AegisRuntime")
306            .field("default_limits", &self.default_limits)
307            .finish()
308    }
309}
310
311/// Builder for creating sandboxes from a runtime.
312pub struct RuntimeSandboxBuilder<'a> {
313    runtime: &'a AegisRuntime,
314    limits: Option<ResourceLimits>,
315    capabilities: Option<Arc<CapabilitySet>>,
316}
317
318impl<'a> RuntimeSandboxBuilder<'a> {
319    fn new(runtime: &'a AegisRuntime) -> Self {
320        Self {
321            runtime,
322            limits: None,
323            capabilities: None,
324        }
325    }
326
327    /// Override resource limits.
328    pub fn with_limits(mut self, limits: ResourceLimits) -> Self {
329        self.limits = Some(limits);
330        self
331    }
332
333    /// Override memory limit.
334    pub fn with_memory_limit(mut self, bytes: usize) -> Self {
335        let mut limits = self.limits.take().unwrap_or_else(|| self.runtime.default_limits.clone());
336        limits.max_memory_bytes = bytes;
337        self.limits = Some(limits);
338        self
339    }
340
341    /// Override fuel limit.
342    pub fn with_fuel_limit(mut self, fuel: u64) -> Self {
343        let mut limits = self.limits.take().unwrap_or_else(|| self.runtime.default_limits.clone());
344        limits.initial_fuel = fuel;
345        self.limits = Some(limits);
346        self
347    }
348
349    /// Override timeout.
350    pub fn with_timeout(mut self, timeout: Duration) -> Self {
351        let mut limits = self.limits.take().unwrap_or_else(|| self.runtime.default_limits.clone());
352        limits.timeout = timeout;
353        self.limits = Some(limits);
354        self
355    }
356
357    /// Override capabilities.
358    pub fn with_capabilities(mut self, capabilities: Arc<CapabilitySet>) -> Self {
359        self.capabilities = Some(capabilities);
360        self
361    }
362
363    /// Build the sandbox.
364    pub fn build(self) -> Result<Sandbox<()>, AegisError> {
365        let limits = self.limits.unwrap_or_else(|| self.runtime.default_limits.clone());
366        let config = SandboxConfig::default().with_limits(limits);
367
368        Sandbox::new(Arc::clone(&self.runtime.engine), (), config).map_err(AegisError::Execution)
369    }
370
371    /// Build the sandbox with custom state.
372    pub fn build_with_state<S: Send + 'static>(self, state: S) -> Result<Sandbox<S>, AegisError> {
373        let limits = self.limits.unwrap_or_else(|| self.runtime.default_limits.clone());
374        let config = SandboxConfig::default().with_limits(limits);
375
376        Sandbox::new(Arc::clone(&self.runtime.engine), state, config).map_err(AegisError::Execution)
377    }
378}
379
380/// Errors from the Aegis runtime.
381#[derive(Debug, thiserror::Error)]
382pub enum AegisError {
383    /// Engine error.
384    #[error("Engine error: {0}")]
385    Engine(#[from] aegis_core::EngineError),
386
387    /// Module error.
388    #[error("Module error: {0}")]
389    Module(#[from] aegis_core::ModuleError),
390
391    /// Execution error.
392    #[error("Execution error: {0}")]
393    Execution(#[from] ExecutionError),
394
395    /// Capability error.
396    #[error("Capability error: {0}")]
397    Capability(#[from] aegis_capability::CapabilityError),
398}
399
400/// Prelude module for convenient imports.
401pub mod prelude {
402    // Main types
403    pub use crate::{Aegis, AegisBuilder, AegisError, AegisRuntime};
404
405    // Core types
406    pub use aegis_core::{
407        AegisEngine, EngineConfig, ModuleLoader, ResourceLimits, Sandbox, SandboxBuilder,
408        SandboxConfig, ValidatedModule,
409    };
410
411    // Capability types
412    pub use aegis_capability::{
413        Capability, CapabilityId, CapabilitySet, ClockCapability, FilesystemCapability,
414        LoggingCapability, NetworkCapability, PathPermission, PermissionResult,
415    };
416
417    // Resource types
418    pub use aegis_resource::{EpochConfig, EpochManager, FuelConfig, FuelManager};
419
420    // Observability types
421    pub use aegis_observe::{
422        EventDispatcher, EventSubscriber, ExecutionOutcome, ExecutionReport, MetricsCollector,
423        SandboxEvent,
424    };
425
426    // Common std types
427    pub use std::sync::Arc;
428    pub use std::time::Duration;
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn test_aegis_builder() {
437        let runtime = Aegis::builder()
438            .with_memory_limit(32 * 1024 * 1024)
439            .with_fuel_limit(100_000)
440            .with_timeout(Duration::from_secs(5))
441            .build()
442            .unwrap();
443
444        assert_eq!(runtime.default_limits().max_memory_bytes, 32 * 1024 * 1024);
445        assert_eq!(runtime.default_limits().initial_fuel, 100_000);
446    }
447
448    #[test]
449    fn test_load_and_execute() {
450        let runtime = Aegis::builder().build().unwrap();
451
452        let module = runtime
453            .load_wat(
454                r#"
455            (module
456                (func (export "answer") (result i32)
457                    i32.const 42
458                )
459            )
460        "#,
461            )
462            .unwrap();
463
464        let mut sandbox = runtime.sandbox().build().unwrap();
465        sandbox.load_module(&module).unwrap();
466
467        let result: i32 = sandbox.call("answer", ()).unwrap();
468        assert_eq!(result, 42);
469    }
470
471    #[test]
472    fn test_sandbox_builder_overrides() {
473        let runtime = Aegis::builder()
474            .with_fuel_limit(1_000_000)
475            .build()
476            .unwrap();
477
478        let sandbox = runtime
479            .sandbox()
480            .with_fuel_limit(500_000)
481            .with_memory_limit(16 * 1024 * 1024)
482            .build()
483            .unwrap();
484
485        // Verify overrides were applied
486        assert_eq!(sandbox.remaining_fuel(), Some(500_000));
487    }
488
489    #[test]
490    fn test_prelude_imports() {
491        use crate::prelude::*;
492
493        let _runtime = Aegis::builder().build().unwrap();
494    }
495}