gravityfile_plugin/wasm/
isolate.rs1use 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
12pub struct WasmIsolatedContext {
14 sandbox: SandboxConfig,
15}
16
17impl WasmIsolatedContext {
18 pub fn new(sandbox: SandboxConfig) -> PluginResult<Self> {
20 Ok(Self { sandbox })
21 }
22
23 fn build_plugin(&self, code: &[u8]) -> Result<Plugin, String> {
25 let wasm = Wasm::data(code);
26 let sandbox = &self.sandbox;
27
28 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 if cancel.is_cancelled() {
70 return Err(PluginError::Cancelled {
71 name: "wasm_isolate".into(),
72 });
73 }
74
75 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 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(()) }
124
125 fn get_global(&self, _name: &str) -> PluginResult<Value> {
126 Ok(Value::Null)
127 }
128}