1use crate::api::openai_compat::SharedPipeline;
2use crate::config::ProviderConfig;
3use crate::layers::{
4 key_selector::ApiKeyState, ErrorAction, ErrorHandlerLayer, KeySelectorLayer, RequestError,
5 RouterLayer,
6};
7use crate::package::config::PackagePermissions;
8use crate::package::circuit_breaker::CircuitBreaker;
9use crate::package::permissions::{Permission, PermissionChecker};
10use crate::process::ProcessManager;
11use crate::types::ChatRequest;
12use crate::vkeys::VirtualKeyStore;
13use anyhow::Result;
14use async_trait::async_trait;
15use base64::Engine;
16use extism::*;
17use rusqlite::{
18 types::{ToSql, ToSqlOutput, ValueRef},
19 Connection, OpenFlags,
20};
21use std::collections::HashMap;
22use std::io::{Read as IoRead, Write};
23use std::path::{Path, PathBuf};
24use std::sync::{Arc, Mutex as StdMutex};
25use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
26use tokio::runtime::Handle;
27
28pub fn host_blocking_handle() -> Handle {
42 use std::sync::OnceLock;
43 static HOST_RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
44 HOST_RT
45 .get_or_init(|| {
46 tokio::runtime::Builder::new_multi_thread()
47 .enable_all()
48 .thread_name("weft-host-blocking")
49 .build()
50 .expect("failed to build dedicated WASM host runtime")
51 })
52 .handle()
53 .clone()
54}
55
56pub type PackageMap = Arc<StdMutex<HashMap<String, Arc<StdMutex<Plugin>>>>>;
61
62#[derive(Clone)]
66pub struct WasmHostState {
67 pub config: Arc<tokio::sync::RwLock<crate::config::AppConfig>>,
68 pub pipeline: SharedPipeline,
69 pub runtime_handle: Handle,
70 pub process_manager: Arc<ProcessManager>,
71 pub vkey_store: Arc<VirtualKeyStore>,
72 pub kv_store: Arc<StdMutex<HashMap<String, String>>>,
73 pub caller_package_name: String,
75 pub package_dir: String,
80 pub permissions: PackagePermissions,
82 pub package_map: PackageMap,
84 pub package_aliases: Arc<StdMutex<HashMap<String, String>>>,
85 pub call_depth: Arc<StdMutex<u32>>,
87 pub app_state: Arc<StdMutex<Option<crate::api::openai_compat::AppState>>>,
88}
89
90macro_rules! gate_permission {
95 ($user_data:expr, $perm:expr) => {{
96 let __ud = $user_data.get()?;
97 let __ud = __ud.lock().unwrap();
98 let __checker =
99 PermissionChecker::new(&__ud.caller_package_name, __ud.permissions.clone());
100 if let Err(__e) = __checker.check($perm) {
101 tracing::warn!(
102 "permission denied: package '{}' lacks '{}' for host call",
103 __ud.caller_package_name,
104 $perm
105 );
106 return Ok(serde_json::json!({
107 "error": format!("permission denied: {}", __e)
108 })
109 .to_string());
110 }
111 }};
112}
113
114macro_rules! gate_permission_unit {
116 ($user_data:expr, $perm:expr) => {{
117 let __ud = $user_data.get()?;
118 let __ud = __ud.lock().unwrap();
119 let __checker =
120 PermissionChecker::new(&__ud.caller_package_name, __ud.permissions.clone());
121 if let Err(__e) = __checker.check($perm) {
122 tracing::warn!(
123 "permission denied: package '{}' lacks '{}' for host call (ignored)",
124 __ud.caller_package_name,
125 $perm
126 );
127 let _ = __e;
128 return Ok(());
129 }
130 }};
131}
132
133fn normalize_package_dir(dir: &Path) -> String {
137 let s = dir.display().to_string();
138 let s = if let Some(r) = s.strip_prefix(r#"\\?\UNC\"#) {
139 format!(r#"\\{}"#, r)
140 } else if let Some(r) = s.strip_prefix(r#"\\?\"#) {
141 r.to_string()
142 } else {
143 s
144 };
145 s.replace('\\', "/")
146}
147
148fn resolve_loaded_package_name(ud: &WasmHostState, requested: &str) -> String {
149 let requested_trimmed = requested.trim();
150 if requested_trimmed.is_empty() {
151 return String::new();
152 }
153
154 let alias_target = ud
155 .package_aliases
156 .lock()
157 .ok()
158 .and_then(|aliases| aliases.get(requested_trimmed).cloned());
159
160 if let Some(target) = alias_target {
161 let target_trimmed = target.trim();
162 if !target_trimmed.is_empty() {
163 let has_target = ud
164 .package_map
165 .lock()
166 .map(|map| map.contains_key(target_trimmed))
167 .unwrap_or(false);
168 if has_target {
169 return target_trimmed.to_string();
170 }
171 }
172 }
173
174 requested_trimmed.to_string()
175}
176
177pub fn resolve_loaded_package_name_from_aliases(
178 package_aliases: &HashMap<String, String>,
179 loaded_package_names: &[String],
180 requested: &str,
181) -> String {
182 let requested_trimmed = requested.trim();
183 if requested_trimmed.is_empty() {
184 return String::new();
185 }
186
187 if let Some(target) = package_aliases.get(requested_trimmed) {
188 let target_trimmed = target.trim();
189 if !target_trimmed.is_empty()
190 && loaded_package_names
191 .iter()
192 .any(|loaded| loaded == target_trimmed)
193 {
194 return target_trimmed.to_string();
195 }
196 }
197
198 requested_trimmed.to_string()
199}
200
201#[derive(Debug, Default, serde::Deserialize)]
204struct HostExecInput {
205 #[serde(default)]
206 command: String,
207 #[serde(default)]
208 args: Vec<String>,
209 #[serde(default)]
210 stdin: Option<String>,
211 #[serde(default)]
212 stdin_base64: Option<String>,
213 #[serde(default)]
214 workdir: Option<String>,
215 #[serde(default)]
216 timeout_ms: Option<u64>,
217 #[serde(default)]
218 env: HashMap<String, String>,
219}
220
221#[derive(Debug, Default, serde::Deserialize)]
222struct HostChatCompletionInput {
223 #[serde(default)]
224 request_label: String,
225 #[serde(default)]
226 endpoint: String,
227 #[serde(default)]
228 body: String,
229}
230
231fn exec_result_json(status: i32, stdout: &[u8], stderr: &[u8]) -> String {
232 serde_json::json!({
233 "status": status,
234 "stdout": String::from_utf8_lossy(stdout),
235 "stderr": String::from_utf8_lossy(stderr),
236 "stdout_base64": base64::engine::general_purpose::STANDARD.encode(stdout),
237 "stderr_base64": base64::engine::general_purpose::STANDARD.encode(stderr),
238 })
239 .to_string()
240}
241
242fn run_host_exec_command(parsed: HostExecInput) -> String {
243 if parsed.command.trim().is_empty() {
244 return r#"{"error":"missing command"}"#.to_string();
245 }
246
247 let mut command = std::process::Command::new(&parsed.command);
248 command.args(&parsed.args);
249 command.stdout(std::process::Stdio::piped());
250 command.stderr(std::process::Stdio::piped());
251
252 #[cfg(windows)]
254 {
255 use std::os::windows::process::CommandExt;
256 command.creation_flags(0x08000000); }
258
259 if parsed.stdin.is_some() || parsed.stdin_base64.is_some() {
260 command.stdin(std::process::Stdio::piped());
261 }
262
263 if let Some(workdir) = parsed
264 .workdir
265 .as_deref()
266 .map(str::trim)
267 .filter(|value| !value.is_empty())
268 {
269 command.current_dir(workdir);
270 }
271
272 if !parsed.env.is_empty() {
273 command.envs(parsed.env.iter());
274 }
275
276 let timeout_ms = parsed.timeout_ms;
277 let stdin_text = parsed.stdin;
278 let stdin_base64 = parsed.stdin_base64;
279
280 let mut child = match command.spawn() {
281 Ok(child) => child,
282 Err(error) => return format!(r#"{{"error":"{}"}}"#, error),
283 };
284
285 if let Some(mut stdin) = child.stdin.take() {
286 let write_result = if let Some(encoded) = stdin_base64 {
287 match base64::engine::general_purpose::STANDARD.decode(encoded) {
288 Ok(bytes) => stdin.write_all(&bytes),
289 Err(error) => {
290 return format!(r#"{{"error":"invalid stdin_base64: {}"}}"#, error);
291 }
292 }
293 } else if let Some(text) = stdin_text {
294 stdin.write_all(text.as_bytes())
295 } else {
296 Ok(())
297 };
298
299 if let Err(error) = write_result {
300 return format!(r#"{{"error":"failed to write stdin: {}"}}"#, error);
301 }
302 }
303
304 if let Some(timeout_ms) = timeout_ms {
305 let deadline = Instant::now() + Duration::from_millis(timeout_ms);
306 loop {
307 match child.try_wait() {
308 Ok(Some(_)) => {
309 return match child.wait_with_output() {
310 Ok(output) => exec_result_json(
311 output.status.code().unwrap_or(-1),
312 &output.stdout,
313 &output.stderr,
314 ),
315 Err(error) => format!(r#"{{"error":"{}"}}"#, error),
316 };
317 }
318 Ok(None) => {
319 if Instant::now() >= deadline {
320 let _ = child.kill();
321 return match child.wait_with_output() {
322 Ok(output) => {
323 let timeout_stderr = format!(
324 "{}{}command timed out after {}ms",
325 String::from_utf8_lossy(&output.stderr),
326 if output.stderr.is_empty() { "" } else { "\n" },
327 timeout_ms
328 );
329 exec_result_json(-1, &output.stdout, timeout_stderr.as_bytes())
330 }
331 Err(error) => format!(r#"{{"error":"{}"}}"#, error),
332 };
333 }
334 std::thread::sleep(Duration::from_millis(10));
335 }
336 Err(error) => return format!(r#"{{"error":"{}"}}"#, error),
337 }
338 }
339 }
340
341 match child.wait_with_output() {
342 Ok(output) => exec_result_json(
343 output.status.code().unwrap_or(-1),
344 &output.stdout,
345 &output.stderr,
346 ),
347 Err(error) => format!(r#"{{"error":"{}"}}"#, error),
348 }
349}
350
351host_fn!(pub host_log(user_data: WasmHostState; input: String) -> String {
352 let parsed: Vec<String> = serde_json::from_str(&input).unwrap_or_default();
354 let level = parsed.first().map(|s| s.as_str()).unwrap_or("info");
355 let msg = parsed.get(1).map(|s| s.as_str()).unwrap_or("");
356 match level {
357 "error" => tracing::error!("[package] {}", msg),
358 "warn" => tracing::warn!("[package] {}", msg),
359 "debug" => tracing::debug!("[package] {}", msg),
360 _ => tracing::info!("[package] {}", msg),
361 }
362 Ok("ok".to_string())
363});
364
365host_fn!(pub host_kv_get(user_data: WasmHostState; key: String) -> String {
366 let ud = user_data.get()?;
367 let ud = ud.lock().unwrap();
368 let store = ud.kv_store.lock().unwrap();
369 Ok(store.get(&key).cloned().unwrap_or_default())
370});
371
372host_fn!(pub host_kv_set(user_data: WasmHostState; input: String) {
373 let parsed: Vec<String> = serde_json::from_str(&input).unwrap_or_default();
375 let key = parsed.first().cloned().unwrap_or_default();
376 let value = parsed.get(1).cloned().unwrap_or_default();
377 let ud = user_data.get()?;
378 let ud = ud.lock().unwrap();
379 ud.kv_store.lock().unwrap().insert(key, value);
380 Ok(())
381});
382
383host_fn!(pub host_kv_list(user_data: WasmHostState; prefix: String) -> String {
384 let ud = user_data.get()?;
385 let ud = ud.lock().unwrap();
386 let store = ud.kv_store.lock().unwrap();
387 let keys: Vec<String> = store
388 .keys()
389 .filter(|key| key.starts_with(&prefix))
390 .cloned()
391 .collect();
392 Ok(serde_json::to_string(&keys).unwrap_or_else(|_| "[]".to_string()))
393});
394
395host_fn!(pub host_kv_delete(user_data: WasmHostState; key: String) {
396 let ud = user_data.get()?;
397 let ud = ud.lock().unwrap();
398 ud.kv_store.lock().unwrap().remove(&key);
399 Ok(())
400});
401
402host_fn!(pub host_env_get(user_data: WasmHostState; key: String) -> String {
403 if key == "WEFT_PACKAGE_DIR" {
405 let ud = user_data.get()?;
406 let ud = ud.lock().unwrap();
407 return Ok(ud.package_dir.clone());
408 }
409 Ok(std::env::var(&key).unwrap_or_default())
410});
411
412host_fn!(pub host_home_dir(user_data: WasmHostState; input: String) -> String {
413 let _ = user_data;
414 let _ = input;
415 Ok(std::env::var("USERPROFILE")
416 .or_else(|_| std::env::var("HOME"))
417 .unwrap_or_default())
418});
419
420host_fn!(pub host_read_file(user_data: WasmHostState; path: String) -> String {
421 gate_permission!(user_data, Permission::Storage);
422 if path.contains("..") {
423 return Ok(r#"{"error":"path traversal not allowed"}"#.to_string());
424 }
425 let path_ref = std::path::Path::new(&path);
426 match std::fs::metadata(path_ref) {
427 Ok(metadata) if metadata.is_dir() => {
428 return Ok(r#"{"error":"path is a directory; use fs_list instead","suggested_tool":"fs_list"}"#.to_string());
429 }
430 Ok(_) => {}
431 Err(e) => return Ok(format!(r#"{{"error":"{}"}}"#, e)),
432 }
433 match std::fs::read_to_string(path_ref) {
434 Ok(content) => Ok(content),
435 Err(e) => Ok(format!(r#"{{"error":"{}"}}"#, e)),
436 }
437});
438
439host_fn!(pub host_write_file(user_data: WasmHostState; input: String) {
440 gate_permission_unit!(user_data, Permission::Storage);
441 let parsed: Vec<String> = serde_json::from_str(&input).unwrap_or_default();
443 let path = parsed.first().cloned().unwrap_or_default();
444 let content = parsed.get(1).cloned().unwrap_or_default();
445 if path.contains("..") {
446 return Ok(());
447 }
448 if let Some(parent) = std::path::Path::new(&path).parent() {
449 let _ = std::fs::create_dir_all(parent);
450 }
451 let _ = std::fs::write(&path, &content);
452 Ok(())
453});
454
455host_fn!(pub host_write_file_base64(user_data: WasmHostState; input: String) -> String {
459 gate_permission!(user_data, Permission::Storage);
460 let parsed: Vec<String> = serde_json::from_str(&input).unwrap_or_default();
461 let path = parsed.first().cloned().unwrap_or_default();
462 let b64 = parsed.get(1).cloned().unwrap_or_default();
463 if path.is_empty() {
464 return Ok(r#"{"error":"host_write_file_base64 requires path"}"#.to_string());
465 }
466 if path.contains("..") {
467 return Ok(r#"{"error":"path traversal not allowed"}"#.to_string());
468 }
469 let bytes = match base64::engine::general_purpose::STANDARD.decode(b64.trim()) {
470 Ok(b) => b,
471 Err(e) => return Ok(serde_json::json!({ "error": format!("invalid base64: {e}") }).to_string()),
472 };
473 if let Some(parent) = std::path::Path::new(&path).parent() {
474 let _ = std::fs::create_dir_all(parent);
475 }
476 match std::fs::write(&path, &bytes) {
477 Ok(()) => {
478 let abs = std::fs::canonicalize(&path)
482 .map(|p| {
483 let s = p.to_string_lossy().to_string();
484 s.strip_prefix(r"\\?\").map(|x| x.to_string()).unwrap_or(s)
485 })
486 .unwrap_or_else(|_| path.clone());
487 Ok(serde_json::json!({ "ok": true, "path": abs, "bytes": bytes.len() }).to_string())
488 }
489 Err(e) => Ok(serde_json::json!({ "error": format!("write failed: {e}") }).to_string()),
490 }
491});
492
493host_fn!(pub host_list_dir(user_data: WasmHostState; path: String) -> String {
494 gate_permission!(user_data, Permission::Storage);
495 if path.contains("..") {
496 return Ok(r#"{"error":"path traversal not allowed"}"#.to_string());
497 }
498 match std::fs::read_dir(&path) {
499 Ok(entries) => {
500 let items: Vec<serde_json::Value> = entries
501 .flatten()
502 .map(|e| {
503 let is_dir = e.file_type().map(|t| t.is_dir()).unwrap_or(false);
504 serde_json::json!({
505 "name": e.file_name().to_string_lossy(),
506 "is_dir": is_dir,
507 })
508 })
509 .collect();
510 Ok(serde_json::to_string(&items).unwrap_or_else(|_| "[]".to_string()))
511 }
512 Err(e) => Ok(format!(r#"{{"error":"{}"}}"#, e)),
513 }
514});
515
516host_fn!(pub host_exec(user_data: WasmHostState; input: String) -> String {
517 gate_permission!(user_data, Permission::Process);
518 let parsed: HostExecInput = serde_json::from_str(&input).unwrap_or_default();
519 Ok(run_host_exec_command(parsed))
520});
521
522host_fn!(pub host_exec_advanced(user_data: WasmHostState; input: String) -> String {
523 gate_permission!(user_data, Permission::Process);
524 let parsed: HostExecInput = serde_json::from_str(&input).unwrap_or_default();
525 Ok(run_host_exec_command(parsed))
526});
527
528host_fn!(pub host_chat_completion(user_data: WasmHostState; input: String) -> String {
529 gate_permission!(user_data, Permission::Network);
530 let parsed: HostChatCompletionInput = serde_json::from_str(&input).unwrap_or_default();
531 if parsed.body.trim().is_empty() {
532 return Ok(r#"{"error":"missing body"}"#.to_string());
533 }
534
535 let request: ChatRequest = match serde_json::from_str(&parsed.body) {
536 Ok(request) => request,
537 Err(error) => {
538 return Ok(serde_json::json!({
539 "error": format!("invalid chat completion body: {}", error),
540 })
541 .to_string())
542 }
543 };
544
545 if request.stream {
546 return Ok(r#"{"error":"streaming chat completions are not supported via host_chat_completion"}"#.to_string());
547 }
548
549 let ud = user_data.get()?;
550 let ud = ud.lock().unwrap();
551 let Some(app_state) = ud.app_state.lock().ok().and_then(|state| state.clone()) else {
552 return Ok(r#"{"error":"app state unavailable for host_chat_completion"}"#.to_string());
553 };
554
555 let request_label = parsed.request_label.trim().to_string();
556 let endpoint = parsed.endpoint.trim().to_string();
557
558
559
560 let result = ud.runtime_handle.block_on(async move {
561 let config = app_state.config.read().await;
562 app_state.pipeline.execute(&request, &config).await
563 });
564
565 match result {
566 Ok(response) => serde_json::to_string(&response).map_err(|error| {
567 extism::Error::msg(format!(
568 "host_chat_completion failed to serialize response (label='{}', endpoint='{}'): {}",
569 request_label, endpoint, error
570 ))
571 }),
572 Err(error) => Ok(serde_json::json!({
573 "error": format!(
574 "host_chat_completion failed (label='{}', endpoint='{}'): {}",
575 request_label,
576 endpoint,
577 error
578 ),
579 })
580 .to_string()),
581 }
582});
583
584host_fn!(pub host_http_request(user_data: WasmHostState; input: String) -> String {
589 gate_permission!(user_data, Permission::Network);
590
591 #[derive(serde::Deserialize, Default)]
592 struct HttpReqInput {
593 #[serde(default)]
594 method: String,
595 #[serde(default)]
596 url: String,
597 #[serde(default)]
598 headers: std::collections::HashMap<String, String>,
599 #[serde(default)]
600 body: String,
601 }
602
603 let parsed: HttpReqInput = serde_json::from_str(&input).unwrap_or_default();
604 if parsed.url.trim().is_empty() {
605 return Ok(r#"{"error":"host_http_request requires url"}"#.to_string());
606 }
607 let method = if parsed.method.trim().is_empty() {
608 "GET".to_string()
609 } else {
610 parsed.method.trim().to_uppercase()
611 };
612
613 let ud = user_data.get()?;
614 let ud = ud.lock().unwrap();
615 let runtime_handle = ud.runtime_handle.clone();
616 drop(ud);
617
618 let result: Result<(u16, String), String> = runtime_handle.block_on(async move {
619 let client = reqwest::Client::new();
620 let m = reqwest::Method::from_bytes(method.as_bytes())
621 .map_err(|e| format!("invalid method: {e}"))?;
622 let mut req = client.request(m, &parsed.url);
623 for (k, v) in &parsed.headers {
624 req = req.header(k.as_str(), v.as_str());
625 }
626 if !parsed.body.is_empty() {
627 req = req.body(parsed.body.clone());
628 }
629 let resp = req.send().await.map_err(|e| format!("request failed: {e}"))?;
630 let status = resp.status().as_u16();
631 let text = resp.text().await.map_err(|e| format!("read body failed: {e}"))?;
632 Ok((status, text))
633 });
634
635 match result {
636 Ok((status, body)) => Ok(serde_json::json!({ "status": status, "body": body }).to_string()),
637 Err(error) => Ok(serde_json::json!({ "error": error }).to_string()),
638 }
639});
640
641
642fn extract_json_string_prefix(raw: &str) -> String {
645 let mut out = String::new();
646 let mut chars = raw.chars().peekable();
647 while let Some(c) = chars.next() {
648 match c {
649 '"' => break,
650 '\\' => {
651 match chars.next() {
652 Some('n') => out.push('\n'),
653 Some('t') => out.push('\t'),
654 Some('r') => out.push('\r'),
655 Some('"') => out.push('"'),
656 Some('\\') => out.push('\\'),
657 Some('/') => out.push('/'),
658 Some(other) => { out.push('\\'); out.push(other); }
659 None => {}
660 }
661 }
662 other => out.push(other),
663 }
664 }
665 out
666}
667
668fn is_json_string_complete(raw: &str) -> bool {
670 let mut escaped = false;
671 for c in raw.chars() {
672 if escaped { escaped = false; continue; }
673 if c == '\\' { escaped = true; continue; }
674 if c == '"' { return true; }
675 }
676 false
677}
678
679host_fn!(pub host_chat_completion_stream(user_data: WasmHostState; input: String) -> String {
680 gate_permission!(user_data, Permission::Network);
681 #[derive(serde::Deserialize, Default)]
682 struct StreamInput {
683 #[serde(default)] request_label: String,
684 #[serde(default)] body: String,
685 #[serde(default)] session_id: String,
686 }
687
688 let parsed: StreamInput = serde_json::from_str(&input).unwrap_or_default();
689 if parsed.body.trim().is_empty() {
690 return Ok(r#"{"error":"missing body"}"#.to_string());
691 }
692 if parsed.session_id.trim().is_empty() {
693 return Ok(r#"{"error":"missing session_id"}"#.to_string());
694 }
695
696 let mut request: crate::types::ChatRequest = match serde_json::from_str(&parsed.body) {
697 Ok(r) => r,
698 Err(e) => return Ok(serde_json::json!({"error": format!("invalid body: {}", e)}).to_string()),
699 };
700 request.stream = true;
701
702 let ud = user_data.get()?;
703 let ud = ud.lock().unwrap();
704 let Some(app_state) = ud.app_state.lock().ok().and_then(|s| s.clone()) else {
705 return Ok(r#"{"error":"app state unavailable"}"#.to_string());
706 };
707
708 let session_id = parsed.session_id.clone();
709 let request_label = parsed.request_label.clone();
710 let runtime_handle = ud.runtime_handle.clone();
711
712 let (tx, rx) = std::sync::mpsc::channel::<Result<String, String>>();
716
717 runtime_handle.spawn(async move {
718 use futures_util::StreamExt;
719
720 let config = app_state.config.read().await.clone();
721 let mut attempt = 0u32;
725 const MAX_STREAM_ATTEMPTS: u32 = 3;
726 let (provider_name, resp) = loop {
727 attempt += 1;
728 let stream_result = match tokio::time::timeout(
731 std::time::Duration::from_secs(30),
732 app_state.pipeline.execute_stream(&request, &config),
733 )
734 .await
735 {
736 Ok(r) => r,
737 Err(_) => Err(anyhow::anyhow!("execute_stream timed out after 30s")),
738 };
739 match stream_result {
740 Ok(r) => break r,
741 Err(e) => {
742 let msg = e.to_string();
743 let is_client_error = msg.contains("status 4");
745 if attempt >= MAX_STREAM_ATTEMPTS || is_client_error {
746 let _ = tx.send(Err(format!("stream request failed: {}", msg)));
747 return;
748 }
749 tracing::warn!(
750 "stream completion attempt {}/{} failed, retrying: {}",
751 attempt, MAX_STREAM_ATTEMPTS, msg
752 );
753 tokio::time::sleep(std::time::Duration::from_millis(500 * attempt as u64)).await;
754 }
755 }
756 };
757
758 let provider = config.providers.iter().find(|p| p.name == provider_name).cloned();
759 let transforms = app_state.pipeline.transforms.clone();
760
761 let mut byte_stream = resp.bytes_stream();
762 let mut buffer: Vec<u8> = Vec::new();
765 let mut full_reply = String::new();
766 let mut assistant_state: u8 = 0;
769 let mut assistant_buf = String::new(); let mut last_emitted_len: usize = 0; loop {
773 let next = tokio::time::timeout(
776 std::time::Duration::from_secs(60),
777 byte_stream.next(),
778 )
779 .await;
780 let chunk_result = match next {
781 Ok(Some(item)) => item,
782 Ok(None) => break, Err(_) => {
784 tracing::warn!("stream idle timeout (60s without chunk), aborting");
785 break;
786 }
787 };
788 let chunk = match chunk_result {
789 Ok(c) => c,
790 Err(e) => { tracing::warn!("stream chunk error: {}", e); break; }
791 };
792 buffer.extend_from_slice(&chunk);
793
794 while let Some(nl) = buffer.iter().position(|&b| b == b'\n') {
795 let line_bytes: Vec<u8> = buffer.drain(..=nl).collect();
796 let line = String::from_utf8_lossy(&line_bytes).trim().to_string();
797 if line.is_empty() { continue; }
798
799 if let Some(ref prov) = provider {
800 match transforms.for_format(&prov.format).transform_stream_chunk(&line, prov).await {
801 Ok(Some(chunk_obj)) => {
802 let delta = chunk_obj.choices.first()
803 .and_then(|c| c.delta.content.as_deref())
804 .unwrap_or("");
805 if !delta.is_empty() {
806 full_reply.push_str(delta);
807
808 if assistant_state < 2 {
812 if assistant_state == 0 {
813 if let Some(pos) = full_reply.find(r#""assistant":""#) {
815 let start = pos + r#""assistant":""#.len();
816 assistant_buf = full_reply[start..].to_string();
817 assistant_state = 1;
818 }
819 } else {
820 assistant_buf.push_str(delta);
822 }
823
824 if assistant_state == 1 {
825 let visible = extract_json_string_prefix(&assistant_buf);
827 if visible.len() > last_emitted_len {
828 let new_text = &visible[last_emitted_len..];
829 if !new_text.is_empty() {
830 if let Ok(mut buf) = app_state.stream_buffer.lock() {
831 buf.entry(session_id.clone()).or_default().push(new_text.to_string());
832 }
833 }
834 last_emitted_len = visible.len();
835 }
836 if is_json_string_complete(&assistant_buf) {
838 assistant_state = 2;
839 }
840 }
841 }
842 }
843 }
844 Ok(None) => {}
845 Err(e) => { tracing::warn!("stream transform error (label={}): {}", request_label, e); }
846 }
847 }
848 }
849 }
850
851 let visible_text = if let Ok(v) = serde_json::from_str::<serde_json::Value>(&full_reply) {
857 v.get("assistant")
858 .and_then(|a| a.as_str())
859 .map(|s| s.to_string())
860 .unwrap_or_else(|| full_reply.clone())
861 } else {
862 full_reply.clone()
863 };
864
865 if let Ok(mut buf) = app_state.stream_buffer.lock() {
867 buf.remove(&session_id);
868 let chunk_size = 20usize;
869 let chars: Vec<char> = visible_text.chars().collect();
870 for chunk in chars.chunks(chunk_size) {
871 buf.entry(session_id.clone()).or_default().push(chunk.iter().collect());
872 }
873 }
874
875 let _ = tx.send(Ok(full_reply));
876 });
877
878 let result = match rx.recv_timeout(std::time::Duration::from_secs(180)) {
882 Ok(r) => r,
883 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
884 tracing::warn!(
885 "host_chat_completion_stream timed out after 180s (no response from stream task)"
886 );
887 Err("stream completion timed out after 180s".to_string())
888 }
889 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
890 Err("stream channel closed unexpectedly".to_string())
891 }
892 };
893
894 match result {
895 Ok(reply) => Ok(serde_json::json!({
896 "choices": [{"message": {"role": "assistant", "content": reply}, "finish_reason": "stop"}],
897 "object": "chat.completion",
898 }).to_string()),
899 Err(e) => Ok(serde_json::json!({"error": e}).to_string()),
900 }
901});
902
903host_fn!(pub host_now_ms(user_data: WasmHostState; input: String) -> String {
904 let now_ms = SystemTime::now()
905 .duration_since(UNIX_EPOCH)
906 .unwrap_or_default()
907 .as_millis() as u64;
908 Ok(now_ms.to_string())
909});
910
911host_fn!(pub host_process_spawn(user_data: WasmHostState; config_json: String) -> String {
912 gate_permission!(user_data, Permission::Process);
913 let ud = user_data.get()?;
914 let ud = ud.lock().unwrap();
915 let parsed: serde_json::Value = serde_json::from_str(&config_json).unwrap_or_default();
916 let name = parsed["name"].as_str().unwrap_or("").to_string();
917 if name.is_empty() {
918 return Ok(r#"{"error":"missing name"}"#.to_string());
919 }
920
921 let svc_config = crate::config::ServiceConfig {
922 name: name.clone(),
923 command: parsed["command"].as_str().unwrap_or("").to_string(),
924 args: parsed["args"]
925 .as_array()
926 .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
927 .unwrap_or_default(),
928 workdir: parsed["workdir"].as_str().map(String::from),
929 env: parsed["env"]
930 .as_object()
931 .map(|o| o.iter().map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())).collect())
932 .unwrap_or_default(),
933 health_url: parsed["health_url"].as_str().map(String::from),
934 health_interval: 10,
935 auto_start: false,
936 restart_on_crash: parsed["restart_on_crash"].as_bool().unwrap_or(false),
937 };
938
939 if ud.process_manager.status_sync(&name).is_none() {
940 ud.process_manager.register_sync(svc_config);
941 }
942 match ud.process_manager.start_sync(&name) {
943 Ok(()) => Ok(format!(r#"{{"status":"ok","name":"{}"}}"#, name)),
944 Err(e) => Ok(format!(r#"{{"error":"{}"}}"#, e)),
945 }
946});
947
948host_fn!(pub host_process_stop(user_data: WasmHostState; name: String) -> String {
949 gate_permission!(user_data, Permission::Process);
950 let ud = user_data.get()?;
951 let ud = ud.lock().unwrap();
952 match ud.process_manager.stop_sync(&name) {
953 Ok(()) => Ok(format!(r#"{{"status":"ok","name":"{}"}}"#, name)),
954 Err(e) => Ok(format!(r#"{{"error":"{}"}}"#, e)),
955 }
956});
957
958host_fn!(pub host_process_status(user_data: WasmHostState; name: String) -> String {
959 gate_permission!(user_data, Permission::Process);
960 let ud = user_data.get()?;
961 let ud = ud.lock().unwrap();
962 match ud.process_manager.status_sync(&name) {
963 Some(s) => Ok(format!(r#"{{"status":"{}"}}"#, s)),
964 None => Ok(r#"{"status":"not_registered"}"#.to_string()),
965 }
966});
967
968host_fn!(pub host_process_write_stdin(user_data: WasmHostState; input: String) -> String {
969 gate_permission!(user_data, Permission::Process);
970 let parsed: serde_json::Value = serde_json::from_str(&input).unwrap_or_default();
971 let name = parsed["name"].as_str().unwrap_or("");
972 let data = parsed["input"].as_str().unwrap_or("");
973 if name.is_empty() {
974 return Ok(r#"{"error":"missing name"}"#.to_string());
975 }
976 let ud = user_data.get()?;
977 let ud = ud.lock().unwrap();
978 match ud.process_manager.write_stdin_sync(name, data) {
979 Ok(()) => Ok(serde_json::json!({"status":"ok","name":name}).to_string()),
980 Err(e) => Ok(serde_json::json!({"error":e.to_string()}).to_string()),
981 }
982});
983
984host_fn!(pub host_process_read_stdout(user_data: WasmHostState; input: String) -> String {
985 gate_permission!(user_data, Permission::Process);
986 let parsed: serde_json::Value = serde_json::from_str(&input).unwrap_or_default();
987 let name = parsed["name"].as_str().unwrap_or("");
988 let offset = parsed["offset"].as_u64().unwrap_or(0) as usize;
989 if name.is_empty() {
990 return Ok(r#"{"error":"missing name"}"#.to_string());
991 }
992 let ud = user_data.get()?;
993 let ud = ud.lock().unwrap();
994 match ud.process_manager.read_stdout_since_sync(name, offset) {
995 Ok((next_offset, chunk)) => Ok(serde_json::json!({
996 "status":"ok",
997 "name": name,
998 "next_offset": next_offset,
999 "chunk": chunk,
1000 }).to_string()),
1001 Err(e) => Ok(serde_json::json!({"error":e.to_string()}).to_string()),
1002 }
1003});
1004
1005fn execute_js_extension_runtime_service(
1006 envelope: &serde_json::Value,
1007 ud: &WasmHostState,
1008) -> serde_json::Value {
1009 let Some(service_config) = ud
1010 .process_manager
1011 .service_config_sync("js-extension-runtime")
1012 else {
1013 return serde_json::json!({"error":"service package 'js-extension-runtime' is not registered"});
1014 };
1015 let Some(health_url) = service_config.health_url.as_deref() else {
1016 return serde_json::json!({"error":"service package 'js-extension-runtime' has no health url"});
1017 };
1018 let base_url = health_url.trim_end_matches("/health");
1019 let action = envelope
1020 .get("action")
1021 .and_then(|value| value.as_str())
1022 .unwrap_or("");
1023 let data = envelope
1024 .get("data")
1025 .cloned()
1026 .unwrap_or_else(|| serde_json::json!({}));
1027 let request_body = serde_json::json!({
1028 "id": "weft-tool-executor",
1029 "action": action,
1030 "payload": data,
1031 });
1032 let request_body = match serde_json::to_vec(&request_body) {
1033 Ok(body) => body,
1034 Err(error) => {
1035 return serde_json::json!({"error": format!("failed to serialize service request: {}", error)})
1036 }
1037 };
1038 let request_url = format!("{}/execute", base_url);
1039 let result = std::thread::spawn(move || -> serde_json::Value {
1040 let parsed_url = match reqwest::Url::parse(&request_url) {
1041 Ok(url) => url,
1042 Err(error) => return serde_json::json!({"error": format!("invalid service url: {}", error)}),
1043 };
1044 let host = match parsed_url.host_str() {
1045 Some(host) => host.to_string(),
1046 None => return serde_json::json!({"error":"service url has no host"}),
1047 };
1048 let port = parsed_url.port_or_known_default().unwrap_or(80);
1049 let path = if parsed_url.path().is_empty() {
1050 "/"
1051 } else {
1052 parsed_url.path()
1053 };
1054 let mut stream = match std::net::TcpStream::connect((host.as_str(), port)) {
1055 Ok(stream) => stream,
1056 Err(error) => return serde_json::json!({"error": format!("service connect failed: {}", error)}),
1057 };
1058 let _ = stream.set_read_timeout(Some(Duration::from_secs(30)));
1059 let _ = stream.set_write_timeout(Some(Duration::from_secs(30)));
1060 let request = format!(
1061 "POST {} HTTP/1.1\r\nHost: {}:{}\r\nContent-Type: application/json\r\nAccept: application/json\r\nConnection: close\r\nContent-Length: {}\r\n\r\n",
1062 path,
1063 host,
1064 port,
1065 request_body.len()
1066 );
1067 if let Err(error) = stream.write_all(request.as_bytes()).and_then(|_| stream.write_all(&request_body)) {
1068 return serde_json::json!({"error": format!("service write failed: {}", error)});
1069 }
1070 let mut response = Vec::new();
1071 if let Err(error) = stream.read_to_end(&mut response) {
1072 return serde_json::json!({"error": format!("service read failed: {}", error)});
1073 }
1074 let raw = String::from_utf8_lossy(&response);
1075 let body = raw.split("\r\n\r\n").nth(1).unwrap_or("").trim();
1076 serde_json::from_str(body).unwrap_or_else(|error| {
1077 serde_json::json!({"error": format!("service returned invalid JSON: {}", error)})
1078 })
1079 })
1080 .join()
1081 .unwrap_or_else(|_| serde_json::json!({"error":"service request thread panicked"}));
1082 result
1083}
1084
1085const MAX_CALL_DEPTH: u32 = 8;
1087
1088host_fn!(pub host_call_package(user_data: WasmHostState; input: String) -> String {
1089 let parsed: serde_json::Value = serde_json::from_str(&input).unwrap_or_default();
1091 let target = parsed["package"].as_str().or_else(|| parsed["plugin"].as_str()).unwrap_or("");
1092 let func = parsed["func"].as_str().unwrap_or("");
1093 let args = parsed["args"].as_str().unwrap_or("");
1094
1095 if target.is_empty() || func.is_empty() {
1096 return Ok(r#"{"error":"missing package or func"}"#.to_string());
1097 }
1098
1099 let ud = user_data.get()?;
1100 let ud = ud.lock().unwrap();
1101 let resolved_target = resolve_loaded_package_name(&ud, target);
1102 let display_target = if resolved_target.is_empty() { target } else { &resolved_target };
1103
1104 {
1106 let mut depth = ud.call_depth.lock().unwrap();
1107 if *depth >= MAX_CALL_DEPTH {
1108 return Ok(format!(
1109 r#"{{"error":"max cross-package call depth ({}) exceeded: {} -> {}::{}"}}"#,
1110 MAX_CALL_DEPTH, ud.caller_package_name, display_target, func
1111 ));
1112 }
1113 *depth += 1;
1114 }
1115
1116 let plugin_arc = {
1118 let map = ud.package_map.lock().unwrap();
1119 match map.get(display_target) {
1120 Some(p) => p.clone(),
1121 None => {
1122 if func == "handle_ws_message" {
1123 if let Ok(envelope) = serde_json::from_str::<serde_json::Value>(args) {
1124 if envelope.get("action").and_then(|value| value.as_str()).is_some() {
1125 if let Some(app_state) = ud.app_state.lock().ok().and_then(|state| state.clone()) {
1126 let package_name = display_target.to_string();
1127 let result = ud.runtime_handle.block_on(async move {
1128 crate::api::package_ws::dispatch_package_payload(&package_name, envelope, &app_state).await
1129 });
1130 *ud.call_depth.lock().unwrap() -= 1;
1131 return Ok(serde_json::to_string(&result).unwrap_or_else(|error| {
1132 format!(r#"{{\"error\":\"{}\"}}"#, error)
1133 }));
1134 }
1135 }
1136 }
1137 }
1138 *ud.call_depth.lock().unwrap() -= 1;
1140 return Ok(format!(
1141 r#"{{"error":"host_call_package missing target '{}' for func '{}'"}}"#,
1142 display_target, func
1143 ));
1144 }
1145 }
1146 };
1147
1148 let result = {
1150 let mut package = plugin_arc.lock().map_err(|e| {
1151 *ud.call_depth.lock().unwrap() -= 1;
1152 extism::Error::msg(format!("package '{}' lock poisoned: {}", display_target, e))
1153 })?;
1154 package.call::<&str, &str>(func, args)
1155 .map(|s| s.to_string())
1156 .map_err(|e| format!("{}", e))
1157 };
1158
1159 *ud.call_depth.lock().unwrap() -= 1;
1161
1162 match result {
1163 Ok(s) => Ok(s),
1164 Err(e) => Ok(format!(r#"{{"error":"{}"}}"#, e.replace('"', "\\\""))),
1165 }
1166});
1167
1168host_fn!(pub host_call_package_ws(user_data: WasmHostState; input: String) -> String {
1169 let parsed: serde_json::Value = serde_json::from_str(&input).unwrap_or_default();
1170 let target = parsed["package"].as_str().or_else(|| parsed["plugin"].as_str()).unwrap_or("");
1171 let action = parsed["action"].as_str().unwrap_or("");
1172 let data = parsed["data"].clone();
1173
1174 if target.is_empty() || action.is_empty() {
1175 return Ok(r#"{"error":"missing package or action"}"#.to_string());
1176 }
1177
1178 let ud = user_data.get()?;
1179 let ud = ud.lock().unwrap();
1180 let resolved_target = resolve_loaded_package_name(&ud, target);
1181 let display_target = if resolved_target.is_empty() { target } else { &resolved_target };
1182
1183 {
1184 let mut depth = ud.call_depth.lock().unwrap();
1185 if *depth >= MAX_CALL_DEPTH {
1186 return Ok(format!(
1187 r#"{{"error":"max cross-package call depth ({}) exceeded: {} -> {}::handle_ws_message"}}"#,
1188 MAX_CALL_DEPTH, ud.caller_package_name, display_target
1189 ));
1190 }
1191 *depth += 1;
1192 }
1193
1194 let plugin_arc = {
1195 let map = ud.package_map.lock().unwrap();
1196 match map.get(display_target) {
1197 Some(package) => package.clone(),
1198 None => {
1199 let has_app_state = ud
1200 .app_state
1201 .lock()
1202 .map(|state| state.is_some())
1203 .unwrap_or(false);
1204 tracing::warn!(
1205 "host_call_package_ws target '{}' is not a loaded WASM package; app_state_available={}",
1206 display_target,
1207 has_app_state
1208 );
1209 if let Some(app_state) = ud.app_state.lock().ok().and_then(|state| state.clone()) {
1210 let package_name = display_target.to_string();
1211 let envelope = serde_json::json!({
1212 "action": action,
1213 "data": data,
1214 });
1215 let result = ud.runtime_handle.block_on(async move {
1216 crate::api::package_ws::dispatch_package_payload(&package_name, envelope, &app_state).await
1217 });
1218 *ud.call_depth.lock().unwrap() -= 1;
1219 return Ok(serde_json::to_string(&result).unwrap_or_else(|error| {
1220 format!(r#"{{"error":"{}"}}"#, error)
1221 }));
1222 }
1223 *ud.call_depth.lock().unwrap() -= 1;
1224 return Ok(format!(r#"{{"error":"package '{}' not loaded"}}"#, display_target));
1225 }
1226 }
1227 };
1228
1229 let envelope = serde_json::json!({
1230 "action": action,
1231 "data": data,
1232 })
1233 .to_string();
1234
1235 let result = {
1236 let mut package = plugin_arc.lock().map_err(|e| {
1237 *ud.call_depth.lock().unwrap() -= 1;
1238 extism::Error::msg(format!("package '{}' lock poisoned: {}", display_target, e))
1239 })?;
1240 package
1241 .call::<&str, &str>("handle_ws_message", &envelope)
1242 .map(|s| s.to_string())
1243 .map_err(|e| format!("{}", e))
1244 };
1245
1246 *ud.call_depth.lock().unwrap() -= 1;
1247
1248 match result {
1249 Ok(s) => Ok(s),
1250 Err(e) => Ok(format!(r#"{{"error":"{}"}}"#, e.replace('"', "\\\""))),
1251 }
1252});
1253
1254host_fn!(pub host_capability_call(user_data: WasmHostState; input: String) -> String {
1255 let parsed: serde_json::Value = serde_json::from_str(&input).unwrap_or_default();
1256 let capability = parsed["capability"].as_str().unwrap_or("");
1257 let action = parsed["action"].as_str().unwrap_or("call");
1258 let data = parsed["data"].clone();
1259 let app = parsed["app"].as_str().map(|s| s.to_string());
1260 let provider = parsed["provider"].as_str().map(|s| s.to_string());
1261
1262 if capability.is_empty() {
1263 return Ok(r#"{"error":"missing capability"}"#.to_string());
1264 }
1265
1266 let ud = user_data.get()?;
1267 let ud = ud.lock().unwrap();
1268 let Some(app_state) = ud.app_state.lock().ok().and_then(|state| state.clone()) else {
1269 return Ok(r#"{"error":"app state unavailable for capability routing"}"#.to_string());
1270 };
1271 let runtime_handle = ud.runtime_handle.clone();
1277 drop(ud);
1278
1279 let capability_name = capability.to_string();
1280 let payload = serde_json::json!({
1281 "action": action,
1282 "data": data,
1283 "app": app,
1284 "provider": provider,
1285 });
1286
1287 let result = runtime_handle.block_on(async move {
1288 crate::api::capabilities::execute_capability_call(&app_state, &capability_name, payload).await
1289 });
1290
1291 match result {
1292 Ok(value) => Ok(value.to_string()),
1293 Err((status, value)) => Ok(serde_json::json!({
1294 "error": value,
1295 "status": status.as_u16(),
1296 }).to_string()),
1297 }
1298});
1299
1300#[derive(Debug, Clone)]
1301struct JsonSqlParam(serde_json::Value);
1302
1303impl ToSql for JsonSqlParam {
1304 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
1305 let output = match &self.0 {
1306 serde_json::Value::Null => ToSqlOutput::Owned(rusqlite::types::Value::Null),
1307 serde_json::Value::Bool(value) => {
1308 ToSqlOutput::Owned(rusqlite::types::Value::Integer(if *value { 1 } else { 0 }))
1309 }
1310 serde_json::Value::Number(number) => {
1311 if let Some(value) = number.as_i64() {
1312 ToSqlOutput::Owned(rusqlite::types::Value::Integer(value))
1313 } else if let Some(value) = number.as_u64() {
1314 match i64::try_from(value) {
1315 Ok(integer) => ToSqlOutput::Owned(rusqlite::types::Value::Integer(integer)),
1316 Err(_) => {
1317 ToSqlOutput::Owned(rusqlite::types::Value::Text(value.to_string()))
1318 }
1319 }
1320 } else if let Some(value) = number.as_f64() {
1321 ToSqlOutput::Owned(rusqlite::types::Value::Real(value))
1322 } else {
1323 ToSqlOutput::Owned(rusqlite::types::Value::Text(number.to_string()))
1324 }
1325 }
1326 serde_json::Value::String(value) => {
1327 ToSqlOutput::Owned(rusqlite::types::Value::Text(value.clone()))
1328 }
1329 other => ToSqlOutput::Owned(rusqlite::types::Value::Text(other.to_string())),
1330 };
1331 Ok(output)
1332 }
1333}
1334
1335fn open_sqlite_connection(path: &str) -> rusqlite::Result<Connection> {
1336 if let Some(parent) = Path::new(path).parent() {
1337 let _ = std::fs::create_dir_all(parent);
1338 }
1339 Connection::open_with_flags(
1340 path,
1341 OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
1342 )
1343}
1344
1345fn json_from_sql_value(value: ValueRef<'_>) -> serde_json::Value {
1346 match value {
1347 ValueRef::Null => serde_json::Value::Null,
1348 ValueRef::Integer(v) => serde_json::json!(v),
1349 ValueRef::Real(v) => serde_json::json!(v),
1350 ValueRef::Text(v) => serde_json::Value::String(String::from_utf8_lossy(v).to_string()),
1351 ValueRef::Blob(v) => {
1352 serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(v))
1353 }
1354 }
1355}
1356
1357host_fn!(pub host_sqlite_query(user_data: WasmHostState; input: String) -> String {
1358 gate_permission!(user_data, Permission::Storage);
1359 let parsed: serde_json::Value = serde_json::from_str(&input).unwrap_or_default();
1360 let path = parsed["path"].as_str().unwrap_or("");
1361 let sql = parsed["sql"].as_str().unwrap_or("");
1362 let params: Vec<JsonSqlParam> = parsed["params"]
1363 .as_array()
1364 .cloned()
1365 .unwrap_or_default()
1366 .into_iter()
1367 .map(JsonSqlParam)
1368 .collect();
1369 let refs: Vec<&dyn ToSql> = params.iter().map(|item| item as &dyn ToSql).collect();
1370
1371 let result = (|| -> rusqlite::Result<String> {
1372 let connection = open_sqlite_connection(path)?;
1373 let mut statement = connection.prepare(sql)?;
1374 let column_count = statement.column_count();
1375 let columns = statement
1376 .column_names()
1377 .into_iter()
1378 .map(|name| name.to_string())
1379 .collect::<Vec<_>>();
1380 let mut rows = statement.query(refs.as_slice())?;
1381 let mut result_rows = Vec::new();
1382 while let Some(row) = rows.next()? {
1383 let mut values = Vec::with_capacity(column_count);
1384 for index in 0..column_count {
1385 values.push(json_from_sql_value(row.get_ref(index)?));
1386 }
1387 result_rows.push(values);
1388 }
1389 Ok(serde_json::json!({ "columns": columns, "rows": result_rows }).to_string())
1390 })();
1391
1392 match result {
1393 Ok(output) => Ok(output),
1394 Err(error) => Ok(format!(r#"{{"error":"{}"}}"#, error.to_string().replace('"', "\\\""))),
1395 }
1396});
1397
1398host_fn!(pub host_sqlite_execute(user_data: WasmHostState; input: String) -> String {
1399 gate_permission!(user_data, Permission::Storage);
1400 let parsed: serde_json::Value = serde_json::from_str(&input).unwrap_or_default();
1401 let path = parsed["path"].as_str().unwrap_or("");
1402 let sql = parsed["sql"].as_str().unwrap_or("");
1403 let params: Vec<JsonSqlParam> = parsed["params"]
1404 .as_array()
1405 .cloned()
1406 .unwrap_or_default()
1407 .into_iter()
1408 .map(JsonSqlParam)
1409 .collect();
1410 let refs: Vec<&dyn ToSql> = params.iter().map(|item| item as &dyn ToSql).collect();
1411
1412 let result = (|| -> rusqlite::Result<String> {
1413 let connection = open_sqlite_connection(path)?;
1414 let affected = connection.execute(sql, refs.as_slice())?;
1415 Ok(serde_json::json!({ "rows_affected": affected }).to_string())
1416 })();
1417
1418 match result {
1419 Ok(output) => Ok(output),
1420 Err(error) => Ok(format!(r#"{{"error":"{}"}}"#, error.to_string().replace('"', "\\\""))),
1421 }
1422});
1423
1424host_fn!(pub host_sqlite_batch(user_data: WasmHostState; input: String) -> String {
1425 gate_permission!(user_data, Permission::Storage);
1426 let parsed: serde_json::Value = serde_json::from_str(&input).unwrap_or_default();
1427 let path = parsed["path"].as_str().unwrap_or("");
1428 let statements = parsed["statements"].as_array().cloned().unwrap_or_default();
1429
1430 let result = (|| -> rusqlite::Result<String> {
1431 let mut connection = open_sqlite_connection(path)?;
1432 let transaction = connection.transaction()?;
1433 let mut total_rows = 0_u64;
1434 for statement in statements {
1435 let sql = statement["sql"].as_str().unwrap_or("");
1436 let params: Vec<JsonSqlParam> = statement["params"]
1437 .as_array()
1438 .cloned()
1439 .unwrap_or_default()
1440 .into_iter()
1441 .map(JsonSqlParam)
1442 .collect();
1443 let refs: Vec<&dyn ToSql> = params.iter().map(|item| item as &dyn ToSql).collect();
1444 total_rows += transaction.execute(sql, refs.as_slice())? as u64;
1445 }
1446 transaction.commit()?;
1447 Ok(serde_json::json!({ "rows_affected": total_rows }).to_string())
1448 })();
1449
1450 match result {
1451 Ok(output) => Ok(output),
1452 Err(error) => Ok(format!(r#"{{"error":"{}"}}"#, error.to_string().replace('"', "\\\""))),
1453 }
1454});
1455
1456#[derive(Debug, Clone)]
1460pub struct PackageLoadInfo {
1461 pub name: String,
1462 pub dir: PathBuf,
1463 pub wasm_path: PathBuf,
1464 pub startup_mode: WasmStartupMode,
1465 pub permissions: PackagePermissions,
1466}
1467
1468#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1469pub enum WasmStartupMode {
1470 Persistent,
1471 OnDemand,
1472}
1473
1474pub struct WasmPackageHost {
1476 package_map: PackageMap,
1478 load_infos: Arc<StdMutex<HashMap<String, PackageLoadInfo>>>,
1479 base_state: WasmHostState,
1481 breaker: CircuitBreaker,
1484}
1485
1486impl WasmPackageHost {
1487 pub fn new(load_infos: &[PackageLoadInfo], host_state: WasmHostState) -> Self {
1489 let package_map: PackageMap = Arc::new(StdMutex::new(HashMap::new()));
1490 let load_infos_map = Arc::new(StdMutex::new(
1491 load_infos
1492 .iter()
1493 .cloned()
1494 .map(|info| (info.name.clone(), info))
1495 .collect::<HashMap<_, _>>(),
1496 ));
1497
1498 let base_state = WasmHostState {
1500 package_map: package_map.clone(),
1501 ..host_state
1502 };
1503
1504 let built: Vec<(String, Result<Plugin>)> = if load_infos.len() <= 1 {
1513 load_infos
1514 .iter()
1515 .map(|info| (info.name.clone(), Self::build_plugin(info, &base_state)))
1516 .collect()
1517 } else {
1518 let worker_count = std::thread::available_parallelism()
1519 .map(|n| n.get())
1520 .unwrap_or(1)
1521 .min(load_infos.len());
1522 let base_state_ref = &base_state;
1523 std::thread::scope(|scope| {
1524 let handles: Vec<_> = (0..worker_count)
1525 .map(|worker_idx| {
1526 let chunk: Vec<&PackageLoadInfo> = load_infos
1528 .iter()
1529 .skip(worker_idx)
1530 .step_by(worker_count)
1531 .collect();
1532 scope.spawn(move || {
1533 chunk
1534 .into_iter()
1535 .map(|info| {
1536 (info.name.clone(), Self::build_plugin(info, base_state_ref))
1537 })
1538 .collect::<Vec<(String, Result<Plugin>)>>()
1539 })
1540 })
1541 .collect();
1542 handles
1543 .into_iter()
1544 .flat_map(|h| h.join().expect("wasm build worker panicked"))
1545 .collect()
1546 })
1547 };
1548
1549 for (name, result) in built {
1550 match result {
1551 Ok(package) => {
1552 tracing::info!("Loaded WASM package '{}'", name);
1553 package_map
1554 .lock()
1555 .unwrap()
1556 .insert(name, Arc::new(StdMutex::new(package)));
1557 }
1558 Err(e) => {
1559 tracing::error!("Failed to load WASM package '{}': {}", name, e);
1560 }
1561 }
1562 }
1563
1564 Self {
1565 package_map,
1566 load_infos: load_infos_map,
1567 base_state,
1568 breaker: CircuitBreaker::default(),
1569 }
1570 }
1571
1572 fn build_user_data(info: &PackageLoadInfo, base_state: &WasmHostState) -> UserData<WasmHostState> {
1573 let per_plugin_state = WasmHostState {
1574 caller_package_name: info.name.clone(),
1575 package_dir: normalize_package_dir(&info.dir),
1576 permissions: info.permissions.clone(),
1577 ..base_state.clone()
1578 };
1579 UserData::new(per_plugin_state)
1580 }
1581
1582 fn build_plugin(info: &PackageLoadInfo, base_state: &WasmHostState) -> Result<Plugin> {
1583 let user_data = Self::build_user_data(info, base_state);
1584 Self::load_one(&info.wasm_path, &info.name, user_data)
1585 }
1586
1587 fn isolated_load_info(&self, package_name: &str) -> Result<PackageLoadInfo> {
1588 self.load_infos
1589 .lock()
1590 .map_err(|e| anyhow::anyhow!("Package load info lock poisoned: {}", e))?
1591 .get(package_name)
1592 .cloned()
1593 .ok_or_else(|| anyhow::anyhow!("Package '{}' not loaded", package_name))
1594 }
1595
1596 pub fn call_isolated(&self, package_name: &str, func: &str, input: &str) -> Result<String> {
1597 if let Err(reason) = self.breaker.check(package_name) {
1598 anyhow::bail!("{reason}");
1599 }
1600
1601 let info = self.isolated_load_info(package_name)?;
1602 let mut package = Self::build_plugin(&info, &self.base_state)?;
1603 let outcome = package.call::<&str, &str>(func, input);
1604 self.breaker.record(package_name, outcome.is_ok());
1605 let result = outcome.map_err(|e| {
1606 anyhow::anyhow!("Package '{}' isolated call '{}' failed: {}", package_name, func, e)
1607 })?;
1608 Ok(result.to_string())
1609 }
1610
1611 fn load_one(
1612 wasm_path: &Path,
1613 name: &str,
1614 user_data: UserData<WasmHostState>,
1615 ) -> Result<Plugin> {
1616 let wasm = Wasm::file(wasm_path);
1617 let manifest = Manifest::new([wasm]).with_allowed_hosts(["*".to_string()].into_iter());
1618
1619 let package = PluginBuilder::new(manifest)
1620 .with_wasi(true)
1621 .with_function("host_log", [PTR], [PTR], user_data.clone(), host_log)
1622 .with_function("host_kv_get", [PTR], [PTR], user_data.clone(), host_kv_get)
1623 .with_function("host_kv_set", [PTR], [], user_data.clone(), host_kv_set)
1624 .with_function(
1625 "host_kv_list",
1626 [PTR],
1627 [PTR],
1628 user_data.clone(),
1629 host_kv_list,
1630 )
1631 .with_function(
1632 "host_kv_delete",
1633 [PTR],
1634 [],
1635 user_data.clone(),
1636 host_kv_delete,
1637 )
1638 .with_function(
1639 "host_env_get",
1640 [PTR],
1641 [PTR],
1642 user_data.clone(),
1643 host_env_get,
1644 )
1645 .with_function(
1646 "host_home_dir",
1647 [PTR],
1648 [PTR],
1649 user_data.clone(),
1650 host_home_dir,
1651 )
1652 .with_function(
1653 "host_read_file",
1654 [PTR],
1655 [PTR],
1656 user_data.clone(),
1657 host_read_file,
1658 )
1659 .with_function(
1660 "host_write_file",
1661 [PTR],
1662 [],
1663 user_data.clone(),
1664 host_write_file,
1665 )
1666 .with_function(
1667 "host_write_file_base64",
1668 [PTR],
1669 [PTR],
1670 user_data.clone(),
1671 host_write_file_base64,
1672 )
1673 .with_function(
1674 "host_list_dir",
1675 [PTR],
1676 [PTR],
1677 user_data.clone(),
1678 host_list_dir,
1679 )
1680 .with_function("host_exec", [PTR], [PTR], user_data.clone(), host_exec)
1681 .with_function(
1682 "host_exec_advanced",
1683 [PTR],
1684 [PTR],
1685 user_data.clone(),
1686 host_exec_advanced,
1687 )
1688 .with_function(
1689 "host_chat_completion",
1690 [PTR],
1691 [PTR],
1692 user_data.clone(),
1693 host_chat_completion,
1694 )
1695 .with_function(
1696 "host_http_request",
1697 [PTR],
1698 [PTR],
1699 user_data.clone(),
1700 host_http_request,
1701 )
1702 .with_function(
1703 "host_chat_completion_stream",
1704 [PTR],
1705 [PTR],
1706 user_data.clone(),
1707 host_chat_completion_stream,
1708 )
1709 .with_function("host_now_ms", [PTR], [PTR], user_data.clone(), host_now_ms)
1710 .with_function(
1711 "host_call_package",
1712 [PTR],
1713 [PTR],
1714 user_data.clone(),
1715 host_call_package,
1716 )
1717 .with_function(
1718 "host_call_package_ws",
1719 [PTR],
1720 [PTR],
1721 user_data.clone(),
1722 host_call_package_ws,
1723 )
1724 .with_function(
1725 "host_call_plugin",
1726 [PTR],
1727 [PTR],
1728 user_data.clone(),
1729 host_call_package,
1730 )
1731 .with_function(
1732 "host_capability_call",
1733 [PTR],
1734 [PTR],
1735 user_data.clone(),
1736 host_capability_call,
1737 )
1738 .with_function(
1739 "host_process_spawn",
1740 [PTR],
1741 [PTR],
1742 user_data.clone(),
1743 host_process_spawn,
1744 )
1745 .with_function(
1746 "host_process_stop",
1747 [PTR],
1748 [PTR],
1749 user_data.clone(),
1750 host_process_stop,
1751 )
1752 .with_function(
1753 "host_process_status",
1754 [PTR],
1755 [PTR],
1756 user_data.clone(),
1757 host_process_status,
1758 )
1759 .with_function(
1760 "host_process_write_stdin",
1761 [PTR],
1762 [PTR],
1763 user_data.clone(),
1764 host_process_write_stdin,
1765 )
1766 .with_function(
1767 "host_process_read_stdout",
1768 [PTR],
1769 [PTR],
1770 user_data.clone(),
1771 host_process_read_stdout,
1772 )
1773 .with_function(
1774 "host_sqlite_query",
1775 [PTR],
1776 [PTR],
1777 user_data.clone(),
1778 host_sqlite_query,
1779 )
1780 .with_function(
1781 "host_sqlite_execute",
1782 [PTR],
1783 [PTR],
1784 user_data.clone(),
1785 host_sqlite_execute,
1786 )
1787 .with_function(
1788 "host_sqlite_batch",
1789 [PTR],
1790 [PTR],
1791 user_data.clone(),
1792 host_sqlite_batch,
1793 )
1794 .build()
1795 .map_err(|e| anyhow::anyhow!("Failed to build package '{}': {}", name, e))?;
1796
1797 Ok(package)
1798 }
1799
1800 pub fn load_package(&mut self, info: &PackageLoadInfo) -> Result<()> {
1802 {
1803 let map = self.package_map.lock().unwrap();
1804 if map.contains_key(&info.name) {
1805 anyhow::bail!("Package '{}' is already loaded", info.name);
1806 }
1807 }
1808
1809 let user_data = Self::build_user_data(info, &self.base_state);
1810 let package = Self::load_one(&info.wasm_path, &info.name, user_data)?;
1811 self.package_map
1812 .lock()
1813 .unwrap()
1814 .insert(info.name.clone(), Arc::new(StdMutex::new(package)));
1815 tracing::info!("Hot-loaded WASM package '{}'", info.name);
1816
1817 if let Err(e) = self.call(&info.name, "init", "") {
1819 tracing::warn!(
1820 "Package '{}' init() failed (may not export it): {}",
1821 info.name,
1822 e
1823 );
1824 }
1825 Ok(())
1826 }
1827
1828 pub fn unload_package(&mut self, name: &str) -> Result<()> {
1830 self.package_map
1831 .lock()
1832 .unwrap()
1833 .remove(name)
1834 .ok_or_else(|| anyhow::anyhow!("Package '{}' not loaded", name))?;
1835 self.breaker.reset(name);
1837 tracing::info!("Unloaded WASM package '{}'", name);
1838 Ok(())
1839 }
1840
1841 pub fn reload_package(&mut self, info: &PackageLoadInfo) -> Result<()> {
1843 let _ = self.unload_package(&info.name);
1845 let user_data = Self::build_user_data(info, &self.base_state);
1847 let package = Self::load_one(&info.wasm_path, &info.name, user_data)?;
1848 self.package_map
1849 .lock()
1850 .unwrap()
1851 .insert(info.name.clone(), Arc::new(StdMutex::new(package)));
1852 tracing::info!("Reloaded WASM package '{}'", info.name);
1853
1854 if let Err(e) = self.call(&info.name, "init", "") {
1855 tracing::warn!(
1856 "Package '{}' init() failed (may not export it): {}",
1857 info.name,
1858 e
1859 );
1860 }
1861 Ok(())
1862 }
1863
1864 pub fn call(&self, package_name: &str, func: &str, input: &str) -> Result<String> {
1866 if let Err(reason) = self.breaker.check(package_name) {
1868 anyhow::bail!("{reason}");
1869 }
1870 let plugin_arc = {
1871 let map = self.package_map.lock().unwrap();
1872 map.get(package_name)
1873 .cloned()
1874 .ok_or_else(|| anyhow::anyhow!("Package '{}' not loaded", package_name))?
1875 };
1876 let mut package = plugin_arc
1877 .lock()
1878 .map_err(|e| anyhow::anyhow!("Package '{}' lock poisoned: {}", package_name, e))?;
1879 let outcome = package.call::<&str, &str>(func, input);
1880 self.breaker.record(package_name, outcome.is_ok());
1883 let result = outcome.map_err(|e| {
1884 anyhow::anyhow!("Package '{}' call '{}' failed: {}", package_name, func, e)
1885 })?;
1886 Ok(result.to_string())
1887 }
1888
1889 pub fn has_package(&self, name: &str) -> bool {
1891 self.package_map.lock().unwrap().contains_key(name)
1892 }
1893
1894 pub fn package_names(&self) -> Vec<String> {
1896 self.package_map.lock().unwrap().keys().cloned().collect()
1897 }
1898
1899 pub fn kv_set(&self, key: &str, value: &str) {
1903 if let Ok(mut store) = self.base_state.kv_store.lock() {
1904 store.insert(key.to_string(), value.to_string());
1905 }
1906 }
1907
1908 #[allow(dead_code)]
1910 pub fn kv_get(&self, key: &str) -> Option<String> {
1911 self.base_state
1912 .kv_store
1913 .lock()
1914 .ok()
1915 .and_then(|store| store.get(key).cloned())
1916 }
1917
1918 pub fn set_app_state(&mut self, app_state: crate::api::openai_compat::AppState) {
1919 if let Ok(mut state) = self.base_state.app_state.lock() {
1920 *state = Some(app_state);
1921 }
1922 }
1923}
1924
1925#[derive(Clone)]
1929pub struct WasmHandle {
1930 host: Arc<StdMutex<WasmPackageHost>>,
1931}
1932
1933impl WasmHandle {
1934 pub fn new(host: WasmPackageHost) -> Self {
1935 Self {
1936 host: Arc::new(StdMutex::new(host)),
1937 }
1938 }
1939
1940 pub fn call(&self, package_name: &str, func: &str, input: &str) -> Result<String> {
1942 let (plugin_arc, breaker) = {
1944 let host = self
1945 .host
1946 .lock()
1947 .map_err(|e| anyhow::anyhow!("WasmHandle lock poisoned: {}", e))?;
1948 let breaker = host.breaker.clone();
1949 let map = host.package_map.lock().unwrap();
1950 let arc = map
1951 .get(package_name)
1952 .cloned()
1953 .ok_or_else(|| anyhow::anyhow!("Package '{}' not loaded", package_name))?;
1954 (arc, breaker)
1955 };
1956
1957 if let Err(reason) = breaker.check(package_name) {
1959 anyhow::bail!("{reason}");
1960 }
1961
1962 let mut package = plugin_arc
1963 .lock()
1964 .map_err(|e| anyhow::anyhow!("Package '{}' lock poisoned: {}", package_name, e))?;
1965 let outcome = package.call::<&str, &str>(func, input);
1966 breaker.record(package_name, outcome.is_ok());
1967 let result = outcome.map_err(|e| {
1968 anyhow::anyhow!("Package '{}' call '{}' failed: {}", package_name, func, e)
1969 })?;
1970 Ok(result.to_string())
1971 }
1972
1973 pub fn call_isolated(&self, package_name: &str, func: &str, input: &str) -> Result<String> {
1974 let (load_infos, base_state, breaker) = {
1979 let host = self
1980 .host
1981 .lock()
1982 .map_err(|e| anyhow::anyhow!("WasmHandle lock poisoned: {}", e))?;
1983 (
1984 host.load_infos.clone(),
1985 host.base_state.clone(),
1986 host.breaker.clone(),
1987 )
1988 };
1989
1990 if let Err(reason) = breaker.check(package_name) {
1991 anyhow::bail!("{reason}");
1992 }
1993 let info = load_infos
1994 .lock()
1995 .map_err(|e| anyhow::anyhow!("Package load info lock poisoned: {}", e))?
1996 .get(package_name)
1997 .cloned()
1998 .ok_or_else(|| anyhow::anyhow!("Package '{}' not loaded", package_name))?;
1999 let mut package = WasmPackageHost::build_plugin(&info, &base_state)?;
2000 let outcome = package.call::<&str, &str>(func, input);
2001 breaker.record(package_name, outcome.is_ok());
2002 let result = outcome.map_err(|e| {
2003 anyhow::anyhow!("Package '{}' isolated call '{}' failed: {}", package_name, func, e)
2004 })?;
2005 Ok(result.to_string())
2006 }
2007
2008 pub fn has_package(&self, name: &str) -> bool {
2009 self.host
2010 .lock()
2011 .map(|h| h.has_package(name))
2012 .unwrap_or(false)
2013 }
2014
2015 pub fn package_names(&self) -> Vec<String> {
2016 self.host
2017 .lock()
2018 .map(|h| h.package_names())
2019 .unwrap_or_default()
2020 }
2021
2022 pub fn kv_set(&self, key: &str, value: &str) {
2024 if let Ok(h) = self.host.lock() {
2025 h.kv_set(key, value);
2026 }
2027 }
2028
2029 #[allow(dead_code)]
2031 pub fn kv_get(&self, key: &str) -> Option<String> {
2032 self.host.lock().ok().and_then(|h| h.kv_get(key))
2033 }
2034
2035 pub fn set_app_state(&self, app_state: crate::api::openai_compat::AppState) -> Result<()> {
2036 let mut host = self
2037 .host
2038 .lock()
2039 .map_err(|e| anyhow::anyhow!("WasmHandle lock poisoned: {}", e))?;
2040 host.set_app_state(app_state);
2041 Ok(())
2042 }
2043
2044 pub fn load_package(&self, info: &PackageLoadInfo) -> Result<()> {
2046 let mut host = self
2047 .host
2048 .lock()
2049 .map_err(|e| anyhow::anyhow!("WasmHandle lock poisoned: {}", e))?;
2050 host.load_package(info)
2051 }
2052
2053 pub fn unload_package(&self, name: &str) -> Result<()> {
2055 let mut host = self
2056 .host
2057 .lock()
2058 .map_err(|e| anyhow::anyhow!("WasmHandle lock poisoned: {}", e))?;
2059 host.unload_package(name)
2060 }
2061
2062 pub fn reload_package(&self, info: &PackageLoadInfo) -> Result<()> {
2064 let mut host = self
2065 .host
2066 .lock()
2067 .map_err(|e| anyhow::anyhow!("WasmHandle lock poisoned: {}", e))?;
2068 host.reload_package(info)
2069 }
2070}
2071
2072pub struct WasmRouterBridge {
2076 package_name: String,
2077 handle: WasmHandle,
2078}
2079
2080impl WasmRouterBridge {
2081 pub fn new(package_name: &str, handle: WasmHandle) -> Self {
2082 Self {
2083 package_name: package_name.to_string(),
2084 handle,
2085 }
2086 }
2087}
2088
2089#[async_trait]
2090impl RouterLayer for WasmRouterBridge {
2091 async fn route(&self, request: &ChatRequest, providers: &[ProviderConfig]) -> Result<String> {
2092 let input = serde_json::json!({
2093 "request": request,
2094 "providers": providers.iter().map(|p| &p.name).collect::<Vec<_>>(),
2095 });
2096 let input_str = serde_json::to_string(&input)?;
2097 let result_str = self.handle.call(&self.package_name, "route", &input_str)?;
2098 let parsed: serde_json::Value = serde_json::from_str(&result_str)?;
2099 parsed["provider"]
2100 .as_str()
2101 .map(|s| s.to_string())
2102 .ok_or_else(|| anyhow::anyhow!("WASM route() missing provider field"))
2103 }
2104}
2105
2106pub struct WasmKeySelectorBridge {
2108 package_name: String,
2109 handle: WasmHandle,
2110}
2111
2112impl WasmKeySelectorBridge {
2113 pub fn new(package_name: &str, handle: WasmHandle) -> Self {
2114 Self {
2115 package_name: package_name.to_string(),
2116 handle,
2117 }
2118 }
2119}
2120
2121#[async_trait]
2122impl KeySelectorLayer for WasmKeySelectorBridge {
2123 async fn select(&self, provider: &str, keys: &[ApiKeyState]) -> Result<usize> {
2124 let input = serde_json::json!({
2125 "provider": provider,
2126 "key_count": keys.len(),
2127 "failed": keys.iter().map(|k| k.failed).collect::<Vec<_>>(),
2128 });
2129 let input_str = serde_json::to_string(&input)?;
2130 let result_str = self
2131 .handle
2132 .call(&self.package_name, "select_key", &input_str)?;
2133 let parsed: serde_json::Value = serde_json::from_str(&result_str)?;
2134 parsed["key_index"]
2135 .as_u64()
2136 .map(|i| i as usize)
2137 .ok_or_else(|| anyhow::anyhow!("WASM select_key() missing key_index field"))
2138 }
2139
2140 fn mark_failed(&self, _provider: &str, _index: usize) {}
2141 fn mark_success(&self, _provider: &str, _index: usize) {}
2142}
2143
2144pub struct WasmErrorHandlerBridge {
2146 package_name: String,
2147 handle: WasmHandle,
2148}
2149
2150impl WasmErrorHandlerBridge {
2151 pub fn new(package_name: &str, handle: WasmHandle) -> Self {
2152 Self {
2153 package_name: package_name.to_string(),
2154 handle,
2155 }
2156 }
2157}
2158
2159#[async_trait]
2160impl ErrorHandlerLayer for WasmErrorHandlerBridge {
2161 async fn handle(&self, error: &RequestError) -> ErrorAction {
2162 let input = serde_json::json!({
2163 "status": error.status,
2164 "message": error.message,
2165 "provider": error.provider,
2166 "retry_count": error.retry_count,
2167 });
2168 let input_str = match serde_json::to_string(&input) {
2169 Ok(s) => s,
2170 Err(_) => {
2171 return ErrorAction::Fail {
2172 message: error.message.clone(),
2173 }
2174 }
2175 };
2176
2177 match self
2178 .handle
2179 .call(&self.package_name, "handle_error", &input_str)
2180 {
2181 Ok(result_str) => {
2182 let parsed: serde_json::Value =
2183 serde_json::from_str(&result_str).unwrap_or_default();
2184 match parsed["action"].as_str() {
2185 Some("retry") => ErrorAction::Retry {
2186 delay_ms: parsed["delay_ms"].as_u64().unwrap_or(1000),
2187 },
2188 Some("switch_key") => ErrorAction::SwitchKey,
2189 Some("switch_provider") => ErrorAction::SwitchProvider,
2190 _ => ErrorAction::Fail {
2191 message: parsed["message"]
2192 .as_str()
2193 .unwrap_or(&error.message)
2194 .to_string(),
2195 },
2196 }
2197 }
2198 Err(e) => {
2199 tracing::error!("WASM handle_error() failed: {}", e);
2200 ErrorAction::Fail {
2201 message: error.message.clone(),
2202 }
2203 }
2204 }
2205 }
2206}
2207
2208#[cfg(test)]
2209mod tests {
2210 use super::resolve_loaded_package_name_from_aliases;
2211 use std::collections::HashMap;
2212
2213 #[test]
2214 fn resolve_loaded_package_name_from_aliases_prefers_loaded_alias_target() {
2215 let aliases = HashMap::from([
2216 ("agent-core".to_string(), "agent-runtime".to_string()),
2217 ("skills".to_string(), "skills-runtime".to_string()),
2218 ]);
2219 let loaded_plugins = vec!["agent-runtime".to_string(), "memory-runtime".to_string()];
2220
2221 assert_eq!(
2222 resolve_loaded_package_name_from_aliases(&aliases, &loaded_plugins, "agent-core"),
2223 "agent-runtime"
2224 );
2225 assert_eq!(
2226 resolve_loaded_package_name_from_aliases(&aliases, &loaded_plugins, "skills"),
2227 "skills"
2228 );
2229 assert_eq!(
2230 resolve_loaded_package_name_from_aliases(&aliases, &loaded_plugins, "memory-runtime"),
2231 "memory-runtime"
2232 );
2233 }
2234}