Skip to main content

capsule_core/wasm/commands/
create.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4use nanoid::nanoid;
5
6use wasmtime::component::{Linker, ResourceTable};
7use wasmtime::{Store, StoreLimitsBuilder};
8use wasmtime_wasi::add_to_linker_async;
9use wasmtime_wasi::{DirPerms, FilePerms, WasiCtxBuilder};
10use wasmtime_wasi_http::WasiHttpCtx;
11
12use crate::config::log::{CreateInstanceLog, InstanceState, UpdateInstanceLog};
13use crate::wasm::execution_policy::ExecutionPolicy;
14use crate::wasm::runtime::{Runtime, RuntimeCommand, WasmRuntimeError};
15use crate::wasm::state::{CapsuleAgent, State, capsule};
16use crate::wasm::utilities::path_validator::{FileAccessMode, validate_path};
17
18use crate::wasm::utilities::cache::load_or_compile_component;
19
20pub struct CreateInstance {
21    pub policy: ExecutionPolicy,
22    pub args: Vec<String>,
23    pub task_id: String,
24    pub task_name: String,
25    pub agent_name: String,
26    pub agent_version: String,
27    pub wasm_path: PathBuf,
28    pub project_root: PathBuf,
29}
30
31impl CreateInstance {
32    pub fn new(policy: ExecutionPolicy, args: Vec<String>) -> Self {
33        Self {
34            policy,
35            args,
36            task_id: nanoid!(10),
37            task_name: "default".to_string(),
38            agent_name: "default".to_string(),
39            agent_version: "0.0.0".to_string(),
40            wasm_path: PathBuf::from(".capsule/wasm/default.wasm"),
41            project_root: std::env::current_dir().unwrap_or_default(),
42        }
43    }
44
45    pub fn task_name(mut self, task_name: impl Into<String>) -> Self {
46        self.task_name = task_name.into();
47        self
48    }
49
50    pub fn agent_name(mut self, agent_name: impl Into<String>) -> Self {
51        self.agent_name = agent_name.into();
52        self
53    }
54
55    pub fn agent_version(mut self, agent_version: impl Into<String>) -> Self {
56        self.agent_version = agent_version.into();
57        self
58    }
59
60    pub fn wasm_path(mut self, wasm_path: PathBuf) -> Self {
61        self.wasm_path = wasm_path;
62        self
63    }
64
65    pub fn project_root(mut self, project_root: PathBuf) -> Self {
66        self.project_root = project_root;
67        self
68    }
69}
70
71impl RuntimeCommand for CreateInstance {
72    type Output = (Store<State>, CapsuleAgent, String);
73
74    async fn execute(
75        self,
76        runtime: Arc<Runtime>,
77    ) -> Result<(Store<State>, CapsuleAgent, String), WasmRuntimeError> {
78        runtime
79            .log
80            .commit_log(CreateInstanceLog {
81                agent_name: self.agent_name,
82                agent_version: self.agent_version,
83                task_id: self.task_id.clone(),
84                task_name: self.task_name,
85                state: InstanceState::Created,
86                fuel_limit: self.policy.compute.as_fuel(),
87                fuel_consumed: 0,
88            })
89            .await?;
90
91        let mut linker = Linker::<State>::new(&runtime.engine);
92
93        add_to_linker_async(&mut linker)?;
94        wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)?;
95
96        capsule::host::api::add_to_linker(&mut linker, |state: &mut State| state)?;
97
98        let envs = std::env::vars()
99            .collect::<Vec<_>>()
100            .into_iter()
101            .filter(|(key, _)| self.policy.env_variables.contains(key))
102            .collect::<Vec<_>>();
103
104        let mut wasi_builder = WasiCtxBuilder::new();
105        wasi_builder
106            .inherit_stdout()
107            .inherit_stderr()
108            .envs(&envs)
109            .args(&self.args);
110
111        for path_spec in &self.policy.allowed_files {
112            match validate_path(path_spec, &self.project_root) {
113                Ok(parsed) => {
114                    let (dir_perms, file_perms) = match parsed.mode {
115                        FileAccessMode::ReadOnly => (DirPerms::READ, FilePerms::READ),
116                        FileAccessMode::ReadWrite => (DirPerms::all(), FilePerms::all()),
117                    };
118
119                    if let Err(e) = wasi_builder.preopened_dir(
120                        &parsed.path,
121                        &parsed.guest_path,
122                        dir_perms,
123                        file_perms,
124                    ) {
125                        return Err(WasmRuntimeError::FilesystemError(format!(
126                            "Failed to preopen '{}': {}",
127                            path_spec, e
128                        )));
129                    }
130                }
131                Err(e) => {
132                    return Err(WasmRuntimeError::FilesystemError(e.to_string()));
133                }
134            }
135        }
136
137        let wasi = wasi_builder.build();
138
139        let mut limits = StoreLimitsBuilder::new();
140
141        if let Some(ram_bytes) = self.policy.ram {
142            limits = limits.memory_size(ram_bytes as usize);
143        }
144
145        let limits = limits.build();
146
147        let state = State {
148            ctx: wasi,
149            http_ctx: WasiHttpCtx::new(),
150            table: ResourceTable::new(),
151            limits,
152            runtime: Some(Arc::clone(&runtime)),
153            policy: self.policy.clone(),
154        };
155
156        let mut store = Store::new(&runtime.engine, state);
157
158        store.set_fuel(self.policy.compute.as_fuel())?;
159
160        store.limiter(|state| state);
161
162        let component = match runtime.get_component().await {
163            Some(c) => c,
164            None => {
165                let file_name = self.wasm_path.file_name().unwrap_or_default();
166                let cwasm_path = runtime
167                    .cache_dir
168                    .join("wasm")
169                    .join(std::path::Path::new(file_name).with_extension("cwasm"));
170
171                let c = load_or_compile_component(&runtime.engine, &self.wasm_path, &cwasm_path)?;
172
173                runtime.set_component(c.clone()).await;
174                c
175            }
176        };
177
178        let instance = match CapsuleAgent::instantiate_async(&mut store, &component, &linker).await
179        {
180            Ok(instance) => instance,
181            Err(e) => {
182                runtime
183                    .log
184                    .update_log(UpdateInstanceLog {
185                        task_id: self.task_id,
186                        state: InstanceState::Failed,
187                        fuel_consumed: 0,
188                    })
189                    .await?;
190                return Err(WasmRuntimeError::WasmtimeError(e));
191            }
192        };
193
194        Ok((store, instance, self.task_id))
195    }
196}