capsule_core/wasm/commands/
create.rs1use 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}