Skip to main content

weft_core/package/
bridge.rs

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
28// ── Dedicated runtime for WASM host-function blocking calls ──
29//
30// WASM host functions are synchronous (extism calls them and blocks for the
31// return value), but several need to drive async work (HTTP egress, nested
32// capability dispatch). They do this via `block_on`. Running those `block_on`
33// calls on the MAIN HTTP runtime deadlocks under nesting: a skills package
34// tool calls `host_capability_call` → `block_on(execute_capability_call)`,
35// which dispatches into another package whose host fn (`host_http_request`)
36// `block_on`s a 60s request. Nested `block_on` on the same runtime starves the
37// shared worker/IO-driver pool — even the lock-free `/health` handler stops
38// responding. Isolating host blocking work on its OWN multi-threaded runtime
39// keeps the main runtime's workers free (health stays live) and gives nested
40// host calls an independent pool to make progress on.
41pub 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
56// ── Shared host state (UserData for host functions) ──
57
58/// Shared package map — allows host functions to call other plugins
59/// without going through the outer WasmPackageHost lock.
60pub type PackageMap = Arc<StdMutex<HashMap<String, Arc<StdMutex<Plugin>>>>>;
61
62/// State shared across all WASM packages via Extism host functions.
63/// Each package instance gets its own clone with a unique `caller_package_name`.
64/// Shared resources (kv_store, package_map, etc.) are behind Arc so clones share them.
65#[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    /// Name of the package that owns this UserData instance.
74    pub caller_package_name: String,
75    /// Filesystem directory the owning package was loaded from (official/ or
76    /// installed/ — whichever the index resolved). Exposed to the guest as the
77    /// `WEFT_PACKAGE_DIR` env var so package resources (skills/, mcp/, assets/)
78    /// can be located relative to the package rather than a hardcoded repo path.
79    pub package_dir: String,
80    /// Permissions declared by the owning package's manifest ([permissions] in package.toml).
81    pub permissions: PackagePermissions,
82    /// Direct access to all loaded plugins for cross-package calls.
83    pub package_map: PackageMap,
84    pub package_aliases: Arc<StdMutex<HashMap<String, String>>>,
85    /// Call depth counter to prevent infinite recursion in cross-package calls.
86    pub call_depth: Arc<StdMutex<u32>>,
87    pub app_state: Arc<StdMutex<Option<crate::api::openai_compat::AppState>>>,
88}
89
90/// Checks whether the calling package holds `perm`. On denial, returns the JSON
91/// error string `{"error":"permission denied: ..."}` so the WASM guest sees a
92/// structured error consistent with other host_fn failures.
93/// Usage inside a `host_fn!` body: `gate_permission!(user_data, Permission::Storage);`
94macro_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
114/// Unit-returning variant of `gate_permission!` for host_fn whose body returns `Ok(())`.
115macro_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
133/// Normalizes a package directory for injection as `WEFT_PACKAGE_DIR`:
134/// strips the Windows `\\?\` verbatim prefix and converts backslashes to
135/// forward slashes, so the guest can join `/skills` etc. cleanly.
136fn 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// ── Host functions ──
202
203#[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    // Windows: 隐藏 shell_exec 子进程的 console 窗口(FFI 嵌入模式下不弹黑框)。
253    #[cfg(windows)]
254    {
255        use std::os::windows::process::CommandExt;
256        command.creation_flags(0x08000000); // CREATE_NO_WINDOW
257    }
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    // input is JSON: ["level", "msg"]
353    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    // input is JSON: ["key", "value"]
374    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    // WEFT_PACKAGE_DIR: 返回该包的实际加载目录(per-package),供包定位自身资源。
404    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    // input is JSON: ["path", "content"]
442    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
455// Write binary content (decoded from base64) to a file. For large media
456// (images/audio/video) that can't go through host_write_file's String content.
457// input JSON: ["path", "<base64>"]. Output JSON: {ok:true,path} or {error}.
458host_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            // 返回绝对路径:写入用的是相对 Core CWD 的路径,但前端(独立进程,
479            // CWD 不同)需要绝对路径才能用 File() 显示。canonicalize 失败则回退原值。
480            // Windows 上 canonicalize 会加 `\\?\` 扩展长度前缀,Flutter File() 不识别,需剥掉。
481            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
584// Generic HTTP egress for WASM packages. Mirrors host_chat_completion's
585// Network permission gate, but issues an arbitrary HTTP request via reqwest
586// (supports custom headers, e.g. Authorization for external model APIs).
587// Input JSON: {method, url, headers:{k:v}, body}. Output JSON: {status, body}.
588host_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
642/// Given the raw content after `"assistant":"` in a streaming JSON response,
643/// return the decoded string up to (but not including) the first unescaped closing quote.
644fn 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
668/// Returns true if the raw content after `"assistant":"` contains a complete JSON string.
669fn 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    // Use std::sync::mpsc to bridge the async stream back to this sync WASM thread.
713    // The stream task runs on the EXISTING tokio runtime (via spawn), so reqwest's
714    // connection pool and wakers stay on the correct runtime — no cross-runtime deadlock.
715    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        // 对瞬时错误(5xx/网关 502/网络抖动)做有限重试:LLM 网关偶发 502
722        // 不应让整个对话回合失败。execute_stream 对 status>=400 会 bail,
723        // 错误信息含状态码,据此判断是否可重试(4xx 不重试)。
724        let mut attempt = 0u32;
725        const MAX_STREAM_ATTEMPTS: u32 = 3;
726        let (provider_name, resp) = loop {
727            attempt += 1;
728            // 给 execute_stream(含 .send().await 建连+首字节)加 30s 超时:
729            // 卡在首字节前不会无限挂起,超时按可重试错误处理。
730            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                    // 4xx 客户端错误(鉴权/参数)不可重试。
744                    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        // 字节级缓冲:避免多字节 UTF-8 字符(中文等)跨 chunk 边界被
763        // from_utf8_lossy 截成 �。在字节层按 \n 切行,只对完整行解码。
764        let mut buffer: Vec<u8> = Vec::new();
765        let mut full_reply = String::new();
766        // Track position inside the "assistant" JSON string value for real-time extraction.
767        // States: 0=before assistant key, 1=inside assistant value, 2=after assistant value
768        let mut assistant_state: u8 = 0;
769        let mut assistant_buf = String::new(); // accumulated raw chars inside assistant value
770        let mut last_emitted_len: usize = 0;   // how many chars of assistant_buf already emitted
771
772        loop {
773            // 流式 60s 无新 chunk → 判定卡死,中断而非无限阻塞 WASM 实例锁。
774            // (reqwest client.timeout 只管到首字节,body 逐块读取不受其约束。)
775            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, // 流正常结束
783                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                                // Real-time extraction of the assistant field value.
809                                // We scan the accumulated full_reply for the assistant key,
810                                // then emit new characters as they arrive.
811                                if assistant_state < 2 {
812                                    if assistant_state == 0 {
813                                        // Look for `"assistant":"` in full_reply
814                                        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                                        // Append new delta to assistant_buf
821                                        assistant_buf.push_str(delta);
822                                    }
823
824                                    if assistant_state == 1 {
825                                        // Find the closing unescaped quote
826                                        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                                        // Check if assistant value is complete (closing quote found)
837                                        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        // Extract the user-visible assistant text from the agent JSON plan.
852        // agent-core requires LLM to output {"mode":"reply","assistant":"...","tool_calls":[]},
853        // so raw stream deltas are JSON fragments. After the stream completes we parse the
854        // full reply, extract the assistant field, clear the raw tokens, and write the
855        // readable text in small chunks so the Flutter client sees a typewriter effect.
856        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        // Replace raw JSON tokens with readable text chunks (~20 chars each).
866        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    // Block this WASM thread until the stream completes —但有上界。
879    // 即使 spawn 的异步任务永不发送(卡在 execute_stream 的 .send() 前、panic 等),
880    // recv_timeout 也会在 180s 后返回,避免无限阻塞 WASM 实例锁、冻结整个 core。
881    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
1085/// Max cross-package call depth to prevent infinite recursion.
1086const MAX_CALL_DEPTH: u32 = 8;
1087
1088host_fn!(pub host_call_package(user_data: WasmHostState; input: String) -> String {
1089    // input: {"package":"name","func":"fn_name","args":"json_string"}
1090    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    // Check call depth
1105    {
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    // Get the target plugin's lock from the shared package map
1117    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                // Decrement depth before returning
1139                *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    // Lock only the target package (per-package lock, no outer lock needed)
1149    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    // Decrement depth
1160    *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    // Clone what the async block needs, then release the ud lock BEFORE block_on.
1272    // Holding the ud Mutex across a (possibly long, e.g. 60s HTTP) nested capability
1273    // call deadlocks: the inner package's host fns (host_http_request, etc.) try to
1274    // re-acquire this same ud lock and block forever. host_http_request (above) uses
1275    // the same drop-before-block_on pattern for exactly this reason.
1276    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// ── WasmPackageHost ──
1457
1458/// Info needed to load a WASM package.
1459#[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
1474/// Manages loaded Extism WASM packages.
1475pub struct WasmPackageHost {
1476    /// Shared package map — same Arc is held by WasmHostState for cross-package calls.
1477    package_map: PackageMap,
1478    load_infos: Arc<StdMutex<HashMap<String, PackageLoadInfo>>>,
1479    /// Base host state (without caller_package_name set).
1480    base_state: WasmHostState,
1481    /// Per-package circuit breaker for fault isolation (A2). Shared (Arc inside)
1482    /// so both WasmPackageHost::call and WasmHandle::call enforce the same state.
1483    breaker: CircuitBreaker,
1484}
1485
1486impl WasmPackageHost {
1487    /// Load all WASM packages with shared host state.
1488    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        // Wire the package_map into the base state so host functions can access it
1499        let base_state = WasmHostState {
1500            package_map: package_map.clone(),
1501            ..host_state
1502        };
1503
1504        // Build plugins in parallel. wasmtime compilation (inside build_plugin ->
1505        // load_one -> PluginBuilder::build) is CPU-bound and independent per package:
1506        // each plugin gets its own UserData clone (shared Arcs), and cross-package
1507        // init() calls happen only after this constructor returns. To avoid
1508        // oversubscribing wasmtime's internal cranelift threadpool we distribute the
1509        // packages across a bounded worker pool sized to the available cores rather
1510        // than spawning one thread per package. extism `Plugin` is `Send`, so each
1511        // worker can return the built plugins to the parent scope.
1512        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                        // Round-robin partition so each worker gets a balanced share.
1527                        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    /// Dynamically load a single plugin. Calls init() after loading.
1801    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        // Best-effort init
1818        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    /// Unload a package by name. The WASM resources are freed on drop.
1829    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        // A2: a reloaded/replaced package deserves a fresh breaker.
1836        self.breaker.reset(name);
1837        tracing::info!("Unloaded WASM package '{}'", name);
1838        Ok(())
1839    }
1840
1841    /// Reload a plugin: unload then load from the same path.
1842    pub fn reload_package(&mut self, info: &PackageLoadInfo) -> Result<()> {
1843        // Unload if present (ignore error if not loaded)
1844        let _ = self.unload_package(&info.name);
1845        // Load fresh
1846        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    /// Call an exported function on a named plugin.
1865    pub fn call(&self, package_name: &str, func: &str, input: &str) -> Result<String> {
1866        // A2: reject calls to a tripped package without touching the plugin.
1867        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        // Record host-side failure (trap/panic/instantiation error), not guest
1881        // business errors carried inside an Ok JSON payload.
1882        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    /// Check if a package is loaded.
1890    pub fn has_package(&self, name: &str) -> bool {
1891        self.package_map.lock().unwrap().contains_key(name)
1892    }
1893
1894    /// List loaded package names.
1895    pub fn package_names(&self) -> Vec<String> {
1896        self.package_map.lock().unwrap().keys().cloned().collect()
1897    }
1898
1899    /// Write a value into the shared KV store (same store WASM packages read via
1900    /// host_kv_get). Lets the API layer push runtime state (e.g. team:role_routing
1901    /// on scene activation) without going through a WASM call.
1902    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    /// Read a value from the shared KV store.
1909    #[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// ── Handle wrapper for async context ──
1926
1927/// Thread-safe handle to WasmPackageHost for use in async Axum handlers.
1928#[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    /// Call a package function. Safe to call from async context.
1941    pub fn call(&self, package_name: &str, func: &str, input: &str) -> Result<String> {
1942        // Grab plugin_arc and a breaker handle under one short host lock (A2).
1943        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        // Reject calls to a tripped package without touching the plugin.
1958        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        // 关键:只在极短临界区内从 host 克隆出隔离调用所需的字段(全是 Arc/可廉价克隆),
1975        // 然后释放 host 总锁,再创建临时实例执行。绝不能在整个 isolated 调用期间持有
1976        // host 锁——否则被调 agent 经 host_capability_call → dispatch_package_payload →
1977        // handle.call 需要同一把 host 锁时会自锁,且并发 fan-out 全被串行阻塞。
1978        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    /// Write a value into the shared KV store (see WasmPackageHost::kv_set).
2023    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    /// Read a value from the shared KV store.
2030    #[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    /// Dynamically load a new plugin.
2045    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    /// Unload a package by name.
2054    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    /// Reload a package (unload + load).
2063    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
2072// ── Pipeline bridge adapters ──
2073
2074/// Calls package-exported route() via WASM.
2075pub 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
2106/// Calls package-exported select_key() via WASM.
2107pub 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
2144/// Calls package-exported handle_error() via WASM.
2145pub 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}