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