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