Skip to main content

gravityfile_plugin/wasm/
isolate.rs

1//! Isolated WASM context for async plugin execution.
2
3use std::time::Duration;
4
5use extism::{Manifest, Plugin, Wasm};
6use tokio_util::sync::CancellationToken;
7
8use crate::runtime::{BoxFuture, IsolatedContext};
9use crate::sandbox::{Permission, SandboxConfig};
10use crate::types::{PluginError, PluginResult, Value};
11
12/// An isolated WASM context with limited API access.
13pub struct WasmIsolatedContext {
14    sandbox: SandboxConfig,
15}
16
17impl WasmIsolatedContext {
18    /// Create a new isolated WASM context.
19    pub fn new(sandbox: SandboxConfig) -> PluginResult<Self> {
20        Ok(Self { sandbox })
21    }
22
23    /// Build a sandboxed Extism plugin from raw WASM bytes.
24    fn build_plugin(&self, code: &[u8]) -> Result<Plugin, String> {
25        let wasm = Wasm::data(code);
26        let sandbox = &self.sandbox;
27
28        // Only enable WASI when the sandbox explicitly permits syscall-level access.
29        let allow_wasi = sandbox.has_permission(Permission::Execute)
30            || sandbox.has_permission(Permission::Network);
31
32        let mut manifest = Manifest::new([wasm]);
33
34        if sandbox.max_memory > 0 {
35            let pages = (sandbox.max_memory / (64 * 1024)).max(1) as u32;
36            manifest = manifest.with_memory_max(pages);
37        }
38
39        if sandbox.timeout_ms > 0 {
40            manifest = manifest.with_timeout(Duration::from_millis(sandbox.timeout_ms));
41        }
42
43        for path in &sandbox.allowed_read_paths {
44            if let Some(s) = path.to_str() {
45                manifest = manifest.with_allowed_path(s.to_string(), path);
46            }
47        }
48
49        for path in &sandbox.allowed_write_paths {
50            if let Some(s) = path.to_str()
51                && !sandbox.allowed_read_paths.contains(path)
52            {
53                manifest = manifest.with_allowed_path(s.to_string(), path);
54            }
55        }
56
57        Plugin::new(&manifest, [], allow_wasi).map_err(|e| e.to_string())
58    }
59}
60
61impl IsolatedContext for WasmIsolatedContext {
62    fn execute<'a>(
63        &'a self,
64        code: &'a [u8],
65        cancel: CancellationToken,
66    ) -> BoxFuture<'a, PluginResult<Value>> {
67        Box::pin(async move {
68            // Check for cancellation
69            if cancel.is_cancelled() {
70                return Err(PluginError::Cancelled {
71                    name: "wasm_isolate".into(),
72                });
73            }
74
75            // Build a sandboxed Extism plugin from the provided WASM binary.
76            let mut plugin = self
77                .build_plugin(code)
78                .map_err(|e| PluginError::ExecutionError {
79                    name: "wasm_isolate".into(),
80                    message: e,
81                })?;
82
83            // Extism allows running a "main" or default function
84            // We assume it's exported as "run"
85            let res = plugin.call::<&[u8], &[u8]>("run", &[]).map_err(|e| {
86                PluginError::ExecutionError {
87                    name: "wasm_isolate".into(),
88                    message: e.to_string(),
89                }
90            })?;
91
92            if res.is_empty() {
93                return Ok(Value::Null);
94            }
95
96            let val: Value =
97                serde_json::from_slice(res).map_err(|e| PluginError::ExecutionError {
98                    name: "wasm_isolate".into(),
99                    message: format!("Failed to parse WASM output: {}", e),
100                })?;
101
102            Ok(val)
103        })
104    }
105
106    fn call_function<'a>(
107        &'a self,
108        _name: &'a str,
109        _args: Vec<Value>,
110        _cancel: CancellationToken,
111    ) -> BoxFuture<'a, PluginResult<Value>> {
112        Box::pin(async move {
113            Err(PluginError::ExecutionError {
114                name: "wasm_isolate".into(),
115                message: "call_function not supported directly on uninitialized WASM context"
116                    .to_string(),
117            })
118        })
119    }
120
121    fn set_global(&mut self, _name: &str, _value: Value) -> PluginResult<()> {
122        Ok(()) // Not applicable for Extism WASM without memory sharing
123    }
124
125    fn get_global(&self, _name: &str) -> PluginResult<Value> {
126        Ok(Value::Null)
127    }
128}