Skip to main content

vulcan_luaskills/runtime/
engine.rs

1use mlua::{Function, HookTriggers, Lua, MultiValue, Table, Value as LuaValue, VmState};
2use serde::{Deserialize, Serialize};
3use serde_json::{Value, json};
4use std::collections::{BTreeMap, HashMap, HashSet};
5use std::fs;
6use std::io::{Read, Write};
7use std::path::{Component, Path, PathBuf};
8use std::process::{Command, Stdio};
9use std::sync::{Arc, Condvar, Mutex, OnceLock};
10use std::thread;
11use std::time::{Duration, Instant};
12
13use crate::dependency::manager::{DependencyManager, DependencyManagerConfig, ensure_directory};
14use crate::entry_descriptor::{RuntimeEntryDescriptor, RuntimeEntryParameterDescriptor};
15use crate::host::callbacks::{
16    RuntimeEntryRegistryDelta, RuntimeSkillLifecycleEvent, RuntimeSkillManagementAction,
17    RuntimeSkillManagementRequest, dispatch_skill_management_request,
18    try_has_skill_management_callback,
19};
20use crate::host::database::RuntimeDatabaseProviderCallbacks;
21use crate::lancedb_host::{LanceDbSkillBinding, LanceDbSkillHost, disabled_skill_status_json};
22use crate::lua_skill::{SkillMeta, validate_luaskills_identifier, validate_luaskills_version};
23use crate::runtime::config::{SkillConfigEntry, SkillConfigStore};
24use crate::runtime_context::{RuntimeClientInfo, RuntimeRequestContext};
25use crate::runtime_help::{
26    RuntimeHelpDetail, RuntimeHelpNodeDescriptor, RuntimeSkillHelpDescriptor,
27};
28use crate::runtime_logging::{error as log_error, info as log_info, warn as log_warn};
29use crate::runtime_options::{LuaInvocationContext, LuaRuntimeHostOptions, RuntimeSkillRoot};
30use crate::runtime_result::{
31    NON_STRING_TOOL_RESULT_ERROR, RuntimeInvocationResult, ToolOverflowMode,
32};
33use crate::skill::dependencies::SkillDependencyManifest;
34use crate::skill::manager::{
35    PreparedSkillApply, SkillApplyResult, SkillInstallRequest, SkillManager, SkillManagerConfig,
36    SkillOperationPlane, SkillUninstallOptions, SkillUninstallResult,
37    collect_effective_skill_instances_from_roots, resolve_declared_skill_instance_from_roots,
38    resolve_effective_skill_instance_from_roots,
39};
40use crate::sqlite_host::{
41    SqliteSkillBinding, SqliteSkillHost,
42    disabled_skill_status_json as disabled_sqlite_skill_status_json,
43};
44use crate::tool_cache::{ToolCacheConfig, configure_global_tool_cache, global_tool_cache};
45
46// ============================================================
47// Loaded skill (compiled Lua function + metadata)
48// ============================================================
49
50#[derive(Clone)]
51struct LoadedSkill {
52    meta: SkillMeta,
53    dir: std::path::PathBuf,
54    root_name: String,
55    lancedb_binding: Option<Arc<LanceDbSkillBinding>>,
56    sqlite_binding: Option<Arc<SqliteSkillBinding>>,
57    resolved_entry_names: HashMap<String, String>,
58}
59
60/// Normalize one host-visible path string so Windows verbatim prefixes never leak into logs or Lua-visible context.
61/// 归一化一个对宿主可见的路径文本,避免 Windows verbatim 前缀泄漏到日志或 Lua 可见上下文中。
62fn normalize_host_visible_path_text(rendered: &str) -> String {
63    #[cfg(windows)]
64    {
65        if let Some(stripped) = rendered.strip_prefix(r"\\?\UNC\") {
66            return format!(r"\\{}", stripped);
67        }
68        if let Some(stripped) = rendered.strip_prefix(r"\\?\") {
69            return stripped.to_string();
70        }
71    }
72    rendered.to_string()
73}
74
75/// Render one filesystem path for host-visible runtime surfaces without Windows verbatim prefixes.
76/// 为宿主可见的运行时表面渲染文件系统路径,并去掉 Windows verbatim 前缀。
77fn render_host_visible_path(path: &Path) -> String {
78    normalize_host_visible_path_text(&path.to_string_lossy())
79}
80
81/// Render one filesystem path for user-facing runtime logs without Windows verbatim prefixes.
82/// 为面向用户的运行时日志渲染文件系统路径,并去掉 Windows verbatim 前缀。
83fn render_log_friendly_path(path: &Path) -> String {
84    render_host_visible_path(path)
85}
86
87/// Normalize one runtime-root path with stable lexical component folding.
88/// 使用稳定的词法组件折叠规则规范化单个运行时根目录路径。
89fn normalize_runtime_root_path(path: &Path) -> PathBuf {
90    let mut normalized = PathBuf::new();
91    let mut can_pop_normal = false;
92    for component in path.components() {
93        match component {
94            Component::Prefix(prefix) => {
95                normalized.push(prefix.as_os_str());
96                can_pop_normal = false;
97            }
98            Component::RootDir => {
99                normalized.push(component.as_os_str());
100                can_pop_normal = false;
101            }
102            Component::CurDir => {}
103            Component::ParentDir => {
104                if can_pop_normal && normalized.pop() {
105                    can_pop_normal = !matches!(
106                        normalized.components().next_back(),
107                        Some(Component::Prefix(_)) | Some(Component::RootDir) | None
108                    );
109                } else if !path.is_absolute() {
110                    normalized.push(component.as_os_str());
111                    can_pop_normal = false;
112                }
113            }
114            Component::Normal(part) => {
115                normalized.push(part);
116                can_pop_normal = true;
117            }
118        }
119    }
120    normalized
121}
122
123/// Pool sizing configuration for Lua virtual machines.
124/// Lua 虚拟机池的容量配置。
125#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
126pub struct LuaVmPoolConfig {
127    /// Minimum number of VMs that should stay warm.
128    /// 需要常驻保温的最小虚拟机数量。
129    pub min_size: usize,
130    /// Maximum number of VMs allowed in the pool.
131    /// 池内允许存在的最大虚拟机数量。
132    pub max_size: usize,
133    /// Idle TTL in seconds before an excess VM can be retired.
134    /// 多余虚拟机在空闲多少秒后允许回收。
135    pub idle_ttl_secs: u64,
136}
137
138impl LuaVmPoolConfig {
139    /// Return a normalized pool config with safe bounds.
140    /// 返回经过安全边界归一化后的池配置。
141    fn normalized(self) -> Self {
142        let min_size = self.min_size.max(1);
143        let max_size = self.max_size.max(min_size);
144        let idle_ttl_secs = self.idle_ttl_secs.max(1);
145        Self {
146            min_size,
147            max_size,
148            idle_ttl_secs,
149        }
150    }
151}
152
153/// Return the default dedicated pool config used by isolated runlua execution.
154/// 返回隔离 runlua 执行使用的默认独立池配置。
155fn default_runlua_vm_pool_config() -> LuaVmPoolConfig {
156    LuaVmPoolConfig {
157        min_size: 1,
158        max_size: 4,
159        idle_ttl_secs: 60,
160    }
161}
162
163/// Runtime state of a single Lua VM instance.
164/// 单个 Lua 虚拟机实例的运行时状态。
165struct LuaVm {
166    lua: Lua,
167    last_used_at: Instant,
168}
169
170/// Shared mutable state for the Lua VM pool.
171/// Lua 虚拟机池的共享可变状态。
172struct LuaVmPoolState {
173    available: Vec<LuaVm>,
174    total_count: usize,
175}
176
177/// Pool of Lua VM instances with opportunistic scaling.
178/// 支持按需扩缩容的 Lua 虚拟机池。
179struct LuaVmPool {
180    config: LuaVmPoolConfig,
181    state: Mutex<LuaVmPoolState>,
182    condvar: Condvar,
183}
184
185// ============================================================
186// LuaEngine — LuaJIT VM wrapper
187// ============================================================
188
189pub struct LuaEngine {
190    skills: HashMap<String, LoadedSkill>,
191    entry_registry: BTreeMap<String, ResolvedEntryTarget>,
192    pool: Arc<LuaVmPool>,
193    runlua_pool: Arc<LuaVmPool>,
194    skill_config_store: Arc<SkillConfigStore>,
195    lancedb_host: Option<Arc<LanceDbSkillHost>>,
196    sqlite_host: Option<Arc<SqliteSkillHost>>,
197    database_provider_callbacks: Arc<RuntimeDatabaseProviderCallbacks>,
198    host_options: Arc<LuaRuntimeHostOptions>,
199}
200
201/// Resolved runtime entry target produced after canonical-name collision indexing.
202/// 经过 canonical 名称冲突编号后得到的运行时入口目标。
203#[derive(Debug, Clone)]
204struct ResolvedEntryTarget {
205    /// Final canonical tool name exposed to hosts and Lua dispatch.
206    /// 暴露给宿主和 Lua 分发器的最终 canonical 工具名。
207    canonical_name: String,
208    /// Internal storage key of the owning loaded skill.
209    /// 所属已加载 skill 的内部存储键。
210    skill_storage_key: String,
211    /// Owning stable skill identifier declared in skill metadata.
212    /// 在 skill 元数据中声明的所属稳定 skill 标识符。
213    skill_id: String,
214    /// Stable local entry name declared by the owning skill.
215    /// 所属 skill 声明的稳定局部入口名称。
216    local_name: String,
217}
218
219/// Construction options used by the host to create one LuaSkills runtime engine.
220/// 宿主创建单个 LuaSkills 运行时引擎时使用的构造选项。
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct LuaEngineOptions {
223    /// Pool sizing configuration for reusable Lua virtual machines.
224    /// 可复用 Lua 虚拟机池的容量配置。
225    pub pool_config: LuaVmPoolConfig,
226    /// Host-owned runtime paths and external library locations.
227    /// 宿主拥有的运行时路径与外部动态库位置配置。
228    pub host_options: LuaRuntimeHostOptions,
229}
230
231impl LuaEngineOptions {
232    /// Build engine options from one pool config and one host option object.
233    /// 基于一份虚拟机池配置和一份宿主选项对象构造引擎选项。
234    pub fn new(pool_config: LuaVmPoolConfig, host_options: LuaRuntimeHostOptions) -> Self {
235        Self {
236            pool_config,
237            host_options,
238        }
239    }
240}
241
242impl LoadedSkill {
243    /// Return the resolved canonical entry name for one local entry name.
244    /// 返回某个局部入口名称对应的已解析 canonical 名称。
245    fn resolved_tool_name(&self, local_name: &str) -> Option<&str> {
246        self.resolved_entry_names
247            .get(local_name)
248            .map(String::as_str)
249    }
250}
251
252/// Create one Lua-facing runtime skill-management bridge function.
253/// 创建一个面向 Lua 的运行时技能管理桥接函数。
254fn create_runtime_skill_management_bridge_fn(
255    lua: &Lua,
256    enabled: bool,
257    action: RuntimeSkillManagementAction,
258    function_name: &'static str,
259) -> mlua::Result<Function> {
260    let action_name = function_name.to_string();
261    lua.create_function(move |lua, input: LuaValue| {
262        if !enabled {
263            return Err(mlua::Error::runtime(format!(
264                "vulcan.runtime.skills.{} is disabled by host policy",
265                action_name
266            )));
267        }
268
269        let payload = lua_value_to_json(&input).map_err(|error| {
270            mlua::Error::runtime(format!("vulcan.runtime.skills.{}: {}", action_name, error))
271        })?;
272        let result = dispatch_skill_management_request(&RuntimeSkillManagementRequest {
273            action: action.clone(),
274            input: payload,
275        })
276        .map_err(|error| {
277            mlua::Error::runtime(format!("vulcan.runtime.skills.{}: {}", action_name, error))
278        })?;
279        json_value_to_lua(lua, &result)
280    })
281}
282
283/// Return a stable human-readable Lua value type name.
284/// 返回稳定且可读的 Lua 值类型名称。
285fn lua_value_type_name(value: &LuaValue) -> &'static str {
286    match value {
287        LuaValue::Nil => "nil",
288        LuaValue::Boolean(_) => "boolean",
289        LuaValue::LightUserData(_) => "lightuserdata",
290        LuaValue::Integer(_) => "integer",
291        LuaValue::Number(_) => "number",
292        LuaValue::String(_) => "string",
293        LuaValue::Table(_) => "table",
294        LuaValue::Function(_) => "function",
295        LuaValue::Thread(_) => "thread",
296        LuaValue::UserData(_) => "userdata",
297        LuaValue::Error(_) => "error",
298        LuaValue::Other(_) => "other",
299    }
300}
301
302/// RunLua execution request accepted by `vulcan.runtime.lua.exec`.
303/// `vulcan.runtime.lua.exec` 接收的 RunLua 执行请求结构。
304#[derive(Debug, Deserialize, Serialize)]
305struct RunLuaExecRequest {
306    /// Human-readable task summary echoed in the result header.
307    /// 展示在结果头部的人类可读任务摘要。
308    #[serde(default)]
309    task: String,
310    /// Inline Lua source code executed inside the isolated runtime VM.
311    /// 在隔离运行时虚拟机中执行的内联 Lua 源代码。
312    #[serde(default)]
313    code: Option<String>,
314    /// Lua file path executed inside the isolated runtime VM.
315    /// 在隔离运行时虚拟机中执行的 Lua 文件路径。
316    #[serde(default)]
317    file: Option<String>,
318    /// Structured arguments exposed to Lua as `args`.
319    /// 以 `args` 变量形式暴露给 Lua 的结构化参数。
320    #[serde(default = "default_runlua_exec_args")]
321    args: Value,
322    /// Maximum execution time in milliseconds. Defaults to 60 seconds.
323    /// 最大执行时长(毫秒),默认 60 秒。
324    #[serde(default = "default_runlua_timeout_ms")]
325    timeout_ms: u64,
326    /// Internal caller tool name used to enforce luaexec reentrancy guards.
327    /// 用于执行 luaexec 重入保护的内部调用者工具名称。
328    #[serde(default)]
329    caller_tool_name: Option<String>,
330}
331
332/// Return the default empty args object for runlua execution.
333/// 返回 runlua 执行默认使用的空参数对象。
334fn default_runlua_exec_args() -> Value {
335    Value::Object(serde_json::Map::new())
336}
337
338/// Return the default timeout for runlua execution in milliseconds.
339/// 返回 runlua 执行的默认超时时间(毫秒)。
340fn default_runlua_timeout_ms() -> u64 {
341    60_000
342}
343
344/// Return the process-wide current-directory guard used by lua file execution.
345/// 返回 Lua 文件执行期间用于保护进程工作目录切换的全局互斥锁。
346fn runlua_cwd_guard() -> &'static Mutex<()> {
347    static RUNLUA_CWD_GUARD: OnceLock<Mutex<()>> = OnceLock::new();
348    RUNLUA_CWD_GUARD.get_or_init(|| Mutex::new(()))
349}
350
351/// Build the restricted simulated request context used by internal luaexec tool calls.
352/// 构建内部 luaexec 工具调用使用的受限模拟请求上下文。
353fn build_luaexec_call_request_context() -> RuntimeRequestContext {
354    RuntimeRequestContext {
355        transport_name: Some("luaexec_call".to_string()),
356        session_id: Some("luaexec-call-internal".to_string()),
357        client_info: Some(RuntimeClientInfo {
358            kind: Some("runtime".to_string()),
359            name: Some("luaexec_call".to_string()),
360            version: Some("internal-runtime".to_string()),
361        }),
362        client_capabilities: json!({}),
363    }
364}
365
366/// One captured renderable runlua return item.
367/// 一项已捕获并可渲染的 runlua 返回值。
368#[derive(Debug)]
369struct RunLuaRenderedValue {
370    /// Render format of the current item, such as `text` or `json`.
371    /// 当前项的渲染格式,例如 `text` 或 `json`。
372    format: &'static str,
373    /// Rendered payload already formatted for Markdown code fences.
374    /// 已格式化好的载荷文本,可直接写入 Markdown 代码块。
375    content: String,
376}
377
378/// Detect whether a string looks like Lua's debug-style coercion output.
379/// 检测字符串是否像 Lua 对象被 `tostring` 后生成的调试文本。
380fn looks_like_lua_debug_value(text: &str) -> bool {
381    ["table: 0x", "function: 0x", "thread: 0x", "userdata: 0x"]
382        .iter()
383        .any(|prefix| text.starts_with(prefix))
384}
385
386/// Validate Windows-specific path syntax conservatively before touching the filesystem.
387/// 在真正访问文件系统之前,对 Windows 路径语法做保守校验。
388#[cfg(windows)]
389fn has_invalid_windows_path_syntax(text: &str) -> bool {
390    let trimmed = text.trim();
391    if trimmed.starts_with(r"\\?\") {
392        return false;
393    }
394
395    let first_char = trimmed.chars().next();
396    for (index, ch) in trimmed.char_indices() {
397        if ch.is_control() {
398            return true;
399        }
400        if matches!(ch, '<' | '>' | '"' | '|' | '?' | '*') {
401            return true;
402        }
403        if ch == ':' {
404            let is_drive_prefix =
405                index == 1 && first_char.map(|c| c.is_ascii_alphabetic()).unwrap_or(false);
406            if !is_drive_prefix {
407                return true;
408            }
409        }
410    }
411    false
412}
413
414/// Require an exact UTF-8 Lua string and reject empty/blank values when needed.
415/// 要求参数必须是精确的 UTF-8 Lua 字符串,并在需要时拒绝空值或纯空白值。
416fn require_string_arg(
417    value: LuaValue,
418    fn_name: &str,
419    param_name: &str,
420    allow_blank: bool,
421) -> mlua::Result<String> {
422    let raw = match value {
423        LuaValue::String(text) => text
424            .to_str()
425            .map_err(|_| {
426                mlua::Error::runtime(format!(
427                    "{fn_name}: {param_name} must be a valid UTF-8 string"
428                ))
429            })?
430            .to_string(),
431        other => {
432            return Err(mlua::Error::runtime(format!(
433                "{fn_name}: {param_name} must be a string, got {}",
434                lua_value_type_name(&other)
435            )));
436        }
437    };
438
439    if !allow_blank && raw.trim().is_empty() {
440        return Err(mlua::Error::runtime(format!(
441            "{fn_name}: {param_name} must not be empty"
442        )));
443    }
444    if raw.contains('\0') {
445        return Err(mlua::Error::runtime(format!(
446            "{fn_name}: {param_name} must not contain NUL bytes"
447        )));
448    }
449    Ok(raw)
450}
451
452/// Validate path-like text before using it in filesystem operations.
453/// 在文件系统函数真正使用路径文本前,先进行统一校验。
454fn validate_path_text(text: &str, fn_name: &str, param_name: &str) -> mlua::Result<()> {
455    if looks_like_lua_debug_value(text) {
456        return Err(mlua::Error::runtime(format!(
457            "{fn_name}: {param_name} looks like a coerced Lua object string `{text}`"
458        )));
459    }
460
461    #[cfg(windows)]
462    if has_invalid_windows_path_syntax(text) {
463        return Err(mlua::Error::runtime(format!(
464            "{fn_name}: {param_name} contains invalid Windows path syntax"
465        )));
466    }
467
468    Ok(())
469}
470
471/// Require a validated path string from Lua input.
472/// 从 Lua 输入中提取并校验路径字符串参数。
473fn require_path_arg(value: LuaValue, fn_name: &str, param_name: &str) -> mlua::Result<String> {
474    let text = require_string_arg(value, fn_name, param_name, false)?;
475    validate_path_text(&text, fn_name, param_name)?;
476    Ok(text)
477}
478
479/// Read an optional non-negative integer argument from Lua.
480/// 从 Lua 读取可选的非负整数参数。
481fn optional_u64_arg(value: LuaValue, fn_name: &str, param_name: &str) -> mlua::Result<Option<u64>> {
482    match value {
483        LuaValue::Nil => Ok(None),
484        LuaValue::Integer(v) if v >= 0 => Ok(Some(v as u64)),
485        LuaValue::Number(v) if v.is_finite() && v >= 0.0 && v.fract() == 0.0 => Ok(Some(v as u64)),
486        other => Err(mlua::Error::runtime(format!(
487            "{fn_name}: {param_name} must be a non-negative integer: {}",
488            lua_value_type_name(&other)
489        ))),
490    }
491}
492
493/// Require a Lua table argument without silent coercion.
494/// 要求参数必须是 Lua table,禁止静默类型转换。
495fn require_table_arg(value: LuaValue, fn_name: &str, param_name: &str) -> mlua::Result<Table> {
496    match value {
497        LuaValue::Table(table) => Ok(table),
498        other => Err(mlua::Error::runtime(format!(
499            "{fn_name}: {param_name} must be a table, got {}",
500            lua_value_type_name(&other)
501        ))),
502    }
503}
504
505/// Execution mode supported by `vulcan.exec`.
506/// `vulcan.exec` 支持的执行模式。
507enum ExecMode {
508    Shell { command: String },
509    Program { program: String, args: Vec<String> },
510}
511
512/// Parsed process execution request from Lua.
513/// 从 Lua 解析得到的进程执行请求。
514struct ExecRequest {
515    mode: ExecMode,
516    cwd: Option<String>,
517    env: HashMap<String, String>,
518    stdin: Option<String>,
519    timeout_ms: Option<u64>,
520}
521
522/// Process execution result returned back to Lua.
523/// 返回给 Lua 的进程执行结果。
524struct ExecResult {
525    ok: bool,
526    success: bool,
527    code: Option<i32>,
528    stdout: String,
529    stderr: String,
530    timed_out: bool,
531    error: Option<String>,
532}
533
534/// Require a scalar text-like value for exec arguments and environment values.
535/// 为 exec 的参数和环境变量值提取标量文本,拒绝 table/function 等复杂类型。
536fn require_exec_scalar_text(
537    value: LuaValue,
538    fn_name: &str,
539    param_name: &str,
540    allow_blank: bool,
541) -> mlua::Result<String> {
542    match value {
543        LuaValue::String(_) => require_string_arg(value, fn_name, param_name, allow_blank),
544        LuaValue::Integer(number) => Ok(number.to_string()),
545        LuaValue::Number(number) => {
546            if !number.is_finite() {
547                return Err(mlua::Error::runtime(format!(
548                    "{fn_name}: {param_name} must be a finite number"
549                )));
550            }
551            Ok(number.to_string())
552        }
553        LuaValue::Boolean(flag) => Ok(flag.to_string()),
554        other => Err(mlua::Error::runtime(format!(
555            "{fn_name}: {param_name} must be a string: {}",
556            lua_value_type_name(&other)
557        ))),
558    }
559}
560
561/// Read an optional string field from a Lua table with strict validation.
562/// 从 Lua table 中读取可选字符串字段,并执行严格校验。
563fn table_get_optional_string_field(
564    table: &Table,
565    fn_name: &str,
566    field_name: &str,
567    allow_blank: bool,
568) -> mlua::Result<Option<String>> {
569    let value: LuaValue = table.get(field_name)?;
570    match value {
571        LuaValue::Nil => Ok(None),
572        other => Ok(Some(require_string_arg(
573            other,
574            fn_name,
575            field_name,
576            allow_blank,
577        )?)),
578    }
579}
580
581/// Read an optional boolean field from a Lua table.
582/// 从 Lua table 中读取可选布尔字段。
583fn table_get_optional_bool_field(
584    table: &Table,
585    fn_name: &str,
586    field_name: &str,
587) -> mlua::Result<Option<bool>> {
588    let value: LuaValue = table.get(field_name)?;
589    match value {
590        LuaValue::Nil => Ok(None),
591        LuaValue::Boolean(flag) => Ok(Some(flag)),
592        other => Err(mlua::Error::runtime(format!(
593            "{fn_name}: {field_name} must be a boolean when provided: {}",
594            lua_value_type_name(&other)
595        ))),
596    }
597}
598
599/// Read an optional timeout field in milliseconds from a Lua table.
600/// 从 Lua table 中读取可选的毫秒级超时字段。
601fn table_get_optional_timeout_field(
602    table: &Table,
603    fn_name: &str,
604    field_name: &str,
605) -> mlua::Result<Option<u64>> {
606    let value: LuaValue = table.get(field_name)?;
607    match value {
608        LuaValue::Nil => Ok(None),
609        LuaValue::Integer(number) if number > 0 => Ok(Some(number as u64)),
610        LuaValue::Number(number) if number.is_finite() && number.fract() == 0.0 && number > 0.0 => {
611            Ok(Some(number as u64))
612        }
613        other => Err(mlua::Error::runtime(format!(
614            "{fn_name}: {field_name} must be a positive integer in milliseconds: {}",
615            lua_value_type_name(&other)
616        ))),
617    }
618}
619
620/// Read an optional string-like array field from a Lua table.
621/// 从 Lua table 中读取可选的字符串类数组字段。
622fn table_get_string_list_field(
623    table: &Table,
624    fn_name: &str,
625    field_name: &str,
626) -> mlua::Result<Vec<String>> {
627    let value: LuaValue = table.get(field_name)?;
628    match value {
629        LuaValue::Nil => Ok(Vec::new()),
630        other => {
631            let list = require_table_arg(other, fn_name, field_name)?;
632            let mut items = Vec::new();
633            for (index, item) in list.sequence_values::<LuaValue>().enumerate() {
634                let item = item.map_err(|error| {
635                    mlua::Error::runtime(format!(
636                        "{fn_name}: failed to read {field_name}[{}]: {}, {}",
637                        index + 1,
638                        index + 1,
639                        error
640                    ))
641                })?;
642                items.push(require_exec_scalar_text(
643                    item,
644                    fn_name,
645                    &format!("{field_name}[{}]", index + 1),
646                    true,
647                )?);
648            }
649            Ok(items)
650        }
651    }
652}
653
654/// Read an optional string map field from a Lua table.
655/// 从 Lua table 中读取可选的字符串映射字段。
656fn table_get_string_map_field(
657    table: &Table,
658    fn_name: &str,
659    field_name: &str,
660) -> mlua::Result<HashMap<String, String>> {
661    let value: LuaValue = table.get(field_name)?;
662    match value {
663        LuaValue::Nil => Ok(HashMap::new()),
664        other => {
665            let map_table = require_table_arg(other, fn_name, field_name)?;
666            let mut items = HashMap::new();
667            for pair in map_table.pairs::<LuaValue, LuaValue>() {
668                let (key_value, field_value) = pair.map_err(|_error| {
669                    mlua::Error::runtime(format!("{fn_name}: failed to read {field_name}"))
670                })?;
671                let key =
672                    require_string_arg(key_value, fn_name, &format!("{field_name}.<key>"), false)?;
673                let value_text = require_exec_scalar_text(
674                    field_value,
675                    fn_name,
676                    &format!("{field_name}.{key}"),
677                    true,
678                )?;
679                items.insert(key, value_text);
680            }
681            Ok(items)
682        }
683    }
684}
685
686/// Parse Lua input into an executable process request.
687/// 将 Lua 输入解析为可执行的进程请求。
688fn parse_exec_request(value: LuaValue, fn_name: &str) -> mlua::Result<ExecRequest> {
689    match value {
690        LuaValue::String(command_text) => Ok(ExecRequest {
691            mode: ExecMode::Shell {
692                command: require_string_arg(
693                    LuaValue::String(command_text),
694                    fn_name,
695                    "command",
696                    false,
697                )?,
698            },
699            cwd: None,
700            env: HashMap::new(),
701            stdin: None,
702            timeout_ms: None,
703        }),
704        LuaValue::Table(spec) => {
705            let command = table_get_optional_string_field(&spec, fn_name, "command", false)?;
706            let program = table_get_optional_string_field(&spec, fn_name, "program", false)?;
707            let args = table_get_string_list_field(&spec, fn_name, "args")?;
708            let cwd = table_get_optional_string_field(&spec, fn_name, "cwd", false)?;
709            let env = table_get_string_map_field(&spec, fn_name, "env")?;
710            let stdin = table_get_optional_string_field(&spec, fn_name, "stdin", true)?;
711            let timeout_ms = table_get_optional_timeout_field(&spec, fn_name, "timeout_ms")?;
712            let shell_override = table_get_optional_bool_field(&spec, fn_name, "shell")?;
713
714            if let Some(current_dir) = cwd.as_deref() {
715                validate_path_text(current_dir, fn_name, "cwd")?;
716            }
717
718            let mode = match (command, program) {
719                (Some(command_text), None) => {
720                    if matches!(shell_override, Some(false)) {
721                        return Err(mlua::Error::runtime(format!(
722                            "{fn_name}: shell=false cannot be used with command mode"
723                        )));
724                    }
725                    if !args.is_empty() {
726                        return Err(mlua::Error::runtime(format!(
727                            "{fn_name}: args is only supported with program mode"
728                        )));
729                    }
730                    ExecMode::Shell {
731                        command: command_text,
732                    }
733                }
734                (None, Some(program_path)) => {
735                    if matches!(shell_override, Some(true)) {
736                        return Err(mlua::Error::runtime(format!(
737                            "{fn_name}: shell=true requires command mode"
738                        )));
739                    }
740                    ExecMode::Program {
741                        program: program_path,
742                        args,
743                    }
744                }
745                (Some(_), Some(_)) => {
746                    return Err(mlua::Error::runtime(format!(
747                        "{fn_name}: command and program are mutually exclusive"
748                    )));
749                }
750                (None, None) => {
751                    return Err(mlua::Error::runtime(format!(
752                        "{fn_name}: expected a string command or a table with command"
753                    )));
754                }
755            };
756
757            Ok(ExecRequest {
758                mode,
759                cwd,
760                env,
761                stdin,
762                timeout_ms,
763            })
764        }
765        other => Err(mlua::Error::runtime(format!(
766            "{fn_name}: expected a string or table, got {}",
767            lua_value_type_name(&other)
768        ))),
769    }
770}
771
772/// Return the default shell program and command flag for the current platform.
773/// 返回当前平台默认的 shell 程序及命令参数开关。
774#[cfg(windows)]
775fn default_shell_launcher() -> (&'static str, &'static str) {
776    ("cmd.exe", "/C")
777}
778
779/// Return the default shell program and command flag for the current platform.
780/// 返回当前平台默认的 shell 程序及命令参数开关。
781#[cfg(not(windows))]
782fn default_shell_launcher() -> (&'static str, &'static str) {
783    ("sh", "-c")
784}
785
786/// Spawn a background reader for a child process output pipe.
787/// 为子进程输出管道启动后台读取线程。
788fn spawn_pipe_reader<R>(mut reader: R) -> thread::JoinHandle<String>
789where
790    R: Read + Send + 'static,
791{
792    thread::spawn(move || {
793        let mut buffer = Vec::new();
794        let _ = reader.read_to_end(&mut buffer);
795        String::from_utf8_lossy(&buffer).to_string()
796    })
797}
798
799/// Spawn a background writer for a child process stdin pipe.
800/// 为子进程标准输入管道启动后台写入线程。
801fn spawn_stdin_writer<W>(mut writer: W, input: String) -> thread::JoinHandle<()>
802where
803    W: Write + Send + 'static,
804{
805    thread::spawn(move || {
806        let _ = writer.write_all(input.as_bytes());
807        let _ = writer.flush();
808    })
809}
810
811/// Execute a process request and capture its structured result.
812/// 执行进程请求并捕获结构化结果。
813fn execute_exec_request(request: ExecRequest) -> ExecResult {
814    let mut command = match &request.mode {
815        ExecMode::Shell { command } => {
816            let (shell_program, shell_flag) = default_shell_launcher();
817            let mut process = Command::new(shell_program);
818            process.arg(shell_flag).arg(command);
819            process
820        }
821        ExecMode::Program { program, args } => {
822            let mut process = Command::new(program);
823            process.args(args);
824            process
825        }
826    };
827
828    if let Some(current_dir) = &request.cwd {
829        command.current_dir(current_dir);
830    }
831    if !request.env.is_empty() {
832        command.envs(&request.env);
833    }
834    command.stdout(Stdio::piped());
835    command.stderr(Stdio::piped());
836    command.stdin(if request.stdin.is_some() {
837        Stdio::piped()
838    } else {
839        Stdio::null()
840    });
841
842    let mut child = match command.spawn() {
843        Ok(child) => child,
844        Err(error) => {
845            let error_text = format!("failed to spawn process: {}", error);
846            return ExecResult {
847                ok: false,
848                success: false,
849                code: None,
850                stdout: String::new(),
851                stderr: error_text.clone(),
852                timed_out: false,
853                error: Some(error_text),
854            };
855        }
856    };
857
858    let stdout_handle = child.stdout.take().map(spawn_pipe_reader);
859    let stderr_handle = child.stderr.take().map(spawn_pipe_reader);
860    let stdin_handle = match (request.stdin.clone(), child.stdin.take()) {
861        (Some(input), Some(stdin)) => Some(spawn_stdin_writer(stdin, input)),
862        _ => None,
863    };
864
865    let mut timed_out = false;
866    let timeout = request.timeout_ms.map(Duration::from_millis);
867    let started_at = Instant::now();
868
869    let final_status = loop {
870        match child.try_wait() {
871            Ok(Some(status)) => {
872                break Some(status);
873            }
874            Ok(None) => {
875                if let Some(limit) = timeout {
876                    if started_at.elapsed() >= limit {
877                        timed_out = true;
878                        let _ = child.kill();
879                        break child.wait().ok();
880                    }
881                }
882                thread::sleep(Duration::from_millis(10));
883            }
884            Err(error) => {
885                let error_text = format!("failed to wait for process: {}", error);
886                return ExecResult {
887                    ok: false,
888                    success: false,
889                    code: None,
890                    stdout: String::new(),
891                    stderr: error_text.clone(),
892                    timed_out,
893                    error: Some(error_text),
894                };
895            }
896        }
897    };
898
899    if let Some(handle) = stdin_handle {
900        let _ = handle.join();
901    }
902
903    let stdout = stdout_handle
904        .map(|handle| handle.join().unwrap_or_default())
905        .unwrap_or_default();
906    let mut stderr = stderr_handle
907        .map(|handle| handle.join().unwrap_or_default())
908        .unwrap_or_default();
909
910    let status = match final_status {
911        Some(status) => status,
912        None => {
913            let error_text = "process finished without status".to_string();
914            return ExecResult {
915                ok: false,
916                success: false,
917                code: None,
918                stdout,
919                stderr: error_text.clone(),
920                timed_out,
921                error: Some(error_text),
922            };
923        }
924    };
925
926    let code = status.code();
927    let success = !timed_out && status.success();
928    let mut error = None;
929
930    if timed_out {
931        let timeout_value = request.timeout_ms.unwrap_or_default();
932        let timeout_text = format!("process execution timed out after {} ms", timeout_value);
933        if !stderr.is_empty() {
934            stderr.push('\n');
935        }
936        stderr.push_str(&timeout_text);
937        error = Some(timeout_text);
938    } else if !success {
939        error = Some(match code {
940            Some(exit_code) => format!("process exited with code {}", exit_code),
941            None => "process terminated without an exit code".to_string(),
942        });
943    }
944
945    ExecResult {
946        ok: success,
947        success,
948        code,
949        stdout,
950        stderr,
951        timed_out,
952        error,
953    }
954}
955
956/// Convert an exec result into a Lua table for skill consumption.
957/// 将 exec 结果转换为供 skill 消费的 Lua table。
958fn exec_result_to_lua_table(lua: &Lua, result: ExecResult) -> mlua::Result<Table> {
959    let table = lua.create_table()?;
960    table.set("ok", result.ok)?;
961    table.set("success", result.success)?;
962    table.set("stdout", result.stdout)?;
963    table.set("stderr", result.stderr)?;
964    table.set("timed_out", result.timed_out)?;
965    match result.code {
966        Some(code) => table.set("code", code)?,
967        None => table.set("code", LuaValue::Nil)?,
968    }
969    match result.error {
970        Some(error_text) => table.set("error", error_text)?,
971        None => table.set("error", LuaValue::Nil)?,
972    }
973    Ok(table)
974}
975
976/// Validate one relative metadata path against a fixed prefix and reject traversal.
977/// 按固定目录前缀校验单个 skill 元数据相对路径,并拒绝路径穿越。
978fn validate_skill_relative_path(
979    relative_path: &str,
980    expected_prefix: &str,
981    field_label: &str,
982) -> Result<(), String> {
983    let trimmed = relative_path.trim();
984    if trimmed.is_empty() {
985        return Err(format!("{field_label} must not be empty"));
986    }
987
988    let path = Path::new(trimmed);
989    if path.is_absolute() {
990        return Err(format!(
991            "{field_label} must be a relative path under {expected_prefix}"
992        ));
993    }
994
995    let normalized = trimmed.replace('\\', "/");
996    let required_prefix = format!("{expected_prefix}/");
997    if !normalized.starts_with(&required_prefix) {
998        return Err(format!("{field_label} must start with {required_prefix}"));
999    }
1000
1001    for component in path.components() {
1002        if !matches!(component, std::path::Component::Normal(_)) {
1003            return Err(format!("{field_label} must not contain parent"));
1004        }
1005    }
1006
1007    Ok(())
1008}
1009
1010/// Validate one discovered skill directory name against the strict LuaSkills rule.
1011/// 按严格 LuaSkills 规则校验一个被发现的 skill 目录名。
1012/// Build the absolute Lua entry file path for a tool.
1013/// 构建工具 Lua 入口文件的绝对路径。
1014fn tool_entry_path(skill_dir: &Path, tool: &crate::lua_skill::SkillToolMeta) -> PathBuf {
1015    skill_dir.join(&tool.lua_entry)
1016}
1017
1018/// Internal per-VM Vulcan execution markers used for tool dispatch guards.
1019/// 用于工具分发保护的每个虚拟机内部 Vulcan 执行标记。
1020#[derive(Debug, Clone, Default)]
1021struct VulcanInternalExecutionContext {
1022    /// Current tool name executing inside this Lua VM.
1023    /// 当前 Lua 虚拟机内正在执行的工具名称。
1024    tool_name: Option<String>,
1025    /// Current owner skill name executing inside this Lua VM.
1026    /// 当前 Lua 虚拟机内正在执行的所属 skill 名称。
1027    skill_name: Option<String>,
1028    /// Whether the current Lua VM is the isolated luaexec runtime environment.
1029    /// 当前 Lua 虚拟机是否处于隔离的 luaexec 运行环境。
1030    luaexec_active: bool,
1031    /// Original tool name that launched the current luaexec request.
1032    /// 发起当前 luaexec 请求的原始工具名称。
1033    luaexec_caller_tool_name: Option<String>,
1034}
1035
1036/// Capture the current Lua entry context stored on `vulcan`.
1037/// 捕获当前存放在 `vulcan` 上的 Lua 入口文件上下文。
1038fn capture_vulcan_file_context(
1039    lua: &Lua,
1040) -> Result<(Option<String>, Option<String>, Option<String>), String> {
1041    let context = get_vulcan_context_table(lua)?;
1042    let skill_dir: Option<String> = context
1043        .get("skill_dir")
1044        .map_err(|error| format!("Failed to read vulcan.context.skill_dir: {}", error))?;
1045    let entry_dir: Option<String> = context
1046        .get("entry_dir")
1047        .map_err(|error| format!("Failed to read vulcan.context.entry_dir: {}", error))?;
1048    let entry_file: Option<String> = context
1049        .get("entry_file")
1050        .map_err(|error| format!("Failed to read vulcan.context.entry_file: {}", error))?;
1051    Ok((skill_dir, entry_dir, entry_file))
1052}
1053
1054/// Populate the current skill directory, entry directory, and entry file onto `vulcan`.
1055/// 将当前 skill 目录、入口目录与入口文件路径注入到 `vulcan` 模块。
1056fn populate_vulcan_file_context(
1057    lua: &Lua,
1058    skill_dir: Option<&Path>,
1059    entry_file: Option<&Path>,
1060) -> Result<(), String> {
1061    let context = get_vulcan_context_table(lua)?;
1062
1063    match skill_dir {
1064        Some(path) => context
1065            .set("skill_dir", render_host_visible_path(path))
1066            .map_err(|error| format!("Failed to set vulcan.context.skill_dir: {}", error))?,
1067        None => context
1068            .set("skill_dir", LuaValue::Nil)
1069            .map_err(|error| format!("Failed to clear vulcan.context.skill_dir: {}", error))?,
1070    }
1071
1072    match entry_file {
1073        Some(path) => {
1074            let entry_dir = path.parent().unwrap_or(path);
1075            context
1076                .set("entry_dir", render_host_visible_path(entry_dir))
1077                .map_err(|error| format!("Failed to set vulcan.context.entry_dir: {}", error))?;
1078            context
1079                .set("entry_file", render_host_visible_path(path))
1080                .map_err(|error| format!("Failed to set vulcan.context.entry_file: {}", error))?;
1081        }
1082        None => {
1083            context
1084                .set("entry_dir", LuaValue::Nil)
1085                .map_err(|error| format!("Failed to clear vulcan.context.entry_dir: {}", error))?;
1086            context
1087                .set("entry_file", LuaValue::Nil)
1088                .map_err(|error| format!("Failed to clear vulcan.context.entry_file: {}", error))?;
1089        }
1090    }
1091
1092    Ok(())
1093}
1094
1095/// Populate the current skill dependency roots onto `vulcan.deps`.
1096/// 将当前技能依赖根路径注入到 `vulcan.deps` 中。
1097fn populate_vulcan_dependency_context(
1098    lua: &Lua,
1099    host_options: &LuaRuntimeHostOptions,
1100    skill_dir: Option<&Path>,
1101    skill_id: Option<&str>,
1102) -> Result<(), String> {
1103    let deps = get_vulcan_deps_table(lua)?;
1104
1105    let clear_paths = || -> Result<(), String> {
1106        deps.set("tools_path", LuaValue::Nil)
1107            .map_err(|error| format!("Failed to clear vulcan.deps.tools_path: {}", error))?;
1108        deps.set("lua_path", LuaValue::Nil)
1109            .map_err(|error| format!("Failed to clear vulcan.deps.lua_path: {}", error))?;
1110        deps.set("ffi_path", LuaValue::Nil)
1111            .map_err(|error| format!("Failed to clear vulcan.deps.ffi_path: {}", error))?;
1112        Ok(())
1113    };
1114
1115    let Some(skill_dir) = skill_dir else {
1116        return clear_paths();
1117    };
1118    let Some(skill_id) = skill_id.filter(|value| !value.trim().is_empty()) else {
1119        return clear_paths();
1120    };
1121
1122    let skills_root = skill_dir.parent().ok_or_else(|| {
1123        format!(
1124            "Failed to derive skills root from skill directory {}",
1125            skill_dir.display()
1126        )
1127    })?;
1128    let runtime_root = skills_root.parent().ok_or_else(|| {
1129        format!(
1130            "Failed to derive runtime root from skill directory {}",
1131            skill_dir.display()
1132        )
1133    })?;
1134    let dependency_root = runtime_root.join(host_options.dependency_dir_name.as_str());
1135
1136    deps.set(
1137        "tools_path",
1138        render_host_visible_path(&dependency_root.join("tools").join(skill_id)),
1139    )
1140    .map_err(|error| format!("Failed to set vulcan.deps.tools_path: {}", error))?;
1141    deps.set(
1142        "lua_path",
1143        render_host_visible_path(&dependency_root.join("lua").join(skill_id)),
1144    )
1145    .map_err(|error| format!("Failed to set vulcan.deps.lua_path: {}", error))?;
1146    deps.set(
1147        "ffi_path",
1148        render_host_visible_path(&dependency_root.join("ffi").join(skill_id)),
1149    )
1150    .map_err(|error| format!("Failed to set vulcan.deps.ffi_path: {}", error))?;
1151    Ok(())
1152}
1153
1154/// Capture the internal execution markers currently stored on `vulcan`.
1155/// 捕获当前存放在 `vulcan` 上的内部执行标记。
1156fn capture_vulcan_internal_execution_context(
1157    lua: &Lua,
1158) -> Result<VulcanInternalExecutionContext, String> {
1159    let internal = get_vulcan_runtime_internal_table(lua)?;
1160    let tool_name: Option<String> = internal.get("tool_name").map_err(|error| {
1161        format!(
1162            "Failed to read vulcan.runtime.internal.tool_name: {}",
1163            error
1164        )
1165    })?;
1166    let skill_name: Option<String> = internal.get("skill_name").map_err(|error| {
1167        format!(
1168            "Failed to read vulcan.runtime.internal.skill_name: {}",
1169            error
1170        )
1171    })?;
1172    let luaexec_active: bool = internal.get("luaexec_active").map_err(|error| {
1173        format!(
1174            "Failed to read vulcan.runtime.internal.luaexec_active: {}",
1175            error
1176        )
1177    })?;
1178    let luaexec_caller_tool_name: Option<String> =
1179        internal.get("luaexec_caller_tool_name").map_err(|error| {
1180            format!(
1181                "Failed to read vulcan.runtime.internal.luaexec_caller_tool_name: {}",
1182                error
1183            )
1184        })?;
1185    Ok(VulcanInternalExecutionContext {
1186        tool_name,
1187        skill_name,
1188        luaexec_active,
1189        luaexec_caller_tool_name,
1190    })
1191}
1192
1193/// Populate the internal execution markers stored on `vulcan`.
1194/// 填充存放在 `vulcan` 上的内部执行标记。
1195fn populate_vulcan_internal_execution_context(
1196    lua: &Lua,
1197    context: &VulcanInternalExecutionContext,
1198) -> Result<(), String> {
1199    let internal = get_vulcan_runtime_internal_table(lua)?;
1200
1201    match context.tool_name.as_deref() {
1202        Some(tool_name) => internal.set("tool_name", tool_name).map_err(|error| {
1203            format!("Failed to set vulcan.runtime.internal.tool_name: {}", error)
1204        })?,
1205        None => internal.set("tool_name", LuaValue::Nil).map_err(|error| {
1206            format!(
1207                "Failed to clear vulcan.runtime.internal.tool_name: {}",
1208                error
1209            )
1210        })?,
1211    }
1212
1213    match context.skill_name.as_deref() {
1214        Some(skill_name) => internal.set("skill_name", skill_name).map_err(|error| {
1215            format!(
1216                "Failed to set vulcan.runtime.internal.skill_name: {}",
1217                error
1218            )
1219        })?,
1220        None => internal.set("skill_name", LuaValue::Nil).map_err(|error| {
1221            format!(
1222                "Failed to clear vulcan.runtime.internal.skill_name: {}",
1223                error
1224            )
1225        })?,
1226    }
1227
1228    internal
1229        .set("luaexec_active", context.luaexec_active)
1230        .map_err(|error| {
1231            format!(
1232                "Failed to set vulcan.runtime.internal.luaexec_active: {}",
1233                error
1234            )
1235        })?;
1236
1237    match context.luaexec_caller_tool_name.as_deref() {
1238        Some(tool_name) => internal
1239            .set("luaexec_caller_tool_name", tool_name)
1240            .map_err(|error| {
1241                format!(
1242                    "Failed to set vulcan.runtime.internal.luaexec_caller_tool_name: {}",
1243                    error
1244                )
1245            })?,
1246        None => internal
1247            .set("luaexec_caller_tool_name", LuaValue::Nil)
1248            .map_err(|error| {
1249                format!(
1250                    "Failed to clear vulcan.runtime.internal.luaexec_caller_tool_name: {}",
1251                    error
1252                )
1253            })?,
1254    }
1255
1256    Ok(())
1257}
1258
1259/// Resolve the active skill identifier currently stored in the internal `vulcan` execution context.
1260/// 解析当前存储在内部 `vulcan` 执行上下文中的活动技能标识符。
1261fn current_vulcan_config_skill_id(lua: &Lua, api_name: &str) -> Result<String, mlua::Error> {
1262    let internal = get_vulcan_runtime_internal_table(lua)
1263        .map_err(|error| mlua::Error::runtime(format!("{}: {}", api_name, error)))?;
1264    let skill_name: Option<String> = internal
1265        .get("skill_name")
1266        .map_err(|error| mlua::Error::runtime(format!("{}: {}", api_name, error)))?;
1267    skill_name
1268        .filter(|value| !value.trim().is_empty())
1269        .ok_or_else(|| {
1270            mlua::Error::runtime(format!("{} requires one active skill context", api_name))
1271        })
1272}
1273
1274/// Return whether one help payload should be executed as Lua instead of read as plain text.
1275/// 判断某个帮助载荷是否应按 Lua 执行,而不是按纯文本读取。
1276fn is_lua_help_file(relative_path: &str) -> bool {
1277    Path::new(relative_path)
1278        .extension()
1279        .and_then(|ext| ext.to_str())
1280        .map(|ext| ext.eq_ignore_ascii_case("lua"))
1281        .unwrap_or(false)
1282}
1283
1284/// Read one UTF-8 text file relative to the skill directory.
1285/// 读取相对于 skill 目录的一份 UTF-8 文本文件。
1286fn read_skill_text_file(
1287    skill_dir: &Path,
1288    relative_path: &str,
1289    label: &str,
1290) -> Result<String, String> {
1291    let file_path = skill_dir.join(relative_path);
1292    std::fs::read_to_string(&file_path).map_err(|error| {
1293        format!(
1294            "Failed to read {label} file {}: {}",
1295            file_path.display(),
1296            error
1297        )
1298    })
1299}
1300
1301/// Return the root `vulcan` Lua table.
1302/// 返回根级 `vulcan` Lua 表。
1303fn get_vulcan_table(lua: &Lua) -> Result<Table, String> {
1304    lua.globals()
1305        .get("vulcan")
1306        .map_err(|error| format!("Failed to get vulcan module: {}", error))
1307}
1308
1309/// Return the nested `vulcan.context` Lua table.
1310/// 返回嵌套的 `vulcan.context` Lua 表。
1311fn get_vulcan_context_table(lua: &Lua) -> Result<Table, String> {
1312    let vulcan = get_vulcan_table(lua)?;
1313    vulcan
1314        .get("context")
1315        .map_err(|error| format!("Failed to get vulcan.context: {}", error))
1316}
1317
1318/// Return the nested `vulcan.deps` Lua table.
1319/// 返回嵌套的 `vulcan.deps` Lua 表。
1320fn get_vulcan_deps_table(lua: &Lua) -> Result<Table, String> {
1321    let vulcan = get_vulcan_table(lua)?;
1322    vulcan
1323        .get("deps")
1324        .map_err(|error| format!("Failed to get vulcan.deps: {}", error))
1325}
1326
1327/// Return the nested `vulcan.runtime` Lua table.
1328/// 返回嵌套的 `vulcan.runtime` Lua 表。
1329fn get_vulcan_runtime_table(lua: &Lua) -> Result<Table, String> {
1330    let vulcan = get_vulcan_table(lua)?;
1331    vulcan
1332        .get("runtime")
1333        .map_err(|error| format!("Failed to get vulcan.runtime: {}", error))
1334}
1335
1336/// Return the nested `vulcan.runtime.internal` Lua table.
1337/// 返回嵌套的 `vulcan.runtime.internal` Lua 表。
1338fn get_vulcan_runtime_internal_table(lua: &Lua) -> Result<Table, String> {
1339    let runtime = get_vulcan_runtime_table(lua)?;
1340    runtime
1341        .get("internal")
1342        .map_err(|error| format!("Failed to get vulcan.runtime.internal: {}", error))
1343}
1344
1345/// Return the nested `vulcan.runtime.lua` Lua table.
1346/// 返回嵌套的 `vulcan.runtime.lua` Lua 表。
1347fn get_vulcan_runtime_lua_table(lua: &Lua) -> Result<Table, String> {
1348    let runtime = get_vulcan_runtime_table(lua)?;
1349    runtime
1350        .get("lua")
1351        .map_err(|error| format!("Failed to get vulcan.runtime.lua: {}", error))
1352}
1353
1354/// Snapshot of the mutable core `vulcan` tables that must survive nested-call failures.
1355/// 会在嵌套调用失败后恢复的 `vulcan` 可变核心表快照。
1356#[derive(Clone)]
1357struct VulcanCoreModuleState {
1358    vulcan: Table,
1359    call: Function,
1360    runtime: Table,
1361    runtime_skills: Table,
1362    runtime_internal: Table,
1363    runtime_lua: Table,
1364    fs: Table,
1365    path: Table,
1366    process: Table,
1367    os: Table,
1368    json: Table,
1369    cache: Table,
1370    context: Table,
1371    deps: Table,
1372}
1373
1374impl VulcanCoreModuleState {
1375    /// Capture the currently installed `vulcan` root tables before one nested skill call mutates them.
1376    /// 在一次嵌套技能调用可能修改它们之前,捕获当前安装好的 `vulcan` 根表结构。
1377    fn capture(lua: &Lua) -> Result<Self, String> {
1378        let vulcan = get_vulcan_table(lua)?;
1379        let runtime = get_vulcan_runtime_table(lua)?;
1380        Ok(Self {
1381            call: vulcan
1382                .get("call")
1383                .map_err(|error| format!("Failed to get vulcan.call: {}", error))?,
1384            runtime_skills: runtime
1385                .get("skills")
1386                .map_err(|error| format!("Failed to get vulcan.runtime.skills: {}", error))?,
1387            runtime_internal: runtime
1388                .get("internal")
1389                .map_err(|error| format!("Failed to get vulcan.runtime.internal: {}", error))?,
1390            runtime_lua: runtime
1391                .get("lua")
1392                .map_err(|error| format!("Failed to get vulcan.runtime.lua: {}", error))?,
1393            fs: vulcan
1394                .get("fs")
1395                .map_err(|error| format!("Failed to get vulcan.fs: {}", error))?,
1396            path: vulcan
1397                .get("path")
1398                .map_err(|error| format!("Failed to get vulcan.path: {}", error))?,
1399            process: vulcan
1400                .get("process")
1401                .map_err(|error| format!("Failed to get vulcan.process: {}", error))?,
1402            os: vulcan
1403                .get("os")
1404                .map_err(|error| format!("Failed to get vulcan.os: {}", error))?,
1405            json: vulcan
1406                .get("json")
1407                .map_err(|error| format!("Failed to get vulcan.json: {}", error))?,
1408            cache: vulcan
1409                .get("cache")
1410                .map_err(|error| format!("Failed to get vulcan.cache: {}", error))?,
1411            context: vulcan
1412                .get("context")
1413                .map_err(|error| format!("Failed to get vulcan.context: {}", error))?,
1414            deps: vulcan
1415                .get("deps")
1416                .map_err(|error| format!("Failed to get vulcan.deps: {}", error))?,
1417            vulcan,
1418            runtime,
1419        })
1420    }
1421
1422    /// Reinstall the captured `vulcan` core table topology after one nested call corrupts it.
1423    /// 在嵌套调用破坏表结构后,重新安装捕获到的 `vulcan` 核心表拓扑。
1424    fn restore(&self, lua: &Lua) -> Result<(), String> {
1425        self.runtime
1426            .set("skills", self.runtime_skills.clone())
1427            .map_err(|error| format!("Failed to restore vulcan.runtime.skills: {}", error))?;
1428        self.runtime
1429            .set("internal", self.runtime_internal.clone())
1430            .map_err(|error| format!("Failed to restore vulcan.runtime.internal: {}", error))?;
1431        self.runtime
1432            .set("lua", self.runtime_lua.clone())
1433            .map_err(|error| format!("Failed to restore vulcan.runtime.lua: {}", error))?;
1434        self.vulcan
1435            .set("call", self.call.clone())
1436            .map_err(|error| format!("Failed to restore vulcan.call: {}", error))?;
1437        self.vulcan
1438            .set("runtime", self.runtime.clone())
1439            .map_err(|error| format!("Failed to restore vulcan.runtime: {}", error))?;
1440        self.vulcan
1441            .set("fs", self.fs.clone())
1442            .map_err(|error| format!("Failed to restore vulcan.fs: {}", error))?;
1443        self.vulcan
1444            .set("path", self.path.clone())
1445            .map_err(|error| format!("Failed to restore vulcan.path: {}", error))?;
1446        self.vulcan
1447            .set("process", self.process.clone())
1448            .map_err(|error| format!("Failed to restore vulcan.process: {}", error))?;
1449        self.vulcan
1450            .set("os", self.os.clone())
1451            .map_err(|error| format!("Failed to restore vulcan.os: {}", error))?;
1452        self.vulcan
1453            .set("json", self.json.clone())
1454            .map_err(|error| format!("Failed to restore vulcan.json: {}", error))?;
1455        self.vulcan
1456            .set("cache", self.cache.clone())
1457            .map_err(|error| format!("Failed to restore vulcan.cache: {}", error))?;
1458        self.vulcan
1459            .set("context", self.context.clone())
1460            .map_err(|error| format!("Failed to restore vulcan.context: {}", error))?;
1461        self.vulcan
1462            .set("deps", self.deps.clone())
1463            .map_err(|error| format!("Failed to restore vulcan.deps: {}", error))?;
1464        lua.globals()
1465            .set("vulcan", self.vulcan.clone())
1466            .map_err(|error| format!("Failed to restore global vulcan module: {}", error))?;
1467        Ok(())
1468    }
1469}
1470
1471/// Return the non-empty skill identifier string when the captured value is usable.
1472/// 当捕获到的技能标识可用时,返回其非空字符串引用。
1473fn non_empty_skill_name(value: &str) -> Option<&str> {
1474    if value.trim().is_empty() {
1475        None
1476    } else {
1477        Some(value)
1478    }
1479}
1480
1481/// Clear the transient `__runlua_args` global used by pooled VM requests.
1482/// 清理池化虚拟机请求期间使用的临时 `__runlua_args` 全局变量。
1483fn clear_runlua_args_global(lua: &Lua) -> Result<(), String> {
1484    lua.globals()
1485        .set("__runlua_args", LuaValue::Nil)
1486        .map_err(|error| format!("Failed to clear __runlua_args: {}", error))
1487}
1488
1489/// Reset one pooled Lua VM back to the neutral per-request baseline.
1490/// 将单个池化 Lua 虚拟机重置回中性的单次请求基线状态。
1491fn reset_pooled_vm_request_scope(
1492    lua: &Lua,
1493    host_options: &LuaRuntimeHostOptions,
1494) -> Result<(), String> {
1495    LuaEngine::populate_vulcan_request_context(lua, None)?;
1496    populate_vulcan_internal_execution_context(lua, &VulcanInternalExecutionContext::default())?;
1497    populate_vulcan_file_context(lua, None, None)?;
1498    populate_vulcan_dependency_context(lua, host_options, None, None)?;
1499    LuaEngine::populate_vulcan_lancedb_context(lua, None, None)?;
1500    LuaEngine::populate_vulcan_sqlite_context(lua, None, None)?;
1501    clear_runlua_args_global(lua)?;
1502    Ok(())
1503}
1504
1505/// One RAII guard that keeps pooled Lua VM request-scoped state isolated.
1506/// 一个用于保持池化 Lua 虚拟机请求级状态隔离的 RAII 守卫。
1507struct LuaVmRequestScopeGuard<'a> {
1508    lease: &'a mut LuaVmLease,
1509    host_options: &'a LuaRuntimeHostOptions,
1510    active: bool,
1511}
1512
1513impl<'a> LuaVmRequestScopeGuard<'a> {
1514    /// Normalize one pooled VM before use and arm cleanup for all exit paths.
1515    /// 在使用前归一化单个池化虚拟机,并为全部退出路径启用清理保护。
1516    fn new(
1517        lease: &'a mut LuaVmLease,
1518        host_options: &'a LuaRuntimeHostOptions,
1519    ) -> Result<Self, String> {
1520        let mut guard = Self {
1521            lease,
1522            host_options,
1523            active: true,
1524        };
1525        if let Err(error) = reset_pooled_vm_request_scope(guard.lua(), host_options) {
1526            guard.lease.discard();
1527            guard.active = false;
1528            return Err(error);
1529        }
1530        Ok(guard)
1531    }
1532
1533    /// Borrow the guarded Lua VM while the request scope is active.
1534    /// 在请求作用域激活期间借用受守卫保护的 Lua 虚拟机。
1535    fn lua(&self) -> &Lua {
1536        self.lease.lua()
1537    }
1538
1539    /// Explicitly finish the request scope and surface cleanup errors to the caller.
1540    /// 显式结束当前请求作用域,并将清理错误返回给调用方。
1541    fn finish(mut self) -> Result<(), String> {
1542        let cleanup_result = reset_pooled_vm_request_scope(self.lua(), self.host_options);
1543        if let Err(error) = cleanup_result {
1544            self.lease.discard();
1545            self.active = false;
1546            return Err(error);
1547        }
1548        self.active = false;
1549        Ok(())
1550    }
1551}
1552
1553impl Drop for LuaVmRequestScopeGuard<'_> {
1554    fn drop(&mut self) {
1555        if !self.active {
1556            return;
1557        }
1558        if let Err(error) = reset_pooled_vm_request_scope(self.lua(), self.host_options) {
1559            log_error(format!(
1560                "[LuaSkill:error] Failed to reset pooled Lua VM request scope: {}",
1561                error
1562            ));
1563            self.lease.discard();
1564        }
1565    }
1566}
1567
1568/// RAII guard that restores the outer `vulcan` execution context after one nested `vulcan.call`.
1569/// 在一次嵌套 `vulcan.call` 之后恢复外层 `vulcan` 执行上下文的 RAII 守卫。
1570struct LuaNestedCallScopeGuard {
1571    lua: Lua,
1572    host_options: Arc<LuaRuntimeHostOptions>,
1573    lancedb_host: Option<Arc<LanceDbSkillHost>>,
1574    sqlite_host: Option<Arc<SqliteSkillHost>>,
1575    core_state: VulcanCoreModuleState,
1576    previous_context: LuaValue,
1577    previous_client_info: LuaValue,
1578    previous_client_capabilities: LuaValue,
1579    previous_client_budget: LuaValue,
1580    previous_tool_config: LuaValue,
1581    previous_lancedb_skill_name: String,
1582    previous_sqlite_skill_name: String,
1583    previous_internal_context: VulcanInternalExecutionContext,
1584    previous_file_context: (Option<String>, Option<String>, Option<String>),
1585    active: bool,
1586}
1587
1588impl LuaNestedCallScopeGuard {
1589    /// Capture the current outer `vulcan` execution state before entering one nested skill.
1590    /// 在进入一次嵌套技能调用之前捕获当前外层 `vulcan` 执行状态。
1591    fn new(
1592        lua: &Lua,
1593        host_options: Arc<LuaRuntimeHostOptions>,
1594        lancedb_host: Option<Arc<LanceDbSkillHost>>,
1595        sqlite_host: Option<Arc<SqliteSkillHost>>,
1596    ) -> Result<Self, String> {
1597        let vulcan = get_vulcan_table(lua)?;
1598        let context_table = get_vulcan_context_table(lua)?;
1599        Ok(Self {
1600            lua: lua.clone(),
1601            host_options,
1602            lancedb_host,
1603            sqlite_host,
1604            core_state: VulcanCoreModuleState::capture(lua)?,
1605            previous_context: context_table
1606                .get("request")
1607                .map_err(|error| format!("Failed to read vulcan.context.request: {}", error))?,
1608            previous_client_info: context_table
1609                .get("client_info")
1610                .map_err(|error| format!("Failed to read vulcan.context.client_info: {}", error))?,
1611            previous_client_capabilities: context_table.get("client_capabilities").map_err(
1612                |error| {
1613                    format!(
1614                        "Failed to read vulcan.context.client_capabilities: {}",
1615                        error
1616                    )
1617                },
1618            )?,
1619            previous_client_budget: context_table.get("client_budget").map_err(|error| {
1620                format!("Failed to read vulcan.context.client_budget: {}", error)
1621            })?,
1622            previous_tool_config: context_table
1623                .get("tool_config")
1624                .map_err(|error| format!("Failed to read vulcan.context.tool_config: {}", error))?,
1625            previous_lancedb_skill_name: vulcan.get("__lancedb_skill_name").unwrap_or_default(),
1626            previous_sqlite_skill_name: vulcan.get("__sqlite_skill_name").unwrap_or_default(),
1627            previous_internal_context: capture_vulcan_internal_execution_context(lua)?,
1628            previous_file_context: capture_vulcan_file_context(lua)?,
1629            active: true,
1630        })
1631    }
1632
1633    /// Switch the current Lua VM into the nested skill request context.
1634    /// 把当前 Lua 虚拟机切换到嵌套技能请求上下文。
1635    fn enter_nested_call(
1636        &self,
1637        dispatch_entry_display_name: &str,
1638        owner_skill_name: &str,
1639        owner_skill_dir: &str,
1640        entry_path: &str,
1641        nested_invocation_context: &LuaInvocationContext,
1642        target_lancedb_binding: Option<Arc<LanceDbSkillBinding>>,
1643        target_sqlite_binding: Option<Arc<SqliteSkillBinding>>,
1644    ) -> Result<(), String> {
1645        LuaEngine::populate_vulcan_request_context(&self.lua, Some(nested_invocation_context))?;
1646        populate_vulcan_internal_execution_context(
1647            &self.lua,
1648            &VulcanInternalExecutionContext {
1649                tool_name: Some(dispatch_entry_display_name.to_string()),
1650                skill_name: Some(owner_skill_name.to_string()),
1651                luaexec_active: self.previous_internal_context.luaexec_active,
1652                luaexec_caller_tool_name: self
1653                    .previous_internal_context
1654                    .luaexec_caller_tool_name
1655                    .clone(),
1656            },
1657        )?;
1658        populate_vulcan_file_context(
1659            &self.lua,
1660            Some(Path::new(owner_skill_dir)),
1661            Some(Path::new(entry_path)),
1662        )?;
1663        populate_vulcan_dependency_context(
1664            &self.lua,
1665            self.host_options.as_ref(),
1666            Some(Path::new(owner_skill_dir)),
1667            Some(owner_skill_name),
1668        )?;
1669        LuaEngine::populate_vulcan_lancedb_context(
1670            &self.lua,
1671            target_lancedb_binding,
1672            Some(owner_skill_name),
1673        )?;
1674        LuaEngine::populate_vulcan_sqlite_context(
1675            &self.lua,
1676            target_sqlite_binding,
1677            Some(owner_skill_name),
1678        )?;
1679        Ok(())
1680    }
1681
1682    /// Restore the outer `vulcan` execution state captured before the nested call began.
1683    /// 恢复嵌套调用开始前捕获到的外层 `vulcan` 执行状态。
1684    fn restore_previous_state(&self) -> Result<(), String> {
1685        self.core_state.restore(&self.lua)?;
1686        let restore_lancedb_binding = match non_empty_skill_name(&self.previous_lancedb_skill_name)
1687        {
1688            Some(skill_name) => self
1689                .lancedb_host
1690                .as_ref()
1691                .map(|host| host.binding_for_skill(skill_name))
1692                .transpose()?
1693                .flatten(),
1694            None => None,
1695        };
1696        let restore_sqlite_binding = match non_empty_skill_name(&self.previous_sqlite_skill_name) {
1697            Some(skill_name) => self
1698                .sqlite_host
1699                .as_ref()
1700                .map(|host| host.binding_for_skill(skill_name))
1701                .transpose()?
1702                .flatten(),
1703            None => None,
1704        };
1705        LuaEngine::populate_vulcan_lancedb_context(
1706            &self.lua,
1707            restore_lancedb_binding,
1708            non_empty_skill_name(&self.previous_lancedb_skill_name),
1709        )?;
1710        LuaEngine::populate_vulcan_sqlite_context(
1711            &self.lua,
1712            restore_sqlite_binding,
1713            non_empty_skill_name(&self.previous_sqlite_skill_name),
1714        )?;
1715        let context_table = get_vulcan_context_table(&self.lua)?;
1716        context_table
1717            .set("request", self.previous_context.clone())
1718            .map_err(|error| format!("Failed to restore vulcan.context.request: {}", error))?;
1719        context_table
1720            .set("client_info", self.previous_client_info.clone())
1721            .map_err(|error| format!("Failed to restore vulcan.context.client_info: {}", error))?;
1722        context_table
1723            .set(
1724                "client_capabilities",
1725                self.previous_client_capabilities.clone(),
1726            )
1727            .map_err(|error| {
1728                format!(
1729                    "Failed to restore vulcan.context.client_capabilities: {}",
1730                    error
1731                )
1732            })?;
1733        context_table
1734            .set("client_budget", self.previous_client_budget.clone())
1735            .map_err(|error| {
1736                format!("Failed to restore vulcan.context.client_budget: {}", error)
1737            })?;
1738        context_table
1739            .set("tool_config", self.previous_tool_config.clone())
1740            .map_err(|error| format!("Failed to restore vulcan.context.tool_config: {}", error))?;
1741        populate_vulcan_internal_execution_context(&self.lua, &self.previous_internal_context)?;
1742        populate_vulcan_file_context(
1743            &self.lua,
1744            self.previous_file_context.0.as_deref().map(Path::new),
1745            self.previous_file_context.2.as_deref().map(Path::new),
1746        )?;
1747        populate_vulcan_dependency_context(
1748            &self.lua,
1749            self.host_options.as_ref(),
1750            self.previous_file_context.0.as_deref().map(Path::new),
1751            self.previous_internal_context.skill_name.as_deref(),
1752        )?;
1753        Ok(())
1754    }
1755
1756    /// Explicitly finish the nested-call scope and surface any restore failure to the caller.
1757    /// 显式结束嵌套调用作用域,并把恢复失败信息返回给调用方。
1758    fn finish(mut self) -> Result<(), String> {
1759        let restore_result = self.restore_previous_state();
1760        self.active = false;
1761        restore_result
1762    }
1763}
1764
1765impl Drop for LuaNestedCallScopeGuard {
1766    fn drop(&mut self) {
1767        if !self.active {
1768            return;
1769        }
1770        if let Err(error) = self.restore_previous_state() {
1771            log_error(format!(
1772                "[LuaSkill:error] Failed to restore nested vulcan.call context: {}",
1773                error
1774            ));
1775        }
1776    }
1777}
1778
1779/// Checked-out VM guard that returns the VM back into the pool on drop.
1780/// 已借出的虚拟机守卫,在释放时会自动归还到池中。
1781struct LuaVmLease {
1782    pool: Arc<LuaVmPool>,
1783    vm: Option<LuaVm>,
1784}
1785
1786impl LuaVmLease {
1787    /// Borrow the underlying Lua VM immutably for the duration of the lease.
1788    /// 在租约生命周期内以只读方式借用底层 Lua 虚拟机。
1789    fn lua(&self) -> &Lua {
1790        &self.vm.as_ref().expect("lua vm lease missing instance").lua
1791    }
1792
1793    /// Permanently retire the currently leased VM instead of returning it to the pool.
1794    /// 永久淘汰当前租出的虚拟机,而不是把它放回池中。
1795    fn discard(&mut self) {
1796        if let Some(vm) = self.vm.take() {
1797            self.pool.discard(vm);
1798        }
1799    }
1800}
1801
1802impl Drop for LuaVmLease {
1803    fn drop(&mut self) {
1804        if let Some(mut vm) = self.vm.take() {
1805            vm.last_used_at = Instant::now();
1806            self.pool.release(vm);
1807        }
1808    }
1809}
1810
1811impl LuaVmPool {
1812    /// Create a new empty Lua VM pool.
1813    /// 创建一个新的空 Lua 虚拟机池。
1814    fn new(config: LuaVmPoolConfig) -> Self {
1815        Self {
1816            config: config.normalized(),
1817            state: Mutex::new(LuaVmPoolState {
1818                available: Vec::new(),
1819                total_count: 0,
1820            }),
1821            condvar: Condvar::new(),
1822        }
1823    }
1824
1825    /// Prewarm the pool to the configured minimum size.
1826    /// 预热到配置要求的最小虚拟机数量。
1827    fn prewarm<F>(&self, mut factory: F) -> Result<(), String>
1828    where
1829        F: FnMut() -> Result<LuaVm, String>,
1830    {
1831        while self.total_count() < self.config.min_size {
1832            {
1833                let mut state = self.state.lock().unwrap();
1834                state.total_count += 1;
1835            }
1836            match factory() {
1837                Ok(vm) => self.release(vm),
1838                Err(error) => {
1839                    let mut state = self.state.lock().unwrap();
1840                    state.total_count = state.total_count.saturating_sub(1);
1841                    return Err(error);
1842                }
1843            }
1844        }
1845        Ok(())
1846    }
1847
1848    /// Acquire a VM from the pool, growing on demand up to the configured limit.
1849    /// 从池中获取虚拟机,并在未达上限时按需扩容。
1850    fn acquire<F>(self: &Arc<Self>, mut factory: F) -> Result<LuaVmLease, String>
1851    where
1852        F: FnMut() -> Result<LuaVm, String>,
1853    {
1854        loop {
1855            let mut state = self.state.lock().unwrap();
1856            self.reap_idle_locked(&mut state);
1857
1858            if let Some(mut vm) = state.available.pop() {
1859                vm.last_used_at = Instant::now();
1860                return Ok(LuaVmLease {
1861                    pool: self.clone(),
1862                    vm: Some(vm),
1863                });
1864            }
1865
1866            if state.total_count < self.config.max_size {
1867                state.total_count += 1;
1868                drop(state);
1869                match factory() {
1870                    Ok(vm) => {
1871                        return Ok(LuaVmLease {
1872                            pool: self.clone(),
1873                            vm: Some(vm),
1874                        });
1875                    }
1876                    Err(error) => {
1877                        let mut state = self.state.lock().unwrap();
1878                        state.total_count = state.total_count.saturating_sub(1);
1879                        self.condvar.notify_one();
1880                        return Err(error);
1881                    }
1882                }
1883            }
1884
1885            let _guard = self.condvar.wait(state).unwrap();
1886        }
1887    }
1888
1889    /// Return a VM back into the pool.
1890    /// 将虚拟机归还到池中。
1891    fn release(&self, vm: LuaVm) {
1892        let mut state = self.state.lock().unwrap();
1893        state.available.push(vm);
1894        self.reap_idle_locked(&mut state);
1895        self.condvar.notify_one();
1896    }
1897
1898    /// Retire one broken VM so later borrowers receive a fresh instance instead of stale state.
1899    /// 退役一个已损坏的虚拟机,确保后续借用方拿到的是新实例而不是陈旧状态。
1900    fn discard(&self, _vm: LuaVm) {
1901        let mut state = self.state.lock().unwrap();
1902        if state.total_count > 0 {
1903            state.total_count -= 1;
1904        }
1905        self.condvar.notify_one();
1906    }
1907
1908    /// Return the current total number of VMs in the pool.
1909    /// 返回当前池中的虚拟机总数。
1910    fn total_count(&self) -> usize {
1911        self.state.lock().unwrap().total_count
1912    }
1913
1914    /// Reap idle available VMs while respecting the minimum pool size.
1915    /// 在保证最小池规模的前提下回收空闲虚拟机。
1916    fn reap_idle_locked(&self, state: &mut LuaVmPoolState) {
1917        if state.total_count <= self.config.min_size {
1918            return;
1919        }
1920
1921        let idle_limit = Duration::from_secs(self.config.idle_ttl_secs);
1922        let now = Instant::now();
1923        let mut index = 0usize;
1924        while index < state.available.len() && state.total_count > self.config.min_size {
1925            let should_remove = now
1926                .checked_duration_since(state.available[index].last_used_at)
1927                .map(|idle| idle >= idle_limit)
1928                .unwrap_or(false);
1929            if should_remove {
1930                state.available.swap_remove(index);
1931                state.total_count = state.total_count.saturating_sub(1);
1932            } else {
1933                index += 1;
1934            }
1935        }
1936    }
1937}
1938
1939/// Parse Lua multi-return values into the host's unified string-result protocol.
1940/// 把 Lua 工具的多返回值解析为宿主统一字符串结果协议。
1941fn parse_tool_call_output(
1942    values: MultiValue,
1943    display_name: &str,
1944) -> Result<RuntimeInvocationResult, String> {
1945    let values_vec: Vec<LuaValue> = values.into_vec();
1946    if values_vec.is_empty() {
1947        return Err(format!(
1948            "Lua skill '{}' must return a plain string result",
1949            display_name
1950        ));
1951    }
1952
1953    if values_vec.len() > 3 {
1954        return Err(format!(
1955            "Lua skill '{}' must return content[, overflow_mode[, template_hint]]",
1956            display_name
1957        ));
1958    }
1959
1960    let content = match &values_vec[0] {
1961        LuaValue::String(text) => text
1962            .to_str()
1963            .map_err(|error| {
1964                format!(
1965                    "Lua skill '{}' returned an invalid UTF-8 string: {}",
1966                    display_name, error
1967                )
1968            })?
1969            .to_string(),
1970        other => {
1971            return Err(format!(
1972                "{} (skill='{}', actual_type='{}')",
1973                NON_STRING_TOOL_RESULT_ERROR,
1974                display_name,
1975                lua_value_type_name(other)
1976            ));
1977        }
1978    };
1979
1980    let overflow_mode = match values_vec.get(1) {
1981        None | Some(LuaValue::Nil) => None,
1982        Some(LuaValue::String(text)) => {
1983            let mode_text = text.to_str().map_err(|error| {
1984                format!(
1985                    "Lua skill '{}' returned an invalid overflow mode string: {}",
1986                    display_name, error
1987                )
1988            })?;
1989            Some(ToolOverflowMode::parse(&mode_text).ok_or_else(|| {
1990                format!(
1991                    "Lua skill '{}' returned an unsupported overflow mode: {}",
1992                    display_name, mode_text
1993                )
1994            })?)
1995        }
1996        Some(_) => {
1997            return Err(format!(
1998                "Lua skill '{}' must return overflow mode as a string constant",
1999                display_name
2000            ));
2001        }
2002    };
2003
2004    let template_hint = match values_vec.get(2) {
2005        None | Some(LuaValue::Nil) => None,
2006        Some(LuaValue::String(text)) => {
2007            let name = text.to_str().map_err(|error| {
2008                format!(
2009                    "Lua skill '{}' returned an invalid template name: {}",
2010                    display_name, error
2011                )
2012            })?;
2013            let trimmed = name.trim();
2014            if trimmed.is_empty() {
2015                None
2016            } else {
2017                Some(trimmed.to_string())
2018            }
2019        }
2020        Some(_) => {
2021            return Err(format!(
2022                "Lua skill '{}' must return template_hint as a string",
2023                display_name
2024            ));
2025        }
2026    };
2027
2028    Ok(RuntimeInvocationResult::from_content_parts(
2029        content,
2030        overflow_mode,
2031        template_hint,
2032    ))
2033}
2034
2035impl LuaEngine {
2036    /// Return the reference root used for host-managed fallback directories when no explicit host path is provided.
2037    /// 在未显式提供宿主管理路径时返回用于回退目录计算的参考根目录。
2038    fn reference_skill_root<'a>(
2039        &self,
2040        skill_roots: &'a [RuntimeSkillRoot],
2041    ) -> Result<&'a RuntimeSkillRoot, String> {
2042        skill_roots
2043            .iter()
2044            .rev()
2045            .find(|root| root.skills_dir.exists() && root.skills_dir.is_dir())
2046            .ok_or_else(|| "at least one skill root is required".to_string())
2047    }
2048
2049    /// Resolve the canonical runtime root used by the unified skill-config file.
2050    /// 解析统一技能配置文件所使用的规范运行时根目录。
2051    fn canonical_skill_config_runtime_root(
2052        &self,
2053        skill_roots: &[RuntimeSkillRoot],
2054    ) -> Result<PathBuf, String> {
2055        let mut candidates: Vec<PathBuf> = Vec::new();
2056        for skill_root in skill_roots {
2057            let candidate = normalize_runtime_root_path(&self.runtime_root_for(skill_root));
2058            if !candidates.iter().any(|existing| existing == &candidate) {
2059                candidates.push(candidate);
2060            }
2061        }
2062        match candidates.len() {
2063            0 => Err("at least one skill root is required to resolve the unified skill config path".to_string()),
2064            1 => Ok(candidates.remove(0)),
2065            _ => Err(
2066                "multiple runtime roots map to different parents; set host_options.skill_config_file_path explicitly".to_string()
2067            ),
2068        }
2069    }
2070
2071    /// Create a new LuaEngine with LuaJIT VM and registered globals.
2072    pub fn new(options: LuaEngineOptions) -> Result<Self, Box<dyn std::error::Error>> {
2073        let runlua_pool_config = options
2074            .host_options
2075            .runlua_pool_config
2076            .map(|config| LuaVmPoolConfig {
2077                min_size: config.min_size,
2078                max_size: config.max_size,
2079                idle_ttl_secs: config.idle_ttl_secs,
2080            })
2081            .unwrap_or_else(default_runlua_vm_pool_config);
2082        configure_global_tool_cache(
2083            options
2084                .host_options
2085                .cache_config
2086                .clone()
2087                .unwrap_or_else(ToolCacheConfig::default),
2088        );
2089        let database_provider_callbacks = Arc::new(
2090            RuntimeDatabaseProviderCallbacks::capture_process_defaults()
2091                .map_err(std::io::Error::other)?,
2092        );
2093        Ok(Self {
2094            skills: HashMap::new(),
2095            entry_registry: BTreeMap::new(),
2096            pool: Arc::new(LuaVmPool::new(options.pool_config)),
2097            runlua_pool: Arc::new(LuaVmPool::new(runlua_pool_config)),
2098            skill_config_store: Arc::new(
2099                SkillConfigStore::new(options.host_options.skill_config_file_path.clone())
2100                    .map_err(std::io::Error::other)?,
2101            ),
2102            lancedb_host: None,
2103            sqlite_host: None,
2104            database_provider_callbacks,
2105            host_options: Arc::new(options.host_options),
2106        })
2107    }
2108
2109    /// Build the shared runtime root used by host-managed sibling directories.
2110    /// 构造宿主管理同级目录所使用的共享运行时根目录。
2111    fn runtime_root_for(&self, skill_root: &RuntimeSkillRoot) -> PathBuf {
2112        skill_root
2113            .skills_dir
2114            .parent()
2115            .map(Path::to_path_buf)
2116            .unwrap_or_else(|| skill_root.skills_dir.clone())
2117    }
2118
2119    /// Build the sibling state root for one named skill root.
2120    /// 为单个命名技能根构造同级状态根目录。
2121    fn state_root_for(&self, skill_root: &RuntimeSkillRoot) -> PathBuf {
2122        self.runtime_root_for(skill_root)
2123            .join(self.host_options.state_dir_name.as_str())
2124    }
2125
2126    /// Build the sibling dependency root for one named skill root.
2127    /// 为单个命名技能根构造同级依赖根目录。
2128    fn dependency_root_for(&self, skill_root: &RuntimeSkillRoot) -> PathBuf {
2129        self.runtime_root_for(skill_root)
2130            .join(self.host_options.dependency_dir_name.as_str())
2131    }
2132
2133    /// Return whether the host policy forces one skill identifier to be ignored.
2134    /// 返回宿主策略是否强制忽略指定技能标识符。
2135    fn is_host_ignored_skill(&self, skill_id: &str) -> bool {
2136        self.host_options
2137            .ignored_skill_ids
2138            .iter()
2139            .any(|ignored| ignored.trim() == skill_id)
2140    }
2141
2142    /// Build the sibling database root for one named skill root.
2143    /// 为单个命名技能根构造同级数据库根目录。
2144    fn database_root_for(&self, skill_root: &RuntimeSkillRoot) -> PathBuf {
2145        self.runtime_root_for(skill_root)
2146            .join(self.host_options.database_dir_name.as_str())
2147    }
2148
2149    /// Capture the shared runtime root used by the unified skill config file.
2150    /// 记录统一技能配置文件所使用的共享运行时根目录。
2151    fn refresh_skill_config_runtime_root(
2152        &self,
2153        skill_roots: &[RuntimeSkillRoot],
2154    ) -> Result<(), String> {
2155        if self.skill_config_store.has_explicit_file_path() {
2156            return Ok(());
2157        }
2158        let runtime_root = self.canonical_skill_config_runtime_root(skill_roots)?;
2159        self.skill_config_store
2160            .set_default_runtime_root(&runtime_root)
2161    }
2162
2163    /// Build the dependency-manager configuration for one named skill root.
2164    /// 为单个命名技能根构造依赖管理器配置。
2165    fn dependency_manager_config_for(
2166        &self,
2167        skill_root: &RuntimeSkillRoot,
2168    ) -> Result<DependencyManagerConfig, String> {
2169        let runtime_root = skill_root
2170            .skills_dir
2171            .parent()
2172            .map(Path::to_path_buf)
2173            .unwrap_or_else(|| skill_root.skills_dir.clone());
2174        let dependency_root = self.dependency_root_for(skill_root);
2175        let tool_root = dependency_root.join("tools");
2176        let host_tool_root = self
2177            .host_options
2178            .host_provided_tool_root
2179            .clone()
2180            .unwrap_or_else(|| runtime_root.join("bin").join("tools"));
2181        let lua_root = dependency_root.join("lua");
2182        let host_lua_root = self
2183            .host_options
2184            .host_provided_lua_root
2185            .clone()
2186            .or_else(|| self.host_options.lua_packages_dir.clone())
2187            .unwrap_or_else(|| runtime_root.join("lua_packages"));
2188        let ffi_root = dependency_root.join("ffi");
2189        let host_ffi_root = self
2190            .host_options
2191            .host_provided_ffi_root
2192            .clone()
2193            .or_else(|| {
2194                self.host_options
2195                    .lancedb_library_path
2196                    .as_ref()
2197                    .and_then(|path| path.parent().map(Path::to_path_buf))
2198            })
2199            .or_else(|| {
2200                self.host_options
2201                    .sqlite_library_path
2202                    .as_ref()
2203                    .and_then(|path| path.parent().map(Path::to_path_buf))
2204            })
2205            .unwrap_or_else(|| runtime_root.join("libs"));
2206        let download_cache_root = self
2207            .host_options
2208            .download_cache_root
2209            .clone()
2210            .unwrap_or_else(|| runtime_root.join("temp").join("downloads"));
2211
2212        ensure_directory(&tool_root)?;
2213        ensure_directory(&host_tool_root)?;
2214        ensure_directory(&lua_root)?;
2215        ensure_directory(&host_lua_root)?;
2216        ensure_directory(&ffi_root)?;
2217        ensure_directory(&host_ffi_root)?;
2218        ensure_directory(&download_cache_root)?;
2219
2220        Ok(DependencyManagerConfig {
2221            tool_root,
2222            host_tool_root,
2223            lua_root,
2224            host_lua_root,
2225            ffi_root,
2226            host_ffi_root,
2227            download_cache_root,
2228            allow_network_download: self.host_options.allow_network_download,
2229            github_base_url: self.host_options.github_base_url.clone(),
2230            github_api_base_url: self.host_options.github_api_base_url.clone(),
2231        })
2232    }
2233
2234    /// Build the skill-manager configuration for one named skill root.
2235    /// 为单个命名技能根构造技能管理器配置。
2236    fn skill_manager_for(&self, skill_root: &RuntimeSkillRoot) -> Result<SkillManager, String> {
2237        let state_root = self.state_root_for(skill_root);
2238        let dependency_config = self.dependency_manager_config_for(skill_root)?;
2239        ensure_directory(&state_root)?;
2240        Ok(SkillManager::new(SkillManagerConfig {
2241            skill_root: skill_root.clone(),
2242            lifecycle_root: state_root,
2243            protection: self.host_options.protection.clone(),
2244            download_cache_root: dependency_config.download_cache_root,
2245            allow_network_download: dependency_config.allow_network_download,
2246            github_base_url: dependency_config.github_base_url,
2247            github_api_base_url: dependency_config.github_api_base_url,
2248        }))
2249    }
2250
2251    /// Ensure dependencies declared by one skill directory are installed before the skill is loaded.
2252    /// 在真正加载 skill 前确保该目录声明的依赖已经安装完成。
2253    fn ensure_skill_dependencies(
2254        &self,
2255        skill_root: &RuntimeSkillRoot,
2256        skill_dir: &Path,
2257    ) -> Result<(), String> {
2258        let dependencies_path = skill_dir.join("dependencies.yaml");
2259        if !dependencies_path.exists() {
2260            return Ok(());
2261        }
2262
2263        let manifest = SkillDependencyManifest::load_from_path(&dependencies_path)?;
2264        if manifest.is_empty() {
2265            return Ok(());
2266        }
2267
2268        let skill_name = skill_dir
2269            .file_name()
2270            .and_then(|value| value.to_str())
2271            .unwrap_or("unknown-skill");
2272        let manager = DependencyManager::new(self.dependency_manager_config_for(skill_root)?);
2273        manager.ensure_skill_dependencies(skill_name, &manifest)
2274    }
2275
2276    /// Load one optional dependency manifest from one skill directory when the file exists.
2277    /// 当依赖文件存在时,从单个技能目录加载可选依赖清单。
2278    fn load_skill_dependency_manifest(
2279        &self,
2280        skill_dir: &Path,
2281    ) -> Result<Option<SkillDependencyManifest>, String> {
2282        let dependencies_path = skill_dir.join("dependencies.yaml");
2283        if !dependencies_path.exists() {
2284            return Ok(None);
2285        }
2286        SkillDependencyManifest::load_from_path(&dependencies_path).map(Some)
2287    }
2288
2289    /// Resolve final canonical entry names for all loaded skills with stable collision indexing.
2290    /// 为全部已加载 skill 解析最终 canonical 入口名,并以稳定顺序处理冲突编号。
2291    fn rebuild_entry_registry(&mut self) -> Result<(), String> {
2292        /// One unresolved entry candidate collected before collision indexing.
2293        /// 冲突编号前收集到的单个未解析入口候选项。
2294        #[derive(Clone)]
2295        struct EntrySeed {
2296            /// Internal storage key of the owning loaded skill.
2297            /// 所属已加载 skill 的内部存储键。
2298            skill_storage_key: String,
2299            /// Stable skill identifier declared in metadata.
2300            /// 元数据中声明的稳定 skill 标识符。
2301            skill_id: String,
2302            /// Stable local entry name declared by the skill.
2303            /// skill 声明的稳定局部入口名称。
2304            local_name: String,
2305            /// Unresolved `skill-entry` base name before numeric suffixing.
2306            /// 添加数字后缀前的未解析 `skill-entry` 基础名称。
2307            base_name: String,
2308            /// Deterministic tie-breaker based on directory basename.
2309            /// 基于目录基名的确定性并列打破键。
2310            directory_name: String,
2311            /// Module name used as the final low-level tie-breaker.
2312            /// 作为最终并列打破条件的模块名称。
2313            module_name: String,
2314        }
2315
2316        let mut seeds = Vec::new();
2317        for (skill_storage_key, skill) in &self.skills {
2318            for tool in skill.meta.entries() {
2319                let local_name = tool.name.trim().to_string();
2320                if seeds.iter().any(|seed: &EntrySeed| {
2321                    seed.skill_storage_key == *skill_storage_key && seed.local_name == local_name
2322                }) {
2323                    return Err(format!(
2324                        "skill '{}' declares duplicate local entry name '{}'",
2325                        skill.meta.effective_skill_id(),
2326                        local_name
2327                    ));
2328                }
2329
2330                let directory_name = skill
2331                    .dir
2332                    .file_name()
2333                    .and_then(|value| value.to_str())
2334                    .unwrap_or_default()
2335                    .to_string();
2336                seeds.push(EntrySeed {
2337                    skill_storage_key: skill_storage_key.clone(),
2338                    skill_id: skill.meta.effective_skill_id().to_string(),
2339                    local_name: local_name.clone(),
2340                    base_name: skill.meta.tool_base_name(tool),
2341                    directory_name,
2342                    module_name: tool.lua_module.clone(),
2343                });
2344            }
2345        }
2346
2347        seeds.sort_by(|left, right| {
2348            (
2349                left.base_name.as_str(),
2350                left.directory_name.as_str(),
2351                left.skill_id.as_str(),
2352                left.local_name.as_str(),
2353                left.module_name.as_str(),
2354            )
2355                .cmp(&(
2356                    right.base_name.as_str(),
2357                    right.directory_name.as_str(),
2358                    right.skill_id.as_str(),
2359                    right.local_name.as_str(),
2360                    right.module_name.as_str(),
2361                ))
2362        });
2363
2364        for skill in self.skills.values_mut() {
2365            skill.resolved_entry_names.clear();
2366        }
2367
2368        let mut registry = BTreeMap::new();
2369        let mut base_name_counters = HashMap::<String, usize>::new();
2370        let mut occupied_names = self
2371            .host_options
2372            .reserved_entry_names
2373            .iter()
2374            .cloned()
2375            .collect::<HashSet<String>>();
2376        for seed in seeds {
2377            let mut duplicate_index = *base_name_counters.get(&seed.base_name).unwrap_or(&0usize);
2378            let canonical_name = loop {
2379                duplicate_index += 1;
2380                let candidate_name = if duplicate_index == 1 {
2381                    seed.base_name.clone()
2382                } else {
2383                    format!("{}-{}", seed.base_name, duplicate_index)
2384                };
2385                if !occupied_names.contains(&candidate_name) {
2386                    break candidate_name;
2387                }
2388            };
2389            base_name_counters.insert(seed.base_name.clone(), duplicate_index);
2390            occupied_names.insert(canonical_name.clone());
2391
2392            let resolved_target = ResolvedEntryTarget {
2393                canonical_name: canonical_name.clone(),
2394                skill_storage_key: seed.skill_storage_key.clone(),
2395                skill_id: seed.skill_id.clone(),
2396                local_name: seed.local_name.clone(),
2397            };
2398            registry.insert(canonical_name.clone(), resolved_target);
2399
2400            let skill = self
2401                .skills
2402                .get_mut(&seed.skill_storage_key)
2403                .ok_or_else(|| {
2404                    format!(
2405                        "internal error: missing loaded skill '{}' while building entry registry",
2406                        seed.skill_storage_key
2407                    )
2408                })?;
2409            skill
2410                .resolved_entry_names
2411                .insert(seed.local_name.clone(), canonical_name);
2412        }
2413
2414        self.entry_registry = registry;
2415        Ok(())
2416    }
2417
2418    /// Load skills from directories. `base_dir` is the system skill directory,
2419    /// `override_dir` is the user override directory (if any).
2420    pub fn load_from_dirs(
2421        &mut self,
2422        base_dir: &Path,
2423        override_dir: Option<&Path>,
2424    ) -> Result<(), Box<dyn std::error::Error>> {
2425        let mut skill_roots = Vec::new();
2426        if let Some(override_dir) = override_dir {
2427            skill_roots.push(RuntimeSkillRoot {
2428                name: "OVERRIDE".to_string(),
2429                skills_dir: override_dir.to_path_buf(),
2430            });
2431        }
2432        skill_roots.push(RuntimeSkillRoot {
2433            name: "ROOT".to_string(),
2434            skills_dir: base_dir.to_path_buf(),
2435        });
2436        self.load_from_roots(&skill_roots)
2437    }
2438
2439    /// Load skills from an ordered root chain where earlier roots override later roots.
2440    /// 从有序根目录覆盖链加载技能,前面的根目录会覆盖后面的同名技能。
2441    pub fn load_from_roots(
2442        &mut self,
2443        skill_roots: &[RuntimeSkillRoot],
2444    ) -> Result<(), Box<dyn std::error::Error>> {
2445        if !skill_roots.is_empty() {
2446            self.refresh_skill_config_runtime_root(skill_roots)
2447                .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
2448        }
2449        if skill_roots.iter().all(|root| !root.skills_dir.exists()) {
2450            return Ok(());
2451        }
2452
2453        for resolved_instance in collect_effective_skill_instances_from_roots(skill_roots)
2454            .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
2455        {
2456            let skill_name = resolved_instance.skill_id;
2457            if self.is_host_ignored_skill(&skill_name) {
2458                log_info(format!(
2459                    "[LuaSkill] Skipped host-ignored skill '{}'",
2460                    skill_name
2461                ));
2462                continue;
2463            }
2464            let resolved_root = RuntimeSkillRoot {
2465                name: resolved_instance.root_name.clone(),
2466                skills_dir: resolved_instance.skills_root.clone(),
2467            };
2468            let resolved_skill_manager = self
2469                .skill_manager_for(&resolved_root)
2470                .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
2471            if !resolved_skill_manager.is_skill_enabled(&skill_name)? {
2472                log_warn(format!(
2473                    "[LuaSkill] Skipped disabled skill '{}'",
2474                    skill_name
2475                ));
2476                continue;
2477            }
2478            let actual_dir = resolved_instance.actual_dir;
2479            log_info(format!(
2480                "[LuaSkill] Loaded '{}' from root '{}'",
2481                skill_name, resolved_instance.root_name
2482            ));
2483
2484            if let Err(error) = self.ensure_skill_dependencies(&resolved_root, &actual_dir) {
2485                log_error(format!(
2486                    "[LuaSkill] Failed to prepare dependencies for {}: {}",
2487                    skill_name, error
2488                ));
2489                continue;
2490            }
2491
2492            if let Err(e) = self.load_single_skill(&actual_dir, &resolved_instance.root_name) {
2493                log_error(format!("[LuaSkill] Failed to load {}: {}", skill_name, e));
2494            }
2495        }
2496
2497        self.rebuild_entry_registry()
2498            .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
2499
2500        self.pool
2501            .prewarm(|| self.create_vm())
2502            .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
2503        self.runlua_pool
2504            .prewarm(|| {
2505                Self::create_runlua_vm(
2506                    &self.skills,
2507                    &self.entry_registry,
2508                    self.host_options.clone(),
2509                    self.skill_config_store.clone(),
2510                    self.lancedb_host.clone(),
2511                    self.sqlite_host.clone(),
2512                )
2513            })
2514            .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
2515
2516        log_info(format!("[LuaSkill] {} skills loaded", self.skills.len()));
2517        Ok(())
2518    }
2519
2520    /// Reload all skills from the given directories and rebuild runtime state from scratch.
2521    /// 从给定目录重新加载全部技能,并从零重建运行时状态。
2522    pub fn reload_from_dirs(
2523        &mut self,
2524        base_dir: &Path,
2525        override_dir: Option<&Path>,
2526    ) -> Result<(), Box<dyn std::error::Error>> {
2527        let mut skill_roots = Vec::new();
2528        if let Some(override_dir) = override_dir {
2529            skill_roots.push(RuntimeSkillRoot {
2530                name: "OVERRIDE".to_string(),
2531                skills_dir: override_dir.to_path_buf(),
2532            });
2533        }
2534        skill_roots.push(RuntimeSkillRoot {
2535            name: "ROOT".to_string(),
2536            skills_dir: base_dir.to_path_buf(),
2537        });
2538        self.reload_from_roots(&skill_roots)
2539    }
2540
2541    /// Reload all skills from one ordered root chain and rebuild runtime state from scratch.
2542    /// 从一条有序根目录覆盖链中重载全部技能,并从零重建运行时状态。
2543    pub fn reload_from_roots(
2544        &mut self,
2545        skill_roots: &[RuntimeSkillRoot],
2546    ) -> Result<(), Box<dyn std::error::Error>> {
2547        let previous_entries = self.list_entries();
2548        self.reset_runtime_state();
2549        self.load_from_roots(skill_roots)?;
2550        self.emit_entry_registry_delta(previous_entries);
2551        Ok(())
2552    }
2553
2554    /// Execute one mutating skill lifecycle action in the requested operation plane and then reload the runtime view.
2555    /// 在指定操作平面执行一次会改变状态的技能生命周期动作,并在完成后立即重载运行时视图。
2556    fn mutate_skill_state_and_reload(
2557        &mut self,
2558        plane: SkillOperationPlane,
2559        action: crate::skill::manager::SkillLifecycleAction,
2560        skill_roots: &[RuntimeSkillRoot],
2561        skill_id: &str,
2562        reason: Option<&str>,
2563    ) -> Result<(), Box<dyn std::error::Error>> {
2564        validate_luaskills_identifier(skill_id, "skill_id")
2565            .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
2566        let resolved_instance = resolve_declared_skill_instance_from_roots(skill_roots, skill_id)
2567            .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
2568            .ok_or_else(|| -> Box<dyn std::error::Error> {
2569                format!("declared skill instance '{}' not found", skill_id).into()
2570            })?;
2571        let resolved_root = RuntimeSkillRoot {
2572            name: resolved_instance.root_name.clone(),
2573            skills_dir: resolved_instance.skills_root.clone(),
2574        };
2575        let manager = self
2576            .skill_manager_for(&resolved_root)
2577            .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
2578        let removed_dependency_manifest =
2579            if action == crate::skill::manager::SkillLifecycleAction::Uninstall {
2580                let dependencies_path = resolved_instance.actual_dir.join("dependencies.yaml");
2581                if dependencies_path.exists() {
2582                    Some(
2583                        SkillDependencyManifest::load_from_path(&dependencies_path)
2584                            .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
2585                    )
2586                } else {
2587                    None
2588                }
2589            } else {
2590                None
2591            };
2592        if let Err(error) = manager.guard_operation(plane, action, skill_id) {
2593            self.emit_skill_lifecycle_event(
2594                plane,
2595                action,
2596                skill_id,
2597                Some(resolved_instance.root_name.clone()),
2598                Some(resolved_instance.actual_dir.display().to_string()),
2599                "blocked",
2600                Some(error.clone()),
2601            );
2602            return Err(error.into());
2603        }
2604        let action_result = match action {
2605            crate::skill::manager::SkillLifecycleAction::Disable => manager
2606                .disable_skill_in_plane(plane, skill_id, reason)
2607                .map_err(|error| -> Box<dyn std::error::Error> { error.into() }),
2608            crate::skill::manager::SkillLifecycleAction::Enable => manager
2609                .enable_skill_in_plane(plane, skill_id)
2610                .map_err(|error| -> Box<dyn std::error::Error> { error.into() }),
2611            crate::skill::manager::SkillLifecycleAction::Uninstall => manager
2612                .uninstall_skill_at_path_in_plane(plane, skill_id, &resolved_instance.actual_dir)
2613                .map(|_| ())
2614                .map_err(|error| -> Box<dyn std::error::Error> { error.into() }),
2615            _ => {
2616                return Err(format!("unsupported state mutation action {:?}", action).into());
2617            }
2618        };
2619        if let Err(error) = action_result {
2620            let message = error.to_string();
2621            self.emit_skill_lifecycle_event(
2622                plane,
2623                action,
2624                skill_id,
2625                Some(resolved_instance.root_name.clone()),
2626                Some(resolved_instance.actual_dir.display().to_string()),
2627                "failed",
2628                Some(message),
2629            );
2630            return Err(error);
2631        }
2632        if action == crate::skill::manager::SkillLifecycleAction::Uninstall {
2633            let dependency_manager = DependencyManager::new(
2634                self.dependency_manager_config_for(&resolved_root)
2635                    .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
2636            );
2637            dependency_manager
2638                .cleanup_uninstalled_skill_dependencies_from_roots(
2639                    skill_roots,
2640                    skill_id,
2641                    removed_dependency_manifest.as_ref(),
2642                )
2643                .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
2644        }
2645        self.reload_from_roots(skill_roots)?;
2646        self.emit_skill_lifecycle_event(
2647            plane,
2648            action,
2649            skill_id,
2650            Some(resolved_instance.root_name.clone()),
2651            Some(resolved_instance.actual_dir.display().to_string()),
2652            "completed",
2653            None,
2654        );
2655        Ok(())
2656    }
2657
2658    /// Remove one optional skill-owned database directory when the caller explicitly requests it.
2659    /// 调用方显式请求时删除单个技能拥有的可选数据库目录。
2660    fn remove_skill_database_dir(
2661        &self,
2662        database_root: &Path,
2663        skill_id: &str,
2664        remove_requested: bool,
2665        database_label: &str,
2666    ) -> Result<(bool, bool), Box<dyn std::error::Error>> {
2667        if !remove_requested {
2668            return Ok((false, true));
2669        }
2670        let database_dir = database_root.join(database_label).join(skill_id);
2671        if !database_dir.exists() {
2672            return Ok((false, false));
2673        }
2674        fs::remove_dir_all(&database_dir).map_err(|error| {
2675            format!(
2676                "failed to remove {database_label} directory {}: {}",
2677                database_dir.display(),
2678                error
2679            )
2680        })?;
2681        Ok((true, false))
2682    }
2683
2684    /// Execute one uninstall action with explicit database-retention semantics and then reload the runtime view.
2685    /// 以显式数据库保留语义执行一次卸载动作,并在完成后重载运行时视图。
2686    fn uninstall_skill_and_reload(
2687        &mut self,
2688        plane: SkillOperationPlane,
2689        skill_roots: &[RuntimeSkillRoot],
2690        skill_id: &str,
2691        options: &SkillUninstallOptions,
2692    ) -> Result<SkillUninstallResult, Box<dyn std::error::Error>> {
2693        validate_luaskills_identifier(skill_id, "skill_id")
2694            .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
2695        let resolved_instance = resolve_effective_skill_instance_from_roots(skill_roots, skill_id)
2696            .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
2697            .ok_or_else(|| -> Box<dyn std::error::Error> {
2698                format!("effective skill instance '{}' not found", skill_id).into()
2699            })?;
2700        let resolved_root = RuntimeSkillRoot {
2701            name: resolved_instance.root_name.clone(),
2702            skills_dir: resolved_instance.skills_root.clone(),
2703        };
2704        let manager = self
2705            .skill_manager_for(&resolved_root)
2706            .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
2707        let dependencies_path = resolved_instance.actual_dir.join("dependencies.yaml");
2708        let removed_dependency_manifest = if dependencies_path.exists() {
2709            Some(
2710                SkillDependencyManifest::load_from_path(&dependencies_path)
2711                    .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
2712            )
2713        } else {
2714            None
2715        };
2716        if let Err(error) = manager.guard_operation(
2717            plane,
2718            crate::skill::manager::SkillLifecycleAction::Uninstall,
2719            skill_id,
2720        ) {
2721            self.emit_skill_lifecycle_event(
2722                plane,
2723                crate::skill::manager::SkillLifecycleAction::Uninstall,
2724                skill_id,
2725                Some(resolved_instance.root_name.clone()),
2726                Some(resolved_instance.actual_dir.display().to_string()),
2727                "blocked",
2728                Some(error.clone()),
2729            );
2730            return Err(error.into());
2731        }
2732        let prepared_uninstall = match manager.prepare_uninstall_skill_at_path_in_plane(
2733            plane,
2734            skill_id,
2735            &resolved_instance.actual_dir,
2736        ) {
2737            Ok(prepared) => prepared,
2738            Err(error) => {
2739                let message = error.to_string();
2740                self.emit_skill_lifecycle_event(
2741                    plane,
2742                    crate::skill::manager::SkillLifecycleAction::Uninstall,
2743                    skill_id,
2744                    Some(resolved_instance.root_name.clone()),
2745                    Some(resolved_instance.actual_dir.display().to_string()),
2746                    "failed",
2747                    Some(message),
2748                );
2749                return Err(error.into());
2750            }
2751        };
2752        if let Err(reload_error) = self.reload_from_roots(skill_roots) {
2753            let rollback_error = manager.rollback_prepared_skill_uninstall(&prepared_uninstall);
2754            let restore_error = self.reload_from_roots(skill_roots);
2755            let rollback_message = rollback_error
2756                .err()
2757                .map(|error| format!(" rollback failed: {}", error))
2758                .unwrap_or_default();
2759            let restore_message = restore_error
2760                .err()
2761                .map(|error| format!(" runtime restore failed: {}", error))
2762                .unwrap_or_default();
2763            let message = format!(
2764                "Failed to reload LuaSkills after uninstall: {}.{}{}",
2765                reload_error, rollback_message, restore_message
2766            );
2767            self.emit_skill_lifecycle_event(
2768                plane,
2769                crate::skill::manager::SkillLifecycleAction::Uninstall,
2770                skill_id,
2771                Some(resolved_instance.root_name.clone()),
2772                Some(resolved_instance.actual_dir.display().to_string()),
2773                "failed",
2774                Some(message.clone()),
2775            );
2776            return Err(message.into());
2777        }
2778        let mut result = match manager.commit_prepared_skill_uninstall(&prepared_uninstall) {
2779            Ok(result) => result,
2780            Err(error) => {
2781                let rollback_error = manager.rollback_prepared_skill_uninstall(&prepared_uninstall);
2782                let restore_error = self.reload_from_roots(skill_roots);
2783                let rollback_message = rollback_error
2784                    .err()
2785                    .map(|rollback| format!(" rollback failed: {}", rollback))
2786                    .unwrap_or_default();
2787                let restore_message = restore_error
2788                    .err()
2789                    .map(|restore| format!(" runtime restore failed: {}", restore))
2790                    .unwrap_or_default();
2791                let message = format!(
2792                    "Failed to finalize uninstall: {}.{}{}",
2793                    error, rollback_message, restore_message
2794                );
2795                self.emit_skill_lifecycle_event(
2796                    plane,
2797                    crate::skill::manager::SkillLifecycleAction::Uninstall,
2798                    skill_id,
2799                    Some(resolved_instance.root_name.clone()),
2800                    Some(resolved_instance.actual_dir.display().to_string()),
2801                    "failed",
2802                    Some(message.clone()),
2803                );
2804                return Err(message.into());
2805            }
2806        };
2807        let dependency_manager = DependencyManager::new(
2808            self.dependency_manager_config_for(&resolved_root)
2809                .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
2810        );
2811        if let Err(error) = dependency_manager.cleanup_uninstalled_skill_dependencies_from_roots(
2812            skill_roots,
2813            skill_id,
2814            removed_dependency_manifest.as_ref(),
2815        ) {
2816            log_warn(format!(
2817                "[LuaSkills:uninstall] Stale dependency cleanup warning for skill '{}': {}",
2818                skill_id, error
2819            ));
2820            result.message = format!(
2821                "{} (warning: stale dependency cleanup failed: {})",
2822                result.message, error
2823            );
2824        }
2825        let (sqlite_removed, sqlite_retained) = match self.remove_skill_database_dir(
2826            &self.database_root_for(&resolved_root),
2827            skill_id,
2828            options.remove_sqlite,
2829            "sqlite",
2830        ) {
2831            Ok(result) => result,
2832            Err(error) => {
2833                log_warn(format!(
2834                    "[LuaSkills:uninstall] SQLite cleanup warning for skill '{}': {}",
2835                    skill_id, error
2836                ));
2837                result.message = format!(
2838                    "{} (warning: sqlite cleanup failed: {})",
2839                    result.message, error
2840                );
2841                (false, false)
2842            }
2843        };
2844        let (lancedb_removed, lancedb_retained) = match self.remove_skill_database_dir(
2845            &self.database_root_for(&resolved_root),
2846            skill_id,
2847            options.remove_lancedb,
2848            "lancedb",
2849        ) {
2850            Ok(result) => result,
2851            Err(error) => {
2852                log_warn(format!(
2853                    "[LuaSkills:uninstall] LanceDB cleanup warning for skill '{}': {}",
2854                    skill_id, error
2855                ));
2856                result.message = format!(
2857                    "{} (warning: lancedb cleanup failed: {})",
2858                    result.message, error
2859                );
2860                (false, false)
2861            }
2862        };
2863        result.sqlite_removed = sqlite_removed;
2864        result.sqlite_retained = sqlite_retained;
2865        result.lancedb_removed = lancedb_removed;
2866        result.lancedb_retained = lancedb_retained;
2867        let summary = format!(
2868            "skill package removed={} sqlite_removed={} sqlite_retained={} lancedb_removed={} lancedb_retained={}",
2869            result.skill_removed,
2870            result.sqlite_removed,
2871            result.sqlite_retained,
2872            result.lancedb_removed,
2873            result.lancedb_retained
2874        );
2875        result.message = if result.message.is_empty() {
2876            summary
2877        } else {
2878            format!("{}; {}", summary, result.message)
2879        };
2880        self.emit_skill_lifecycle_event(
2881            plane,
2882            crate::skill::manager::SkillLifecycleAction::Uninstall,
2883            skill_id,
2884            Some(resolved_instance.root_name.clone()),
2885            Some(resolved_instance.actual_dir.display().to_string()),
2886            "completed",
2887            Some(result.message.clone()),
2888        );
2889        Ok(result)
2890    }
2891
2892    /// Execute one install or update preflight request in the requested operation plane.
2893    /// 在指定操作平面执行一次安装或更新预检查请求。
2894    fn apply_skill_request(
2895        &mut self,
2896        plane: SkillOperationPlane,
2897        action: crate::skill::manager::SkillLifecycleAction,
2898        skill_roots: &[RuntimeSkillRoot],
2899        request: &SkillInstallRequest,
2900    ) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
2901        if !matches!(
2902            action,
2903            crate::skill::manager::SkillLifecycleAction::Install
2904                | crate::skill::manager::SkillLifecycleAction::Update
2905        ) {
2906            return Err(format!("unsupported apply action {:?}", action).into());
2907        }
2908        let target_root = match action {
2909            crate::skill::manager::SkillLifecycleAction::Install => {
2910                self.reference_skill_root(skill_roots)?.clone()
2911            }
2912            crate::skill::manager::SkillLifecycleAction::Update => {
2913                let target_skill_id = request
2914                    .skill_id
2915                    .as_deref()
2916                    .map(str::trim)
2917                    .filter(|value| !value.is_empty())
2918                    .map(ToOwned::to_owned)
2919                    .or_else(|| {
2920                        request
2921                            .source
2922                            .as_deref()
2923                            .and_then(|value| value.trim().rsplit('/').next())
2924                            .map(str::trim)
2925                            .filter(|value| !value.is_empty())
2926                            .map(ToOwned::to_owned)
2927                    })
2928                    .ok_or_else(|| -> Box<dyn std::error::Error> {
2929                        "update request requires skill_id or one derivable source".into()
2930                    })?;
2931                let resolved_instance =
2932                    resolve_declared_skill_instance_from_roots(skill_roots, &target_skill_id)
2933                        .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
2934                        .ok_or_else(|| -> Box<dyn std::error::Error> {
2935                            format!("skill '{}' is not installed", target_skill_id).into()
2936                        })?;
2937                RuntimeSkillRoot {
2938                    name: resolved_instance.root_name,
2939                    skills_dir: resolved_instance.skills_root,
2940                }
2941            }
2942            _ => unreachable!("unsupported apply action should have returned early"),
2943        };
2944        let previous_dependency_manifest =
2945            if action == crate::skill::manager::SkillLifecycleAction::Update {
2946                let target_skill_id = request
2947                    .skill_id
2948                    .as_deref()
2949                    .map(str::trim)
2950                    .filter(|value| !value.is_empty())
2951                    .map(ToOwned::to_owned)
2952                    .or_else(|| {
2953                        request
2954                            .source
2955                            .as_deref()
2956                            .and_then(|value| value.trim().rsplit('/').next())
2957                            .map(str::trim)
2958                            .filter(|value| !value.is_empty())
2959                            .map(ToOwned::to_owned)
2960                    });
2961                if let Some(target_skill_id) = target_skill_id {
2962                    resolve_declared_skill_instance_from_roots(skill_roots, &target_skill_id)
2963                        .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
2964                        .and_then(|resolved| {
2965                            self.load_skill_dependency_manifest(&resolved.actual_dir)
2966                                .transpose()
2967                        })
2968                        .transpose()
2969                        .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
2970                } else {
2971                    None
2972                }
2973            } else {
2974                None
2975            };
2976        let manager = self
2977            .skill_manager_for(&target_root)
2978            .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
2979        let prepared = match action {
2980            crate::skill::manager::SkillLifecycleAction::Install => manager
2981                .prepare_install_skill(plane, skill_roots, request)
2982                .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
2983            crate::skill::manager::SkillLifecycleAction::Update => manager
2984                .prepare_update_skill(plane, skill_roots, request)
2985                .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
2986            _ => unreachable!("unsupported apply action should have returned early"),
2987        };
2988        let mut result = match &prepared {
2989            PreparedSkillApply::Immediate(result) => result.clone(),
2990            PreparedSkillApply::Install(_) | PreparedSkillApply::Update(_) => {
2991                if let Err(reload_error) = self.reload_from_roots(skill_roots) {
2992                    let rollback_error = manager.rollback_prepared_skill_apply(&prepared);
2993                    let restore_error = self.reload_from_roots(skill_roots);
2994                    let rollback_message = rollback_error
2995                        .err()
2996                        .map(|error| format!(" rollback failed: {}", error))
2997                        .unwrap_or_default();
2998                    let restore_message = restore_error
2999                        .err()
3000                        .map(|error| format!(" runtime restore failed: {}", error))
3001                        .unwrap_or_default();
3002                    return Err(format!(
3003                        "Failed to reload LuaSkills after {:?}: {}.{}{}",
3004                        action, reload_error, rollback_message, restore_message
3005                    )
3006                    .into());
3007                }
3008
3009                let committed = manager.commit_prepared_skill_apply(&prepared).map_err(
3010                    |error| -> Box<dyn std::error::Error> {
3011                        let rollback_error = manager.rollback_prepared_skill_apply(&prepared);
3012                        let restore_error = self.reload_from_roots(skill_roots);
3013                        let rollback_message = rollback_error
3014                            .err()
3015                            .map(|rollback| format!(" rollback failed: {}", rollback))
3016                            .unwrap_or_default();
3017                        let restore_message = restore_error
3018                            .err()
3019                            .map(|restore| format!(" runtime restore failed: {}", restore))
3020                            .unwrap_or_default();
3021                        format!(
3022                            "Failed to finalize {:?}: {}.{}{}",
3023                            action, error, rollback_message, restore_message
3024                        )
3025                        .into()
3026                    },
3027                )?;
3028                committed
3029            }
3030        };
3031        if action == crate::skill::manager::SkillLifecycleAction::Update
3032            && result.status == "updated"
3033        {
3034            let current_dependency_manifest =
3035                resolve_declared_skill_instance_from_roots(skill_roots, &result.skill_id)
3036                    .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
3037                    .and_then(|resolved| {
3038                        self.load_skill_dependency_manifest(&resolved.actual_dir)
3039                            .transpose()
3040                    })
3041                    .transpose()
3042                    .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
3043            let dependency_manager = DependencyManager::new(
3044                self.dependency_manager_config_for(&target_root)
3045                    .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
3046            );
3047            if let Err(error) = dependency_manager.cleanup_updated_skill_dependencies(
3048                &result.skill_id,
3049                previous_dependency_manifest.as_ref(),
3050                current_dependency_manifest.as_ref(),
3051            ) {
3052                log_warn(format!(
3053                    "[LuaSkills:update] Stale dependency cleanup warning for skill '{}': {}",
3054                    result.skill_id, error
3055                ));
3056                result.message = format!(
3057                    "{} (warning: stale dependency cleanup failed: {})",
3058                    result.message, error
3059                );
3060            }
3061        }
3062        let resolved_instance =
3063            resolve_declared_skill_instance_from_roots(skill_roots, &result.skill_id)
3064                .map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
3065        self.emit_skill_lifecycle_event(
3066            plane,
3067            action,
3068            &result.skill_id,
3069            resolved_instance
3070                .as_ref()
3071                .map(|instance| instance.root_name.clone()),
3072            resolved_instance
3073                .as_ref()
3074                .map(|instance| instance.actual_dir.display().to_string()),
3075            &result.status,
3076            Some(result.message.clone()),
3077        );
3078        Ok(result)
3079    }
3080
3081    /// Mark one skill disabled through the ordinary skills plane and immediately reload the runtime view.
3082    /// 通过普通 skills 平面将单个技能标记为停用,并立即重载运行时视图。
3083    pub fn disable_skill(
3084        &mut self,
3085        base_dir: &Path,
3086        override_dir: Option<&Path>,
3087        skill_id: &str,
3088        reason: Option<&str>,
3089    ) -> Result<(), Box<dyn std::error::Error>> {
3090        let mut skill_roots = Vec::new();
3091        if let Some(override_dir) = override_dir {
3092            skill_roots.push(RuntimeSkillRoot {
3093                name: "OVERRIDE".to_string(),
3094                skills_dir: override_dir.to_path_buf(),
3095            });
3096        }
3097        skill_roots.push(RuntimeSkillRoot {
3098            name: "ROOT".to_string(),
3099            skills_dir: base_dir.to_path_buf(),
3100        });
3101        self.disable_skill_in_roots(&skill_roots, skill_id, reason)
3102    }
3103
3104    /// Mark one skill disabled through the ordinary skills plane using an ordered root chain and immediately reload the runtime view.
3105    /// 通过普通 skills 平面使用有序根目录链将单个技能标记为停用,并立即重载运行时视图。
3106    pub fn disable_skill_in_roots(
3107        &mut self,
3108        skill_roots: &[RuntimeSkillRoot],
3109        skill_id: &str,
3110        reason: Option<&str>,
3111    ) -> Result<(), Box<dyn std::error::Error>> {
3112        self.mutate_skill_state_and_reload(
3113            SkillOperationPlane::Skills,
3114            crate::skill::manager::SkillLifecycleAction::Disable,
3115            skill_roots,
3116            skill_id,
3117            reason,
3118        )
3119    }
3120
3121    /// Mark one skill disabled through the host-controlled system plane and immediately reload the runtime view.
3122    /// 通过宿主控制的 system 平面将单个技能标记为停用,并立即重载运行时视图。
3123    pub fn system_disable_skill(
3124        &mut self,
3125        base_dir: &Path,
3126        override_dir: Option<&Path>,
3127        skill_id: &str,
3128        reason: Option<&str>,
3129    ) -> Result<(), Box<dyn std::error::Error>> {
3130        let mut skill_roots = Vec::new();
3131        if let Some(override_dir) = override_dir {
3132            skill_roots.push(RuntimeSkillRoot {
3133                name: "OVERRIDE".to_string(),
3134                skills_dir: override_dir.to_path_buf(),
3135            });
3136        }
3137        skill_roots.push(RuntimeSkillRoot {
3138            name: "ROOT".to_string(),
3139            skills_dir: base_dir.to_path_buf(),
3140        });
3141        self.system_disable_skill_in_roots(&skill_roots, skill_id, reason)
3142    }
3143
3144    /// Mark one skill disabled through the host-controlled system plane using an ordered root chain and immediately reload the runtime view.
3145    /// 通过宿主控制的 system 平面使用有序根目录链将单个技能标记为停用,并立即重载运行时视图。
3146    pub fn system_disable_skill_in_roots(
3147        &mut self,
3148        skill_roots: &[RuntimeSkillRoot],
3149        skill_id: &str,
3150        reason: Option<&str>,
3151    ) -> Result<(), Box<dyn std::error::Error>> {
3152        self.mutate_skill_state_and_reload(
3153            SkillOperationPlane::System,
3154            crate::skill::manager::SkillLifecycleAction::Disable,
3155            skill_roots,
3156            skill_id,
3157            reason,
3158        )
3159    }
3160
3161    /// Remove the disabled marker of one skill through the ordinary skills plane and immediately reload the runtime view.
3162    /// 通过普通 skills 平面移除单个技能的停用标记,并立即重载运行时视图。
3163    pub fn enable_skill(
3164        &mut self,
3165        skill_roots: &[RuntimeSkillRoot],
3166        skill_id: &str,
3167    ) -> Result<(), Box<dyn std::error::Error>> {
3168        self.mutate_skill_state_and_reload(
3169            SkillOperationPlane::Skills,
3170            crate::skill::manager::SkillLifecycleAction::Enable,
3171            skill_roots,
3172            skill_id,
3173            None,
3174        )
3175    }
3176
3177    /// Remove the disabled marker of one skill through the host-controlled system plane and immediately reload the runtime view.
3178    /// 通过宿主控制的 system 平面移除单个技能的停用标记,并立即重载运行时视图。
3179    pub fn system_enable_skill(
3180        &mut self,
3181        skill_roots: &[RuntimeSkillRoot],
3182        skill_id: &str,
3183    ) -> Result<(), Box<dyn std::error::Error>> {
3184        self.mutate_skill_state_and_reload(
3185            SkillOperationPlane::System,
3186            crate::skill::manager::SkillLifecycleAction::Enable,
3187            skill_roots,
3188            skill_id,
3189            None,
3190        )
3191    }
3192
3193    /// Uninstall one skill directory through the ordinary skills plane and immediately reload the runtime view.
3194    /// 通过普通 skills 平面卸载单个技能目录,并立即重载运行时视图。
3195    pub fn uninstall_skill(
3196        &mut self,
3197        skill_roots: &[RuntimeSkillRoot],
3198        skill_id: &str,
3199        options: &SkillUninstallOptions,
3200    ) -> Result<SkillUninstallResult, Box<dyn std::error::Error>> {
3201        self.uninstall_skill_and_reload(SkillOperationPlane::Skills, skill_roots, skill_id, options)
3202    }
3203
3204    /// Uninstall one skill directory through the host-controlled system plane and immediately reload the runtime view.
3205    /// 通过宿主控制的 system 平面卸载单个技能目录,并立即重载运行时视图。
3206    pub fn system_uninstall_skill(
3207        &mut self,
3208        skill_roots: &[RuntimeSkillRoot],
3209        skill_id: &str,
3210        options: &SkillUninstallOptions,
3211    ) -> Result<SkillUninstallResult, Box<dyn std::error::Error>> {
3212        self.uninstall_skill_and_reload(SkillOperationPlane::System, skill_roots, skill_id, options)
3213    }
3214
3215    /// Preflight one install request through the ordinary skills plane and return a structured result.
3216    /// 通过普通 skills 平面对一次安装请求执行预检查,并返回结构化结果。
3217    pub fn install_skill(
3218        &mut self,
3219        skill_roots: &[RuntimeSkillRoot],
3220        request: &SkillInstallRequest,
3221    ) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
3222        self.apply_skill_request(
3223            SkillOperationPlane::Skills,
3224            crate::skill::manager::SkillLifecycleAction::Install,
3225            skill_roots,
3226            request,
3227        )
3228    }
3229
3230    /// Preflight one install request through the host-controlled system plane and return a structured result.
3231    /// 通过宿主控制的 system 平面对一次安装请求执行预检查,并返回结构化结果。
3232    pub fn system_install_skill(
3233        &mut self,
3234        skill_roots: &[RuntimeSkillRoot],
3235        request: &SkillInstallRequest,
3236    ) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
3237        self.apply_skill_request(
3238            SkillOperationPlane::System,
3239            crate::skill::manager::SkillLifecycleAction::Install,
3240            skill_roots,
3241            request,
3242        )
3243    }
3244
3245    /// Preflight one update request through the ordinary skills plane and return a structured result.
3246    /// 通过普通 skills 平面对一次更新请求执行预检查,并返回结构化结果。
3247    pub fn update_skill(
3248        &mut self,
3249        skill_roots: &[RuntimeSkillRoot],
3250        request: &SkillInstallRequest,
3251    ) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
3252        self.apply_skill_request(
3253            SkillOperationPlane::Skills,
3254            crate::skill::manager::SkillLifecycleAction::Update,
3255            skill_roots,
3256            request,
3257        )
3258    }
3259
3260    /// Preflight one update request through the host-controlled system plane and return a structured result.
3261    /// 通过宿主控制的 system 平面对一次更新请求执行预检查,并返回结构化结果。
3262    pub fn system_update_skill(
3263        &mut self,
3264        skill_roots: &[RuntimeSkillRoot],
3265        request: &SkillInstallRequest,
3266    ) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
3267        self.apply_skill_request(
3268            SkillOperationPlane::System,
3269            crate::skill::manager::SkillLifecycleAction::Update,
3270            skill_roots,
3271            request,
3272        )
3273    }
3274
3275    /// Reset all loaded skills, providers, and pooled VMs before one full reload.
3276    /// 在执行一次完整重载前重置全部已加载技能、provider 与虚拟机池。
3277    fn reset_runtime_state(&mut self) {
3278        let pool_config = self.pool.config;
3279        let runlua_pool_config = self.runlua_pool.config;
3280        self.skills.clear();
3281        self.entry_registry.clear();
3282        self.lancedb_host = None;
3283        self.sqlite_host = None;
3284        self.pool = Arc::new(LuaVmPool::new(pool_config));
3285        self.runlua_pool = Arc::new(LuaVmPool::new(runlua_pool_config));
3286    }
3287
3288    /// Emit one structured lifecycle event through the host callback bridge.
3289    /// 通过宿主回调桥发送一条结构化生命周期事件。
3290    fn emit_skill_lifecycle_event(
3291        &self,
3292        plane: SkillOperationPlane,
3293        action: crate::skill::manager::SkillLifecycleAction,
3294        skill_id: &str,
3295        root_name: Option<String>,
3296        skill_dir: Option<String>,
3297        status: &str,
3298        message: Option<String>,
3299    ) {
3300        crate::host::callbacks::emit_skill_lifecycle_event(&RuntimeSkillLifecycleEvent {
3301            plane,
3302            action,
3303            skill_id: skill_id.to_string(),
3304            root_name,
3305            skill_dir,
3306            status: status.to_string(),
3307            message,
3308        });
3309    }
3310
3311    /// Compare pre-reload and post-reload entry snapshots and emit one host-visible registry delta.
3312    /// 对比重载前后入口快照并发出一条宿主可见的注册表差异事件。
3313    fn emit_entry_registry_delta(&self, previous_entries: Vec<RuntimeEntryDescriptor>) {
3314        let current_entries = self.list_entries();
3315        let previous_map = previous_entries
3316            .into_iter()
3317            .map(|entry| (entry.canonical_name.clone(), entry))
3318            .collect::<BTreeMap<String, RuntimeEntryDescriptor>>();
3319        let current_map = current_entries
3320            .into_iter()
3321            .map(|entry| (entry.canonical_name.clone(), entry))
3322            .collect::<BTreeMap<String, RuntimeEntryDescriptor>>();
3323
3324        let mut added_entries = Vec::new();
3325        let mut updated_entries = Vec::new();
3326        let mut removed_entry_names = Vec::new();
3327
3328        for (canonical_name, current_entry) in &current_map {
3329            match previous_map.get(canonical_name) {
3330                None => added_entries.push(current_entry.clone()),
3331                Some(previous_entry) if previous_entry != current_entry => {
3332                    updated_entries.push(current_entry.clone());
3333                }
3334                Some(_) => {}
3335            }
3336        }
3337
3338        for canonical_name in previous_map.keys() {
3339            if !current_map.contains_key(canonical_name) {
3340                removed_entry_names.push(canonical_name.clone());
3341            }
3342        }
3343
3344        if added_entries.is_empty() && updated_entries.is_empty() && removed_entry_names.is_empty()
3345        {
3346            return;
3347        }
3348
3349        crate::host::callbacks::emit_entry_registry_delta(&RuntimeEntryRegistryDelta {
3350            added_entries,
3351            removed_entry_names,
3352            updated_entries,
3353        });
3354    }
3355
3356    /// Load a single skill from its directory.
3357    fn load_single_skill(
3358        &mut self,
3359        dir: &Path,
3360        root_name: &str,
3361    ) -> Result<(), Box<dyn std::error::Error>> {
3362        let skill_yaml = dir.join("skill.yaml");
3363        if !skill_yaml.exists() {
3364            return Err(format!("skill.yaml not found in {}", dir.display()).into());
3365        }
3366
3367        let yaml_str = std::fs::read_to_string(&skill_yaml)?;
3368        let yaml_value: serde_yaml::Value = serde_yaml::from_str(&yaml_str)?;
3369        if yaml_value.as_mapping().is_some_and(|mapping| {
3370            mapping.contains_key(serde_yaml::Value::String("skill_id".to_string()))
3371        }) {
3372            return Err(format!("skill {} must not declare skill_id in skill.yaml; directory name is the only skill_id", dir.display())
3373            .into());
3374        }
3375        let mut meta: SkillMeta = serde_yaml::from_value(yaml_value)?;
3376        let directory_skill_id = dir
3377            .file_name()
3378            .and_then(|value| value.to_str())
3379            .ok_or_else(|| format!("invalid skill directory name: {}", dir.display()))?
3380            .trim()
3381            .to_string();
3382        validate_luaskills_identifier(&directory_skill_id, "skill directory name")
3383            .map_err(|error| format!("skill {}: {}", dir.display(), error))?;
3384        meta.bind_directory_skill_id(directory_skill_id.clone());
3385
3386        if !meta.is_enabled() {
3387            log_info(format!(
3388                "[LuaSkill] Skip disabled skill '{}'",
3389                meta.effective_skill_id()
3390            ));
3391            return Ok(());
3392        }
3393        validate_luaskills_identifier(meta.effective_skill_id(), "skill_id")
3394            .map_err(|error| format!("skill {}: {}", meta.name, error))?;
3395        validate_luaskills_version(meta.version(), "version")
3396            .map_err(|error| format!("skill {}: {}", meta.effective_skill_id(), error))?;
3397
3398        if meta.entries.is_empty() {
3399            return Err(format!("skill {} must declare at least one entry", meta.name).into());
3400        }
3401
3402        for tool in meta.entries() {
3403            validate_luaskills_identifier(tool.name.trim(), "entry.name").map_err(|error| {
3404                format!("skill {} entry {}: {}", meta.name, tool.name.trim(), error)
3405            })?;
3406            if tool.lua_entry.trim().is_empty() || tool.lua_module.trim().is_empty() {
3407                return Err(format!(
3408                    "skill {} declares entry {} but lua_entry/lua_module is missing",
3409                    meta.name, tool.name
3410                )
3411                .into());
3412            }
3413
3414            validate_skill_relative_path(&tool.lua_entry, "runtime", "entry.lua_entry")
3415                .map_err(|error| format!("skill {} entry {}: {}", meta.name, tool.name, error))?;
3416
3417            let lua_path = tool_entry_path(dir, tool);
3418            if !lua_path.exists() {
3419                return Err(format!(
3420                    "Lua entry {} not found in {}",
3421                    tool.lua_entry,
3422                    dir.display()
3423                )
3424                .into());
3425            }
3426        }
3427
3428        if !meta.help.main.file.trim().is_empty() {
3429            validate_skill_relative_path(&meta.help.main.file, "help", "help.main.file")
3430                .map_err(|error| format!("skill {} help main: {}", meta.name, error))?;
3431        }
3432        for topic in &meta.help.topics {
3433            validate_skill_relative_path(&topic.file, "help", "help.topic.file").map_err(
3434                |error| {
3435                    format!(
3436                        "skill {} help topic {}: {}",
3437                        meta.name,
3438                        topic.name.trim(),
3439                        error
3440                    )
3441                },
3442            )?;
3443        }
3444
3445        let effective_lancedb = meta.effective_lancedb();
3446        let lancedb_binding = if effective_lancedb.enable {
3447            if self.lancedb_host.is_none() {
3448                self.lancedb_host = Some(Arc::new(
3449                    LanceDbSkillHost::new(
3450                        self.host_options.as_ref().clone(),
3451                        self.database_provider_callbacks.clone(),
3452                    )
3453                    .map_err(|error| {
3454                        format!("Failed to initialize LanceDB skill host: {}", error)
3455                    })?,
3456                ));
3457            }
3458
3459            let host = self
3460                .lancedb_host
3461                .as_ref()
3462                .ok_or("LanceDB skill host missing after initialization")?
3463                .clone();
3464
3465            Some(
3466                host.register_skill(root_name, meta.effective_skill_id(), dir, effective_lancedb)
3467                    .map_err(|error| {
3468                        format!(
3469                            "Failed to register LanceDB for skill {}: {}",
3470                            meta.effective_skill_id(),
3471                            error
3472                        )
3473                    })?,
3474            )
3475        } else {
3476            None
3477        };
3478
3479        let effective_sqlite = meta.effective_sqlite();
3480        let sqlite_binding = if effective_sqlite.enable {
3481            if self.sqlite_host.is_none() {
3482                self.sqlite_host = Some(Arc::new(
3483                    SqliteSkillHost::new(
3484                        self.host_options.as_ref().clone(),
3485                        self.database_provider_callbacks.clone(),
3486                    )
3487                    .map_err(|error| {
3488                        format!("Failed to initialize SQLite skill host: {}", error)
3489                    })?,
3490                ));
3491            }
3492
3493            let host = self
3494                .sqlite_host
3495                .as_ref()
3496                .ok_or("SQLite skill host missing after initialization")?
3497                .clone();
3498
3499            Some(
3500                host.register_skill(root_name, meta.effective_skill_id(), dir, effective_sqlite)
3501                    .map_err(|error| {
3502                        format!(
3503                            "Failed to register SQLite for skill {}: {}",
3504                            meta.effective_skill_id(),
3505                            error
3506                        )
3507                    })?,
3508            )
3509        } else {
3510            None
3511        };
3512
3513        self.skills.insert(
3514            meta.effective_skill_id().to_string(),
3515            LoadedSkill {
3516                meta,
3517                dir: dir.to_path_buf(),
3518                root_name: root_name.to_string(),
3519                lancedb_binding,
3520                sqlite_binding,
3521                resolved_entry_names: HashMap::new(),
3522            },
3523        );
3524
3525        Ok(())
3526    }
3527
3528    /// Build a fresh Lua VM instance with all loaded skills registered.
3529    /// 创建一个全新的 Lua 虚拟机实例,并注册当前已加载的全部技能。
3530    fn create_vm(&self) -> Result<LuaVm, String> {
3531        let skills = Arc::new(self.skills.clone());
3532        let entry_registry = Arc::new(self.entry_registry.clone());
3533        let lua = unsafe { Lua::unsafe_new() };
3534        Self::setup_package_paths(&lua, self.host_options.as_ref())
3535            .map_err(|error| error.to_string())?;
3536        Self::register_vulcan_module(
3537            &lua,
3538            self.host_options.as_ref(),
3539            self.skill_config_store.clone(),
3540        )
3541        .map_err(|error| error.to_string())?;
3542        Self::populate_vulcan_luaexec_bridge(
3543            &lua,
3544            self.host_options.clone(),
3545            self.runlua_pool.clone(),
3546            self.skill_config_store.clone(),
3547            skills.clone(),
3548            entry_registry.clone(),
3549            self.lancedb_host.clone(),
3550            self.sqlite_host.clone(),
3551        )?;
3552        Self::register_skill_functions(&lua, skills.as_ref())?;
3553        Self::populate_vulcan_call_for_lua(
3554            &lua,
3555            skills.as_ref(),
3556            entry_registry.as_ref(),
3557            self.host_options.clone(),
3558            self.lancedb_host.clone(),
3559            self.sqlite_host.clone(),
3560        )?;
3561        Ok(LuaVm {
3562            lua,
3563            last_used_at: Instant::now(),
3564        })
3565    }
3566
3567    /// Borrow a Lua VM from the pool for one operation.
3568    /// 从虚拟机池借出一个 Lua 实例执行一次操作。
3569    fn acquire_vm(&self) -> Result<LuaVmLease, String> {
3570        self.pool.acquire(|| self.create_vm())
3571    }
3572
3573    /// Build a fresh isolated runlua VM instance with current runtime state registered.
3574    /// 创建一个带有当前运行时状态注册信息的全新隔离 runlua 虚拟机实例。
3575    fn create_runlua_vm(
3576        skills: &HashMap<String, LoadedSkill>,
3577        entry_registry: &BTreeMap<String, ResolvedEntryTarget>,
3578        host_options: Arc<LuaRuntimeHostOptions>,
3579        skill_config_store: Arc<SkillConfigStore>,
3580        lancedb_host: Option<Arc<LanceDbSkillHost>>,
3581        sqlite_host: Option<Arc<SqliteSkillHost>>,
3582    ) -> Result<LuaVm, String> {
3583        let lua = unsafe { Lua::unsafe_new() };
3584        Self::setup_package_paths(&lua, host_options.as_ref())
3585            .map_err(|error| error.to_string())?;
3586        Self::register_vulcan_module(&lua, host_options.as_ref(), skill_config_store)
3587            .map_err(|error| error.to_string())?;
3588        Self::register_skill_functions(&lua, skills)?;
3589        Self::populate_vulcan_call_for_lua(
3590            &lua,
3591            skills,
3592            entry_registry,
3593            host_options,
3594            lancedb_host,
3595            sqlite_host,
3596        )?;
3597        Ok(LuaVm {
3598            lua,
3599            last_used_at: Instant::now(),
3600        })
3601    }
3602
3603    /// Populate the `vulcan.runtime.lua.exec` bridge for normal skill VMs.
3604    /// 为普通 skill 虚拟机注入 `vulcan.runtime.lua.exec` 桥接函数。
3605    fn populate_vulcan_luaexec_bridge(
3606        lua: &Lua,
3607        host_options: Arc<LuaRuntimeHostOptions>,
3608        runlua_pool: Arc<LuaVmPool>,
3609        skill_config_store: Arc<SkillConfigStore>,
3610        skills: Arc<HashMap<String, LoadedSkill>>,
3611        entry_registry: Arc<BTreeMap<String, ResolvedEntryTarget>>,
3612        lancedb_host: Option<Arc<LanceDbSkillHost>>,
3613        sqlite_host: Option<Arc<SqliteSkillHost>>,
3614    ) -> Result<(), String> {
3615        let runtime_lua = get_vulcan_runtime_lua_table(lua)?;
3616
3617        let exec_fn = lua
3618            .create_function(move |lua, input: LuaValue| {
3619                let input_table = require_table_arg(input, "runtime.lua.exec", "input")?;
3620                let input_json = lua_value_to_json(&LuaValue::Table(input_table))
3621                    .map_err(mlua::Error::runtime)?;
3622                let mut request: RunLuaExecRequest =
3623                    serde_json::from_value(input_json).map_err(|error| {
3624                        mlua::Error::runtime(format!("luaexec input is invalid: {}", error))
3625                    })?;
3626                let internal =
3627                    get_vulcan_runtime_internal_table(lua).map_err(mlua::Error::runtime)?;
3628                let caller_tool_name: Option<String> =
3629                    internal.get("tool_name").map_err(mlua::Error::runtime)?;
3630                request.caller_tool_name = caller_tool_name
3631                    .map(|value| value.trim().to_string())
3632                    .filter(|value| !value.is_empty());
3633                let rendered = LuaEngine::execute_runlua_request_inline_with_runtime(
3634                    &request,
3635                    runlua_pool.clone(),
3636                    skills.clone(),
3637                    entry_registry.clone(),
3638                    host_options.clone(),
3639                    skill_config_store.clone(),
3640                    lancedb_host.clone(),
3641                    sqlite_host.clone(),
3642                )
3643                .map_err(mlua::Error::runtime)?;
3644                Ok(LuaValue::String(
3645                    lua.create_string(&rendered).map_err(mlua::Error::runtime)?,
3646                ))
3647            })
3648            .map_err(|error| format!("Failed to create vulcan.runtime.lua.exec: {}", error))?;
3649        runtime_lua
3650            .set("exec", exec_fn)
3651            .map_err(|error| format!("Failed to set vulcan.runtime.lua.exec: {}", error))?;
3652        Ok(())
3653    }
3654
3655    /// Register all tool-bearing skill entries into a specific Lua VM.
3656    /// 将所有声明了工具入口的 skill 条目注册到指定 Lua 虚拟机中。
3657    fn register_skill_functions(
3658        lua: &Lua,
3659        skills: &HashMap<String, LoadedSkill>,
3660    ) -> Result<(), String> {
3661        for skill in skills.values() {
3662            for tool in skill.meta.entries() {
3663                Self::compile_skill_into_lua(lua, skill, tool, false)?;
3664            }
3665        }
3666        Ok(())
3667    }
3668
3669    /// Compile one tool entry into the target Lua VM.
3670    /// 将单个工具入口编译并注册到目标 Lua 虚拟机中。
3671    fn compile_skill_into_lua(
3672        lua: &Lua,
3673        skill: &LoadedSkill,
3674        tool: &crate::lua_skill::SkillToolMeta,
3675        always_reload: bool,
3676    ) -> Result<(), String> {
3677        let lua_path = tool_entry_path(&skill.dir, tool);
3678        let source = std::fs::read_to_string(&lua_path)
3679            .map_err(|error| format!("Failed to read {}: {}", lua_path.display(), error))?;
3680        if always_reload {
3681            log_info(format!(
3682                "[LuaSkill] Hot reload {}: {}",
3683                tool.lua_module,
3684                render_log_friendly_path(&lua_path)
3685            ));
3686        }
3687
3688        let chunk = lua.load(&source).set_name(&tool.lua_module);
3689        let outer: Function = chunk.into_function().map_err(|error| {
3690            format!(
3691                "Failed to compile skill '{}::{}': {}",
3692                skill.meta.name, tool.lua_module, error
3693            )
3694        })?;
3695        let handler: Function = outer.call(()).map_err(|error| {
3696            format!(
3697                "Failed to initialize skill '{}::{}': {}",
3698                skill.meta.name, tool.lua_module, error
3699            )
3700        })?;
3701        lua.globals()
3702            .set(format!("__skill_{}", tool.lua_module), handler)
3703            .map_err(|error| {
3704                format!(
3705                    "Failed to register skill '{}::{}': {}",
3706                    skill.meta.name, tool.lua_module, error
3707                )
3708            })?;
3709        Ok(())
3710    }
3711
3712    /// Return generic runtime entry descriptors for all loaded skills.
3713    /// 返回当前已加载全部 skill 的通用运行时入口描述。
3714    pub fn list_entries(&self) -> Vec<RuntimeEntryDescriptor> {
3715        self.entry_registry
3716            .values()
3717            .filter_map(|target| {
3718                let skill = self.skills.get(&target.skill_storage_key)?;
3719                let tool = skill.meta.find_tool_by_local_name(&target.local_name)?;
3720                Some(RuntimeEntryDescriptor {
3721                    canonical_name: target.canonical_name.clone(),
3722                    skill_id: target.skill_id.clone(),
3723                    local_name: target.local_name.clone(),
3724                    root_name: skill.root_name.clone(),
3725                    skill_dir: skill.dir.display().to_string(),
3726                    description: tool.description.clone(),
3727                    parameters: tool
3728                        .parameters
3729                        .iter()
3730                        .map(|parameter| RuntimeEntryParameterDescriptor {
3731                            name: parameter.name.clone(),
3732                            param_type: parameter.param_type.clone(),
3733                            description: parameter.description.clone(),
3734                            required: parameter.required,
3735                        })
3736                        .collect(),
3737                })
3738            })
3739            .collect()
3740    }
3741
3742    /// List all structured help trees currently registered in the runtime.
3743    /// 列出当前运行时中已注册的全部结构化帮助树。
3744    pub fn list_skill_help(&self) -> Vec<RuntimeSkillHelpDescriptor> {
3745        let mut descriptors = self
3746            .skills
3747            .values()
3748            .map(|skill| RuntimeSkillHelpDescriptor {
3749                skill_id: skill.meta.effective_skill_id().to_string(),
3750                skill_name: skill.meta.name.clone(),
3751                skill_version: skill.meta.version().to_string(),
3752                root_name: skill.root_name.clone(),
3753                skill_dir: skill.dir.display().to_string(),
3754                main: self.build_help_node_descriptor(skill, skill.meta.main_help(), true),
3755                flows: skill
3756                    .meta
3757                    .help_topics()
3758                    .map(|topic| self.build_help_node_descriptor(skill, topic, false))
3759                    .collect::<Vec<RuntimeHelpNodeDescriptor>>(),
3760            })
3761            .collect::<Vec<RuntimeSkillHelpDescriptor>>();
3762
3763        descriptors.sort_by(|left, right| left.skill_id.cmp(&right.skill_id));
3764        descriptors
3765    }
3766
3767    /// Render one structured help detail payload for one skill flow node.
3768    /// 为单个 skill 流程节点渲染一份结构化帮助详情载荷。
3769    pub fn render_skill_help_detail(
3770        &self,
3771        skill_id: &str,
3772        flow_name: &str,
3773        request_context: Option<&RuntimeRequestContext>,
3774    ) -> Result<Option<RuntimeHelpDetail>, String> {
3775        let Some(skill) = self
3776            .skills
3777            .values()
3778            .find(|skill| skill.meta.effective_skill_id() == skill_id)
3779        else {
3780            return Ok(None);
3781        };
3782
3783        let normalized_flow_name = flow_name.trim();
3784        if normalized_flow_name.is_empty() {
3785            return Err("Help flow name must not be empty".to_string());
3786        }
3787
3788        let (selected_help, is_main) = if normalized_flow_name == "main" {
3789            (skill.meta.main_help(), true)
3790        } else {
3791            (
3792                skill
3793                    .meta
3794                    .find_help_topic(normalized_flow_name)
3795                    .ok_or_else(|| {
3796                        format!(
3797                            "Skill '{}' does not declare help flow '{}'",
3798                            skill.meta.effective_skill_id(),
3799                            normalized_flow_name
3800                        )
3801                    })?,
3802                false,
3803            )
3804        };
3805
3806        let rendered_body =
3807            self.render_help_payload(skill, &selected_help.file, request_context)?;
3808        let descriptor = self.build_help_node_descriptor(skill, selected_help, is_main);
3809        Ok(Some(RuntimeHelpDetail {
3810            skill_id: skill.meta.effective_skill_id().to_string(),
3811            skill_name: skill.meta.name.clone(),
3812            skill_version: skill.meta.version().to_string(),
3813            root_name: skill.root_name.clone(),
3814            skill_dir: skill.dir.display().to_string(),
3815            flow_name: descriptor.flow_name,
3816            description: descriptor.description,
3817            related_entries: descriptor.related_entries,
3818            is_main: descriptor.is_main,
3819            content_type: "markdown".to_string(),
3820            content: rendered_body,
3821        }))
3822    }
3823
3824    /// Build one structured help node descriptor with related canonical entries.
3825    /// 构建单个结构化帮助节点描述及其关联 canonical 入口列表。
3826    fn build_help_node_descriptor(
3827        &self,
3828        skill: &LoadedSkill,
3829        help_node: &crate::lua_skill::SkillHelpNodeMeta,
3830        is_main: bool,
3831    ) -> RuntimeHelpNodeDescriptor {
3832        let flow_name = if is_main {
3833            "main".to_string()
3834        } else {
3835            help_node.name.trim().to_string()
3836        };
3837        let related_entries = if is_main {
3838            skill
3839                .meta
3840                .entries()
3841                .filter_map(|entry| {
3842                    skill
3843                        .resolved_tool_name(entry.name.trim())
3844                        .map(str::to_string)
3845                })
3846                .collect::<Vec<String>>()
3847        } else {
3848            skill
3849                .meta
3850                .entries_for_help_topic(help_node.name.trim())
3851                .filter_map(|entry| {
3852                    skill
3853                        .resolved_tool_name(entry.name.trim())
3854                        .map(str::to_string)
3855                })
3856                .collect::<Vec<String>>()
3857        };
3858
3859        RuntimeHelpNodeDescriptor {
3860            flow_name,
3861            description: help_node.description.trim().to_string(),
3862            related_entries,
3863            is_main,
3864        }
3865    }
3866
3867    /// Return configured completion candidates for a prompt argument, if declared by a skill.
3868    /// 返回某个提示词参数在 skill 元数据中声明的候选补全项。
3869    pub fn prompt_argument_completions(
3870        &self,
3871        prompt_name: &str,
3872        argument_name: &str,
3873    ) -> Option<Vec<String>> {
3874        let _ = prompt_name;
3875        let _ = argument_name;
3876        None
3877    }
3878
3879    /// Check if a tool_name is a Lua skill.
3880    pub fn is_skill(&self, name: &str) -> bool {
3881        self.entry_registry.contains_key(name)
3882    }
3883
3884    /// Return the owning skill name for an MCP tool name; return `None` when the tool is not provided by a Lua skill.
3885    /// 根据 MCP 工具名返回所属 skill 名称;未命中时返回 `None`。
3886    pub fn skill_name_for_tool(&self, tool_name: &str) -> Option<String> {
3887        self.entry_registry
3888            .get(tool_name)
3889            .map(|target| target.skill_id.clone())
3890    }
3891
3892    /// List flattened skill config records for one optional skill namespace.
3893    /// 列出某个可选技能命名空间下的扁平化技能配置记录。
3894    pub fn list_skill_config_entries(
3895        &self,
3896        skill_id: Option<&str>,
3897    ) -> Result<Vec<SkillConfigEntry>, String> {
3898        self.skill_config_store.list_entries(skill_id)
3899    }
3900
3901    /// Read one optional string config value for one `(skill_id, key)` pair.
3902    /// 读取某个 `(skill_id, key)` 对下的可选字符串配置值。
3903    pub fn get_skill_config_value(
3904        &self,
3905        skill_id: &str,
3906        key: &str,
3907    ) -> Result<Option<String>, String> {
3908        self.skill_config_store.get_value(skill_id, key)
3909    }
3910
3911    /// Insert or replace one string config value for one `(skill_id, key)` pair.
3912    /// 为某个 `(skill_id, key)` 对插入或替换一个字符串配置值。
3913    pub fn set_skill_config_value(
3914        &mut self,
3915        skill_id: &str,
3916        key: &str,
3917        value: &str,
3918    ) -> Result<(), String> {
3919        self.skill_config_store.set_value(skill_id, key, value)
3920    }
3921
3922    /// Delete one config key from one skill namespace and report whether one value was removed.
3923    /// 从某个技能命名空间删除单个配置键,并返回是否移除了一个值。
3924    pub fn delete_skill_config_value(&mut self, skill_id: &str, key: &str) -> Result<bool, String> {
3925        self.skill_config_store.delete_value(skill_id, key)
3926    }
3927
3928    /// Populate per-request context into the `vulcan` module.
3929    /// 将单次请求的上下文注入到 `vulcan` 模块中。
3930    fn populate_vulcan_request_context(
3931        lua: &Lua,
3932        invocation_context: Option<&LuaInvocationContext>,
3933    ) -> Result<(), String> {
3934        let context_table = get_vulcan_context_table(lua)?;
3935        let request_context =
3936            invocation_context.and_then(|context| context.request_context.as_ref());
3937        let context_value = match request_context {
3938            Some(context) => serde_json::to_value(context)
3939                .map_err(|error| format!("Failed to serialize request context: {}", error))?,
3940            None => Value::Object(serde_json::Map::new()),
3941        };
3942        let context_lua = json_value_to_lua(lua, &context_value)
3943            .map_err(|error| format!("Failed to convert request context to Lua: {}", error))?;
3944        let client_info_value = match &context_value {
3945            Value::Object(object) => object.get("client_info").cloned().unwrap_or(Value::Null),
3946            _ => Value::Null,
3947        };
3948        let client_capabilities_value = match &context_value {
3949            Value::Object(object) => object
3950                .get("client_capabilities")
3951                .cloned()
3952                .unwrap_or_else(|| Value::Object(serde_json::Map::new())),
3953            _ => Value::Object(serde_json::Map::new()),
3954        };
3955        let client_info_lua = json_value_to_lua(lua, &client_info_value)
3956            .map_err(|error| format!("Failed to convert client_info to Lua: {}", error))?;
3957        let client_capabilities_lua = json_value_to_lua(lua, &client_capabilities_value)
3958            .map_err(|error| format!("Failed to convert client_capabilities to Lua: {}", error))?;
3959        let client_budget_value = invocation_context
3960            .map(|context| context.client_budget.clone())
3961            .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
3962        let client_budget_lua = json_value_to_lua(lua, &client_budget_value)
3963            .map_err(|error| format!("Failed to convert client_budget to Lua: {}", error))?;
3964        let tool_config_value = invocation_context
3965            .map(|context| context.tool_config.clone())
3966            .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
3967        let tool_config_lua = json_value_to_lua(lua, &tool_config_value)
3968            .map_err(|error| format!("Failed to convert tool_config to Lua: {}", error))?;
3969
3970        context_table
3971            .set("request", context_lua)
3972            .map_err(|error| format!("Failed to set vulcan.context.request: {}", error))?;
3973        context_table
3974            .set("client_info", client_info_lua)
3975            .map_err(|error| format!("Failed to set vulcan.context.client_info: {}", error))?;
3976        context_table
3977            .set("client_capabilities", client_capabilities_lua)
3978            .map_err(|error| {
3979                format!(
3980                    "Failed to set vulcan.context.client_capabilities: {}",
3981                    error
3982                )
3983            })?;
3984        context_table
3985            .set("client_budget", client_budget_lua)
3986            .map_err(|error| format!("Failed to set vulcan.context.client_budget: {}", error))?;
3987        context_table
3988            .set("tool_config", tool_config_lua)
3989            .map_err(|error| format!("Failed to set vulcan.context.tool_config: {}", error))?;
3990        Ok(())
3991    }
3992
3993    /// Populate the skill-scoped LanceDB host interface into the `vulcan` module.
3994    /// 将按 skill 作用域隔离的 LanceDB 宿主接口注入到 `vulcan` 模块中。
3995    fn populate_vulcan_lancedb_context(
3996        lua: &Lua,
3997        binding: Option<Arc<LanceDbSkillBinding>>,
3998        current_skill_name: Option<&str>,
3999    ) -> Result<(), String> {
4000        let vulcan: Table = lua
4001            .globals()
4002            .get("vulcan")
4003            .map_err(|error| format!("Failed to get vulcan module: {}", error))?;
4004
4005        let lancedb_table = lua
4006            .create_table()
4007            .map_err(|error| format!("Failed to create vulcan.lancedb table: {}", error))?;
4008
4009        let current_skill = current_skill_name.unwrap_or("");
4010        vulcan
4011            .set("__lancedb_skill_name", current_skill)
4012            .map_err(|error| format!("Failed to set vulcan.__lancedb_skill_name: {}", error))?;
4013
4014        if let Some(binding) = binding {
4015            lancedb_table
4016                .set("enabled", true)
4017                .map_err(|error| format!("Failed to set vulcan.lancedb.enabled: {}", error))?;
4018            let info_binding = binding.clone();
4019            let info_fn = lua
4020                .create_function(move |lua, ()| {
4021                    json_value_to_lua(lua, &info_binding.info_json()).map_err(mlua::Error::external)
4022                })
4023                .map_err(|error| format!("Failed to create vulcan.lancedb.info: {}", error))?;
4024            lancedb_table
4025                .set("info", info_fn)
4026                .map_err(|error| format!("Failed to set vulcan.lancedb.info: {}", error))?;
4027
4028            let status_binding = binding.clone();
4029            let status_fn = lua
4030                .create_function(move |lua, ()| {
4031                    json_value_to_lua(lua, &status_binding.status_json())
4032                        .map_err(mlua::Error::external)
4033                })
4034                .map_err(|error| format!("Failed to create vulcan.lancedb.status: {}", error))?;
4035            lancedb_table
4036                .set("status", status_fn)
4037                .map_err(|error| format!("Failed to set vulcan.lancedb.status: {}", error))?;
4038
4039            let create_binding = binding.clone();
4040            let create_table_fn = lua
4041                .create_function(move |lua, input: LuaValue| {
4042                    let input_table = require_table_arg(input, "lancedb.create_table", "input")?;
4043                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4044                        .map_err(mlua::Error::runtime)?;
4045                    let result = create_binding
4046                        .create_table_json(&input_json)
4047                        .map_err(mlua::Error::runtime)?;
4048                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4049                })
4050                .map_err(|error| {
4051                    format!("Failed to create vulcan.lancedb.create_table: {}", error)
4052                })?;
4053            lancedb_table
4054                .set("create_table", create_table_fn)
4055                .map_err(|error| format!("Failed to set vulcan.lancedb.create_table: {}", error))?;
4056
4057            let upsert_binding = binding.clone();
4058            let vector_upsert_fn = lua
4059                .create_function(move |lua, input: LuaValue| {
4060                    let input_table = require_table_arg(input, "lancedb.vector_upsert", "input")?;
4061                    let mut input_json = lua_value_to_json(&LuaValue::Table(input_table))
4062                        .map_err(mlua::Error::runtime)?;
4063                    let input_object = input_json.as_object_mut().ok_or_else(|| {
4064                        mlua::Error::runtime("lancedb.vector_upsert input must be an object")
4065                    })?;
4066
4067                    let payload_value = if let Some(rows) = input_object.remove("rows") {
4068                        input_object
4069                            .entry("input_format".to_string())
4070                            .or_insert_with(|| Value::String("json".to_string()));
4071                        rows
4072                    } else if let Some(data) = input_object.remove("data") {
4073                        data
4074                    } else {
4075                        return Err(mlua::Error::runtime(
4076                            "lancedb.vector_upsert requires rows or data",
4077                        ));
4078                    };
4079
4080                    let payload_bytes = match payload_value {
4081                        Value::String(text) => {
4082                            if !input_object.contains_key("input_format") {
4083                                input_object.insert(
4084                                    "input_format".to_string(),
4085                                    Value::String("arrow_ipc".to_string()),
4086                                );
4087                            }
4088                            text.into_bytes()
4089                        }
4090                        Value::Array(_) | Value::Object(_) => {
4091                            if !input_object.contains_key("input_format") {
4092                                input_object.insert(
4093                                    "input_format".to_string(),
4094                                    Value::String("json".to_string()),
4095                                );
4096                            }
4097                            serde_json::to_vec(&payload_value).map_err(|error| {
4098                                mlua::Error::runtime(format!(
4099                                    "failed to encode lancedb upsert payload: {}",
4100                                    error
4101                                ))
4102                            })?
4103                        }
4104                        _ => {
4105                            return Err(mlua::Error::runtime(
4106                                "lancedb.vector_upsert payload must be string",
4107                            ));
4108                        }
4109                    };
4110
4111                    let result = upsert_binding
4112                        .vector_upsert_json(&input_json, &payload_bytes)
4113                        .map_err(mlua::Error::runtime)?;
4114                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4115                })
4116                .map_err(|error| {
4117                    format!("Failed to create vulcan.lancedb.vector_upsert: {}", error)
4118                })?;
4119            lancedb_table
4120                .set("vector_upsert", vector_upsert_fn)
4121                .map_err(|error| {
4122                    format!("Failed to set vulcan.lancedb.vector_upsert: {}", error)
4123                })?;
4124
4125            let search_binding = binding.clone();
4126            let vector_search_fn = lua
4127                .create_function(move |lua, input: LuaValue| {
4128                    let input_table = require_table_arg(input, "lancedb.vector_search", "input")?;
4129                    let mut input_json = lua_value_to_json(&LuaValue::Table(input_table))
4130                        .map_err(mlua::Error::runtime)?;
4131                    let input_object = input_json.as_object_mut().ok_or_else(|| {
4132                        mlua::Error::runtime("lancedb.vector_search input must be an object")
4133                    })?;
4134                    input_object
4135                        .entry("output_format".to_string())
4136                        .or_insert_with(|| Value::String("json".to_string()));
4137
4138                    let (meta, raw_bytes) = search_binding
4139                        .vector_search_json(&input_json)
4140                        .map_err(mlua::Error::runtime)?;
4141                    let result_table =
4142                        json_to_lua_table_inner(lua, &meta).map_err(mlua::Error::external)?;
4143
4144                    if meta
4145                        .get("format")
4146                        .and_then(Value::as_str)
4147                        .map(|value| value == "json")
4148                        .unwrap_or(false)
4149                    {
4150                        let rows_json: Value =
4151                            serde_json::from_slice(&raw_bytes).map_err(|error| {
4152                                mlua::Error::runtime(format!(
4153                                    "failed to parse LanceDB JSON rows: {}",
4154                                    error
4155                                ))
4156                            })?;
4157                        result_table
4158                            .set(
4159                                "data_json",
4160                                json_value_to_lua(lua, &rows_json)
4161                                    .map_err(mlua::Error::external)?,
4162                            )
4163                            .map_err(mlua::Error::external)?;
4164                    } else {
4165                        result_table
4166                            .set(
4167                                "data",
4168                                LuaValue::String(
4169                                    lua.create_string(&raw_bytes)
4170                                        .map_err(mlua::Error::external)?,
4171                                ),
4172                            )
4173                            .map_err(mlua::Error::external)?;
4174                    }
4175                    Ok(LuaValue::Table(result_table))
4176                })
4177                .map_err(|error| {
4178                    format!("Failed to create vulcan.lancedb.vector_search: {}", error)
4179                })?;
4180            lancedb_table
4181                .set("vector_search", vector_search_fn)
4182                .map_err(|error| {
4183                    format!("Failed to set vulcan.lancedb.vector_search: {}", error)
4184                })?;
4185
4186            let delete_binding = binding.clone();
4187            let delete_fn = lua
4188                .create_function(move |lua, input: LuaValue| {
4189                    let input_table = require_table_arg(input, "lancedb.delete", "input")?;
4190                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4191                        .map_err(mlua::Error::runtime)?;
4192                    let result = delete_binding
4193                        .delete_json(&input_json)
4194                        .map_err(mlua::Error::runtime)?;
4195                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4196                })
4197                .map_err(|error| format!("Failed to create vulcan.lancedb.delete: {}", error))?;
4198            lancedb_table
4199                .set("delete", delete_fn)
4200                .map_err(|error| format!("Failed to set vulcan.lancedb.delete: {}", error))?;
4201
4202            let drop_binding = binding;
4203            let drop_table_fn = lua
4204                .create_function(move |lua, input: LuaValue| {
4205                    let input_table = require_table_arg(input, "lancedb.drop_table", "input")?;
4206                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4207                        .map_err(mlua::Error::runtime)?;
4208                    let result = drop_binding
4209                        .drop_table_json(&input_json)
4210                        .map_err(mlua::Error::runtime)?;
4211                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4212                })
4213                .map_err(|error| {
4214                    format!("Failed to create vulcan.lancedb.drop_table: {}", error)
4215                })?;
4216            lancedb_table
4217                .set("drop_table", drop_table_fn)
4218                .map_err(|error| format!("Failed to set vulcan.lancedb.drop_table: {}", error))?;
4219        } else {
4220            let disabled_status = disabled_skill_status_json(current_skill_name);
4221            lancedb_table
4222                .set("enabled", false)
4223                .map_err(|error| format!("Failed to set vulcan.lancedb.enabled: {}", error))?;
4224            let status_value = disabled_status.clone();
4225            let status_fn = lua
4226                .create_function(move |lua, ()| {
4227                    json_value_to_lua(lua, &status_value).map_err(mlua::Error::external)
4228                })
4229                .map_err(|error| {
4230                    format!("Failed to create disabled vulcan.lancedb.status: {}", error)
4231                })?;
4232            lancedb_table
4233                .set("status", status_fn)
4234                .map_err(|error| format!("Failed to set vulcan.lancedb.status: {}", error))?;
4235            let info_value = disabled_status.clone();
4236            let info_fn = lua
4237                .create_function(move |lua, ()| {
4238                    json_value_to_lua(lua, &info_value).map_err(mlua::Error::external)
4239                })
4240                .map_err(|error| {
4241                    format!("Failed to create disabled vulcan.lancedb.info: {}", error)
4242                })?;
4243            lancedb_table.set("info", info_fn).map_err(|error| {
4244                format!("Failed to set disabled vulcan.lancedb.info: {}", error)
4245            })?;
4246            let disabled_error = "current skill has not enabled lancedb".to_string();
4247            for method_name in [
4248                "create_table",
4249                "vector_upsert",
4250                "vector_search",
4251                "delete",
4252                "drop_table",
4253            ] {
4254                let error_text = disabled_error.clone();
4255                let fn_value = lua
4256                    .create_function(move |_, _: MultiValue| {
4257                        Err::<LuaValue, _>(mlua::Error::runtime(error_text.clone()))
4258                    })
4259                    .map_err(|error| {
4260                        format!("Failed to create disabled vulcan.lancedb proxy: {}", error)
4261                    })?;
4262                lancedb_table.set(method_name, fn_value).map_err(|error| {
4263                    format!("Failed to set disabled method {}: {}", method_name, error)
4264                })?;
4265            }
4266        }
4267
4268        vulcan
4269            .set("lancedb", lancedb_table)
4270            .map_err(|error| format!("Failed to set vulcan.lancedb: {}", error))?;
4271        Ok(())
4272    }
4273
4274    /// Populate the skill-scoped SQLite host interface into the `vulcan` module.
4275    /// 将按 skill 作用域隔离的 SQLite 宿主接口注入到 `vulcan` 模块中。
4276    fn populate_vulcan_sqlite_context(
4277        lua: &Lua,
4278        binding: Option<Arc<SqliteSkillBinding>>,
4279        current_skill_name: Option<&str>,
4280    ) -> Result<(), String> {
4281        let vulcan: Table = lua
4282            .globals()
4283            .get("vulcan")
4284            .map_err(|error| format!("Failed to get vulcan module: {}", error))?;
4285
4286        let sqlite_table = lua
4287            .create_table()
4288            .map_err(|error| format!("Failed to create vulcan.sqlite table: {}", error))?;
4289
4290        let current_skill = current_skill_name.unwrap_or("");
4291        vulcan
4292            .set("__sqlite_skill_name", current_skill)
4293            .map_err(|error| format!("Failed to set vulcan.__sqlite_skill_name: {}", error))?;
4294
4295        if let Some(binding) = binding {
4296            sqlite_table
4297                .set("enabled", true)
4298                .map_err(|error| format!("Failed to set vulcan.sqlite.enabled: {}", error))?;
4299
4300            let info_binding = binding.clone();
4301            let info_fn = lua
4302                .create_function(move |lua, ()| {
4303                    json_value_to_lua(lua, &info_binding.info_json()).map_err(mlua::Error::external)
4304                })
4305                .map_err(|error| format!("Failed to create vulcan.sqlite.info: {}", error))?;
4306            sqlite_table
4307                .set("info", info_fn)
4308                .map_err(|error| format!("Failed to set vulcan.sqlite.info: {}", error))?;
4309
4310            let status_binding = binding.clone();
4311            let status_fn = lua
4312                .create_function(move |lua, ()| {
4313                    json_value_to_lua(lua, &status_binding.status_json())
4314                        .map_err(mlua::Error::external)
4315                })
4316                .map_err(|error| format!("Failed to create vulcan.sqlite.status: {}", error))?;
4317            sqlite_table
4318                .set("status", status_fn)
4319                .map_err(|error| format!("Failed to set vulcan.sqlite.status: {}", error))?;
4320
4321            let tokenize_binding = binding.clone();
4322            let tokenize_fn = lua
4323                .create_function(move |lua, input: LuaValue| {
4324                    let input_table = require_table_arg(input, "sqlite.tokenize_text", "input")?;
4325                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4326                        .map_err(mlua::Error::runtime)?;
4327                    let result = tokenize_binding
4328                        .tokenize_text_json(&input_json)
4329                        .map_err(mlua::Error::runtime)?;
4330                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4331                })
4332                .map_err(|error| {
4333                    format!("Failed to create vulcan.sqlite.tokenize_text: {}", error)
4334                })?;
4335            sqlite_table
4336                .set("tokenize_text", tokenize_fn)
4337                .map_err(|error| format!("Failed to set vulcan.sqlite.tokenize_text: {}", error))?;
4338
4339            let execute_script_binding = binding.clone();
4340            let execute_script_fn = lua
4341                .create_function(move |lua, input: LuaValue| {
4342                    let input_table = require_table_arg(input, "sqlite.execute_script", "input")?;
4343                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4344                        .map_err(mlua::Error::runtime)?;
4345                    let result = execute_script_binding
4346                        .execute_script(&input_json)
4347                        .map_err(mlua::Error::runtime)?;
4348                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4349                })
4350                .map_err(|error| {
4351                    format!("Failed to create vulcan.sqlite.execute_script: {}", error)
4352                })?;
4353            sqlite_table
4354                .set("execute_script", execute_script_fn)
4355                .map_err(|error| {
4356                    format!("Failed to set vulcan.sqlite.execute_script: {}", error)
4357                })?;
4358
4359            let execute_batch_binding = binding.clone();
4360            let execute_batch_fn = lua
4361                .create_function(move |lua, input: LuaValue| {
4362                    let input_table = require_table_arg(input, "sqlite.execute_batch", "input")?;
4363                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4364                        .map_err(mlua::Error::runtime)?;
4365                    let result = execute_batch_binding
4366                        .execute_batch(&input_json)
4367                        .map_err(mlua::Error::runtime)?;
4368                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4369                })
4370                .map_err(|error| {
4371                    format!("Failed to create vulcan.sqlite.execute_batch: {}", error)
4372                })?;
4373            sqlite_table
4374                .set("execute_batch", execute_batch_fn)
4375                .map_err(|error| format!("Failed to set vulcan.sqlite.execute_batch: {}", error))?;
4376
4377            let query_json_binding = binding.clone();
4378            let query_json_fn = lua
4379                .create_function(move |lua, input: LuaValue| {
4380                    let input_table = require_table_arg(input, "sqlite.query_json", "input")?;
4381                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4382                        .map_err(mlua::Error::runtime)?;
4383                    let result = query_json_binding
4384                        .query_json(&input_json)
4385                        .map_err(mlua::Error::runtime)?;
4386                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4387                })
4388                .map_err(|error| format!("Failed to create vulcan.sqlite.query_json: {}", error))?;
4389            sqlite_table
4390                .set("query_json", query_json_fn)
4391                .map_err(|error| format!("Failed to set vulcan.sqlite.query_json: {}", error))?;
4392
4393            let query_stream_binding = binding.clone();
4394            let query_stream_fn = lua
4395                .create_function(move |lua, input: LuaValue| {
4396                    let input_table = require_table_arg(input, "sqlite.query_stream", "input")?;
4397                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4398                        .map_err(mlua::Error::runtime)?;
4399                    let result = query_stream_binding
4400                        .query_stream(&input_json)
4401                        .map_err(mlua::Error::runtime)?;
4402                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4403                })
4404                .map_err(|error| {
4405                    format!("Failed to create vulcan.sqlite.query_stream: {}", error)
4406                })?;
4407            sqlite_table
4408                .set("query_stream", query_stream_fn)
4409                .map_err(|error| format!("Failed to set vulcan.sqlite.query_stream: {}", error))?;
4410
4411            let query_stream_wait_metrics_binding = binding.clone();
4412            let query_stream_wait_metrics_fn = lua
4413                .create_function(move |lua, input: LuaValue| {
4414                    let input_table =
4415                        require_table_arg(input, "sqlite.query_stream_wait_metrics", "input")?;
4416                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4417                        .map_err(mlua::Error::runtime)?;
4418                    let result = query_stream_wait_metrics_binding
4419                        .query_stream_wait_metrics(&input_json)
4420                        .map_err(mlua::Error::runtime)?;
4421                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4422                })
4423                .map_err(|error| {
4424                    format!(
4425                        "Failed to create vulcan.sqlite.query_stream_wait_metrics: {}",
4426                        error
4427                    )
4428                })?;
4429            sqlite_table
4430                .set("query_stream_wait_metrics", query_stream_wait_metrics_fn)
4431                .map_err(|error| {
4432                    format!(
4433                        "Failed to set vulcan.sqlite.query_stream_wait_metrics: {}",
4434                        error
4435                    )
4436                })?;
4437
4438            let query_stream_chunk_binding = binding.clone();
4439            let query_stream_chunk_fn = lua
4440                .create_function(move |lua, input: LuaValue| {
4441                    let input_table =
4442                        require_table_arg(input, "sqlite.query_stream_chunk", "input")?;
4443                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4444                        .map_err(mlua::Error::runtime)?;
4445                    let result = query_stream_chunk_binding
4446                        .query_stream_chunk(&input_json)
4447                        .map_err(mlua::Error::runtime)?;
4448                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4449                })
4450                .map_err(|error| {
4451                    format!(
4452                        "Failed to create vulcan.sqlite.query_stream_chunk: {}",
4453                        error
4454                    )
4455                })?;
4456            sqlite_table
4457                .set("query_stream_chunk", query_stream_chunk_fn)
4458                .map_err(|error| {
4459                    format!("Failed to set vulcan.sqlite.query_stream_chunk: {}", error)
4460                })?;
4461
4462            let query_stream_close_binding = binding.clone();
4463            let query_stream_close_fn = lua
4464                .create_function(move |lua, input: LuaValue| {
4465                    let input_table =
4466                        require_table_arg(input, "sqlite.query_stream_close", "input")?;
4467                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4468                        .map_err(mlua::Error::runtime)?;
4469                    let result = query_stream_close_binding
4470                        .query_stream_close(&input_json)
4471                        .map_err(mlua::Error::runtime)?;
4472                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4473                })
4474                .map_err(|error| {
4475                    format!(
4476                        "Failed to create vulcan.sqlite.query_stream_close: {}",
4477                        error
4478                    )
4479                })?;
4480            sqlite_table
4481                .set("query_stream_close", query_stream_close_fn)
4482                .map_err(|error| {
4483                    format!("Failed to set vulcan.sqlite.query_stream_close: {}", error)
4484                })?;
4485
4486            let upsert_word_binding = binding.clone();
4487            let upsert_word_fn = lua
4488                .create_function(move |lua, input: LuaValue| {
4489                    let input_table =
4490                        require_table_arg(input, "sqlite.upsert_custom_word", "input")?;
4491                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4492                        .map_err(mlua::Error::runtime)?;
4493                    let result = upsert_word_binding
4494                        .upsert_custom_word_json(&input_json)
4495                        .map_err(mlua::Error::runtime)?;
4496                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4497                })
4498                .map_err(|error| {
4499                    format!(
4500                        "Failed to create vulcan.sqlite.upsert_custom_word: {}",
4501                        error
4502                    )
4503                })?;
4504            sqlite_table
4505                .set("upsert_custom_word", upsert_word_fn)
4506                .map_err(|error| {
4507                    format!("Failed to set vulcan.sqlite.upsert_custom_word: {}", error)
4508                })?;
4509
4510            let remove_word_binding = binding.clone();
4511            let remove_word_fn = lua
4512                .create_function(move |lua, input: LuaValue| {
4513                    let input_table =
4514                        require_table_arg(input, "sqlite.remove_custom_word", "input")?;
4515                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4516                        .map_err(mlua::Error::runtime)?;
4517                    let result = remove_word_binding
4518                        .remove_custom_word_json(&input_json)
4519                        .map_err(mlua::Error::runtime)?;
4520                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4521                })
4522                .map_err(|error| {
4523                    format!(
4524                        "Failed to create vulcan.sqlite.remove_custom_word: {}",
4525                        error
4526                    )
4527                })?;
4528            sqlite_table
4529                .set("remove_custom_word", remove_word_fn)
4530                .map_err(|error| {
4531                    format!("Failed to set vulcan.sqlite.remove_custom_word: {}", error)
4532                })?;
4533
4534            let list_words_binding = binding.clone();
4535            let list_words_fn = lua
4536                .create_function(move |lua, ()| {
4537                    let result = list_words_binding
4538                        .list_custom_words_json()
4539                        .map_err(mlua::Error::runtime)?;
4540                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4541                })
4542                .map_err(|error| {
4543                    format!(
4544                        "Failed to create vulcan.sqlite.list_custom_words: {}",
4545                        error
4546                    )
4547                })?;
4548            sqlite_table
4549                .set("list_custom_words", list_words_fn)
4550                .map_err(|error| {
4551                    format!("Failed to set vulcan.sqlite.list_custom_words: {}", error)
4552                })?;
4553
4554            let ensure_index_binding = binding.clone();
4555            let ensure_index_fn = lua
4556                .create_function(move |lua, input: LuaValue| {
4557                    let input_table = require_table_arg(input, "sqlite.ensure_fts_index", "input")?;
4558                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4559                        .map_err(mlua::Error::runtime)?;
4560                    let result = ensure_index_binding
4561                        .ensure_fts_index_json(&input_json)
4562                        .map_err(mlua::Error::runtime)?;
4563                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4564                })
4565                .map_err(|error| {
4566                    format!("Failed to create vulcan.sqlite.ensure_fts_index: {}", error)
4567                })?;
4568            sqlite_table
4569                .set("ensure_fts_index", ensure_index_fn)
4570                .map_err(|error| {
4571                    format!("Failed to set vulcan.sqlite.ensure_fts_index: {}", error)
4572                })?;
4573
4574            let rebuild_index_binding = binding.clone();
4575            let rebuild_index_fn = lua
4576                .create_function(move |lua, input: LuaValue| {
4577                    let input_table =
4578                        require_table_arg(input, "sqlite.rebuild_fts_index", "input")?;
4579                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4580                        .map_err(mlua::Error::runtime)?;
4581                    let result = rebuild_index_binding
4582                        .rebuild_fts_index_json(&input_json)
4583                        .map_err(mlua::Error::runtime)?;
4584                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4585                })
4586                .map_err(|error| {
4587                    format!(
4588                        "Failed to create vulcan.sqlite.rebuild_fts_index: {}",
4589                        error
4590                    )
4591                })?;
4592            sqlite_table
4593                .set("rebuild_fts_index", rebuild_index_fn)
4594                .map_err(|error| {
4595                    format!("Failed to set vulcan.sqlite.rebuild_fts_index: {}", error)
4596                })?;
4597
4598            let upsert_doc_binding = binding.clone();
4599            let upsert_doc_fn = lua
4600                .create_function(move |lua, input: LuaValue| {
4601                    let input_table =
4602                        require_table_arg(input, "sqlite.upsert_fts_document", "input")?;
4603                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4604                        .map_err(mlua::Error::runtime)?;
4605                    let result = upsert_doc_binding
4606                        .upsert_fts_document_json(&input_json)
4607                        .map_err(mlua::Error::runtime)?;
4608                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4609                })
4610                .map_err(|error| {
4611                    format!(
4612                        "Failed to create vulcan.sqlite.upsert_fts_document: {}",
4613                        error
4614                    )
4615                })?;
4616            sqlite_table
4617                .set("upsert_fts_document", upsert_doc_fn)
4618                .map_err(|error| {
4619                    format!("Failed to set vulcan.sqlite.upsert_fts_document: {}", error)
4620                })?;
4621
4622            let delete_doc_binding = binding.clone();
4623            let delete_doc_fn = lua
4624                .create_function(move |lua, input: LuaValue| {
4625                    let input_table =
4626                        require_table_arg(input, "sqlite.delete_fts_document", "input")?;
4627                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4628                        .map_err(mlua::Error::runtime)?;
4629                    let result = delete_doc_binding
4630                        .delete_fts_document_json(&input_json)
4631                        .map_err(mlua::Error::runtime)?;
4632                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4633                })
4634                .map_err(|error| {
4635                    format!(
4636                        "Failed to create vulcan.sqlite.delete_fts_document: {}",
4637                        error
4638                    )
4639                })?;
4640            sqlite_table
4641                .set("delete_fts_document", delete_doc_fn)
4642                .map_err(|error| {
4643                    format!("Failed to set vulcan.sqlite.delete_fts_document: {}", error)
4644                })?;
4645
4646            let search_binding = binding;
4647            let search_fn = lua
4648                .create_function(move |lua, input: LuaValue| {
4649                    let input_table = require_table_arg(input, "sqlite.search_fts", "input")?;
4650                    let input_json = lua_value_to_json(&LuaValue::Table(input_table))
4651                        .map_err(mlua::Error::runtime)?;
4652                    let result = search_binding
4653                        .search_fts_json(&input_json)
4654                        .map_err(mlua::Error::runtime)?;
4655                    json_value_to_lua(lua, &result).map_err(mlua::Error::external)
4656                })
4657                .map_err(|error| format!("Failed to create vulcan.sqlite.search_fts: {}", error))?;
4658            sqlite_table
4659                .set("search_fts", search_fn)
4660                .map_err(|error| format!("Failed to set vulcan.sqlite.search_fts: {}", error))?;
4661        } else {
4662            let disabled_status = disabled_sqlite_skill_status_json(current_skill_name);
4663            sqlite_table
4664                .set("enabled", false)
4665                .map_err(|error| format!("Failed to set vulcan.sqlite.enabled: {}", error))?;
4666            let status_value = disabled_status.clone();
4667            let status_fn = lua
4668                .create_function(move |lua, ()| {
4669                    json_value_to_lua(lua, &status_value).map_err(mlua::Error::external)
4670                })
4671                .map_err(|error| {
4672                    format!("Failed to create disabled vulcan.sqlite.status: {}", error)
4673                })?;
4674            sqlite_table
4675                .set("status", status_fn)
4676                .map_err(|error| format!("Failed to set vulcan.sqlite.status: {}", error))?;
4677            let info_value = disabled_status.clone();
4678            let info_fn = lua
4679                .create_function(move |lua, ()| {
4680                    json_value_to_lua(lua, &info_value).map_err(mlua::Error::external)
4681                })
4682                .map_err(|error| {
4683                    format!("Failed to create disabled vulcan.sqlite.info: {}", error)
4684                })?;
4685            sqlite_table
4686                .set("info", info_fn)
4687                .map_err(|error| format!("Failed to set disabled vulcan.sqlite.info: {}", error))?;
4688            let disabled_error = "current skill has not enabled sqlite".to_string();
4689            for method_name in [
4690                "execute_script",
4691                "execute_batch",
4692                "query_json",
4693                "query_stream",
4694                "query_stream_wait_metrics",
4695                "query_stream_chunk",
4696                "query_stream_close",
4697                "tokenize_text",
4698                "upsert_custom_word",
4699                "remove_custom_word",
4700                "list_custom_words",
4701                "ensure_fts_index",
4702                "rebuild_fts_index",
4703                "upsert_fts_document",
4704                "delete_fts_document",
4705                "search_fts",
4706            ] {
4707                let error_text = disabled_error.clone();
4708                let fn_value = lua
4709                    .create_function(move |_, _: MultiValue| {
4710                        Err::<LuaValue, _>(mlua::Error::runtime(error_text.clone()))
4711                    })
4712                    .map_err(|error| {
4713                        format!("Failed to create disabled vulcan.sqlite proxy: {}", error)
4714                    })?;
4715                sqlite_table.set(method_name, fn_value).map_err(|error| {
4716                    format!("Failed to set disabled method {}: {}", method_name, error)
4717                })?;
4718            }
4719        }
4720
4721        vulcan
4722            .set("sqlite", sqlite_table)
4723            .map_err(|error| format!("Failed to set vulcan.sqlite: {}", error))?;
4724        Ok(())
4725    }
4726
4727    /// Call a loaded Lua skill with the given JSON arguments.
4728    /// This is synchronous — wrap in spawn_blocking for async contexts.
4729    pub fn call_skill(
4730        &self,
4731        tool_name: &str,
4732        args: &Value,
4733        invocation_context: Option<&LuaInvocationContext>,
4734    ) -> Result<RuntimeInvocationResult, String> {
4735        let resolved_target = self
4736            .entry_registry
4737            .get(tool_name)
4738            .ok_or_else(|| format!("Lua skill '{}' not found", tool_name))?;
4739        let skill = self
4740            .skills
4741            .get(&resolved_target.skill_storage_key)
4742            .ok_or_else(|| format!("Lua skill '{}' not found", tool_name))?;
4743        let tool = skill
4744            .meta
4745            .find_tool_by_local_name(&resolved_target.local_name)
4746            .ok_or_else(|| format!("Lua skill '{}' not found", tool_name))?;
4747        let display_tool_name = resolved_target.canonical_name.clone();
4748
4749        let module_name = tool.lua_module.clone();
4750        let func_name = format!("__skill_{}", module_name);
4751
4752        let mut lease = self.acquire_vm()?;
4753        let scope_guard = LuaVmRequestScopeGuard::new(&mut lease, self.host_options.as_ref())?;
4754        let lua = scope_guard.lua();
4755
4756        if skill.meta.debug {
4757            Self::compile_skill_into_lua(lua, skill, tool, true)?;
4758        }
4759
4760        Self::populate_vulcan_request_context(lua, invocation_context)?;
4761        populate_vulcan_internal_execution_context(
4762            lua,
4763            &VulcanInternalExecutionContext {
4764                tool_name: Some(display_tool_name.clone()),
4765                skill_name: Some(skill.meta.effective_skill_id().to_string()),
4766                luaexec_active: false,
4767                luaexec_caller_tool_name: None,
4768            },
4769        )?;
4770        let entry_path = tool_entry_path(&skill.dir, tool);
4771        populate_vulcan_file_context(lua, Some(&skill.dir), Some(&entry_path))?;
4772        populate_vulcan_dependency_context(
4773            lua,
4774            self.host_options.as_ref(),
4775            Some(&skill.dir),
4776            Some(skill.meta.effective_skill_id()),
4777        )?;
4778        Self::populate_vulcan_lancedb_context(
4779            lua,
4780            skill.lancedb_binding.clone(),
4781            Some(skill.meta.effective_skill_id()),
4782        )?;
4783        Self::populate_vulcan_sqlite_context(
4784            lua,
4785            skill.sqlite_binding.clone(),
4786            Some(skill.meta.effective_skill_id()),
4787        )?;
4788
4789        let handler: Function = lua
4790            .globals()
4791            .get(func_name.as_str())
4792            .map_err(|e| format!("Skill function '{}' not found: {}", module_name, e))?;
4793
4794        // Convert JSON args to Lua table
4795        let args_table = json_to_lua_table(lua, args)?;
4796
4797        let call_result = (|| {
4798            // Call the function
4799            let result: MultiValue = handler.call(args_table).map_err(|e| {
4800                let msg = format!("Lua skill '{}' error: {}", display_tool_name, e);
4801                log_error(format!("[LuaSkill:error] {}", msg));
4802                msg
4803            })?;
4804
4805            parse_tool_call_output(result, &display_tool_name).map_err(|e| {
4806                log_error(format!("[LuaSkill:error] {}", e));
4807                e
4808            })
4809        })();
4810        let cleanup_result = scope_guard.finish();
4811        match (call_result, cleanup_result) {
4812            (Ok(result), Ok(())) => Ok(result),
4813            (Ok(_), Err(cleanup_error)) => Err(cleanup_error),
4814            (Err(call_error), Ok(())) => Err(call_error),
4815            (Err(call_error), Err(cleanup_error)) => Err(format!(
4816                "{}; pooled Lua VM cleanup failed: {}",
4817                call_error, cleanup_error
4818            )),
4819        }
4820    }
4821
4822    /// Execute arbitrary Lua code and return the result.
4823    pub fn run_lua(
4824        &self,
4825        code: &str,
4826        args: &Value,
4827        invocation_context: Option<&LuaInvocationContext>,
4828    ) -> Result<Value, String> {
4829        let mut lease = self.acquire_vm()?;
4830        let scope_guard = LuaVmRequestScopeGuard::new(&mut lease, self.host_options.as_ref())?;
4831        let lua = scope_guard.lua();
4832        Self::populate_vulcan_request_context(lua, invocation_context)?;
4833        populate_vulcan_internal_execution_context(
4834            lua,
4835            &VulcanInternalExecutionContext::default(),
4836        )?;
4837        populate_vulcan_file_context(lua, None, None)?;
4838        populate_vulcan_dependency_context(lua, self.host_options.as_ref(), None, None)?;
4839        Self::populate_vulcan_lancedb_context(lua, None, None)?;
4840        Self::populate_vulcan_sqlite_context(lua, None, None)?;
4841
4842        // Build a wrapper that passes args as a local variable
4843        let args_table = json_to_lua_table(lua, args)?;
4844        lua.globals()
4845            .set("__runlua_args", args_table)
4846            .map_err(|e| format!("Failed to set args: {}", e))?;
4847
4848        let wrapper = format!(
4849            "return (function()\n  local args = __runlua_args\n  {}\nend)()",
4850            code
4851        );
4852
4853        let run_result = (|| {
4854            let result = lua.load(&wrapper).eval::<LuaValue>().map_err(|e| {
4855                let msg = format!("Lua run_lua error: {}", e);
4856                log_error(format!("[LuaSkill:error] {}", msg));
4857                msg
4858            })?;
4859
4860            lua_value_to_json(&result)
4861        })();
4862        let cleanup_result = scope_guard.finish();
4863        match (run_result, cleanup_result) {
4864            (Ok(result), Ok(())) => Ok(result),
4865            (Ok(_), Err(cleanup_error)) => Err(cleanup_error),
4866            (Err(run_error), Ok(())) => Err(run_error),
4867            (Err(run_error), Err(cleanup_error)) => Err(format!(
4868                "{}; pooled Lua VM cleanup failed: {}",
4869                run_error, cleanup_error
4870            )),
4871        }
4872    }
4873
4874    /// Acquire one isolated runlua VM from the dedicated pool.
4875    /// 从独立池中获取一个隔离 runlua 虚拟机。
4876    fn acquire_runlua_vm(
4877        runlua_pool: Arc<LuaVmPool>,
4878        skills: Arc<HashMap<String, LoadedSkill>>,
4879        entry_registry: Arc<BTreeMap<String, ResolvedEntryTarget>>,
4880        host_options: Arc<LuaRuntimeHostOptions>,
4881        skill_config_store: Arc<SkillConfigStore>,
4882        lancedb_host: Option<Arc<LanceDbSkillHost>>,
4883        sqlite_host: Option<Arc<SqliteSkillHost>>,
4884    ) -> Result<LuaVmLease, String> {
4885        runlua_pool.acquire(move || {
4886            Self::create_runlua_vm(
4887                skills.as_ref(),
4888                entry_registry.as_ref(),
4889                host_options.clone(),
4890                skill_config_store.clone(),
4891                lancedb_host.clone(),
4892                sqlite_host.clone(),
4893            )
4894        })
4895    }
4896
4897    /// Execute one isolated runlua request through the dedicated pooled runtime.
4898    /// 通过独立的池化运行时执行一次隔离 runlua 请求。
4899    fn execute_runlua_request_inline_with_runtime(
4900        request: &RunLuaExecRequest,
4901        runlua_pool: Arc<LuaVmPool>,
4902        skills: Arc<HashMap<String, LoadedSkill>>,
4903        entry_registry: Arc<BTreeMap<String, ResolvedEntryTarget>>,
4904        host_options: Arc<LuaRuntimeHostOptions>,
4905        skill_config_store: Arc<SkillConfigStore>,
4906        lancedb_host: Option<Arc<LanceDbSkillHost>>,
4907        sqlite_host: Option<Arc<SqliteSkillHost>>,
4908    ) -> Result<String, String> {
4909        if request.timeout_ms == 0 {
4910            return Err("luaexec timeout_ms must be greater than 0".to_string());
4911        }
4912        let (resolved_code, entry_file) = Self::resolve_runlua_source(request)?;
4913        let mut lease = Self::acquire_runlua_vm(
4914            runlua_pool,
4915            skills,
4916            entry_registry,
4917            host_options.clone(),
4918            skill_config_store,
4919            lancedb_host,
4920            sqlite_host,
4921        )?;
4922        let scope_guard = LuaVmRequestScopeGuard::new(&mut lease, host_options.as_ref())?;
4923        let lua = scope_guard.lua();
4924        let simulated_request_context = build_luaexec_call_request_context();
4925        let simulated_invocation_context = LuaInvocationContext::new(
4926            Some(simulated_request_context),
4927            Value::Object(serde_json::Map::new()),
4928            Value::Object(serde_json::Map::new()),
4929        );
4930        Self::populate_vulcan_request_context(lua, Some(&simulated_invocation_context))?;
4931        populate_vulcan_internal_execution_context(
4932            lua,
4933            &VulcanInternalExecutionContext {
4934                tool_name: None,
4935                skill_name: None,
4936                luaexec_active: true,
4937                luaexec_caller_tool_name: request.caller_tool_name.clone(),
4938            },
4939        )?;
4940        populate_vulcan_file_context(lua, None, entry_file.as_deref())?;
4941        Self::populate_vulcan_lancedb_context(lua, None, None)?;
4942        Self::populate_vulcan_sqlite_context(lua, None, None)?;
4943
4944        let captured_output: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
4945        Self::configure_runlua_execution_environment(lua, captured_output.clone())?;
4946
4947        let args_table = json_to_lua_table(lua, &request.args)?;
4948        lua.globals()
4949            .set("__runlua_args", args_table)
4950            .map_err(|error| format!("Failed to set runlua args: {}", error))?;
4951
4952        let wrapper = format!(
4953            "return (function()\n  local args = __runlua_args\n  return table.pack((function()\n{}\nend)())\nend)()",
4954            resolved_code
4955        );
4956
4957        Self::install_runlua_timeout_guard(lua, request.timeout_ms)
4958            .map_err(|error| error.to_string())?;
4959        let execution_result = Self::execute_runlua_wrapper(lua, &wrapper, entry_file.as_deref());
4960        Self::remove_runlua_timeout_guard(lua);
4961        let printed_output = captured_output
4962            .lock()
4963            .map_err(|_| "Failed to lock runlua output capture".to_string())?
4964            .clone();
4965
4966        let render_result = match execution_result {
4967            Ok(returned_values) => {
4968                let rendered_values = Self::collect_runlua_return_values(&returned_values)?;
4969                Ok(Self::render_runlua_success_markdown(
4970                    request,
4971                    &printed_output,
4972                    &rendered_values,
4973                ))
4974            }
4975            Err(error) => Ok(Self::render_runlua_error_markdown(
4976                request,
4977                &printed_output,
4978                error.to_string().as_str(),
4979            )),
4980        };
4981        let cleanup_result = scope_guard.finish();
4982        match (render_result, cleanup_result) {
4983            (Ok(rendered), Ok(())) => Ok(rendered),
4984            (Ok(_), Err(cleanup_error)) => Err(cleanup_error),
4985            (Err(render_error), Ok(())) => Err(render_error),
4986            (Err(render_error), Err(cleanup_error)) => Err(format!(
4987                "{}; pooled runlua VM cleanup failed: {}",
4988                render_error, cleanup_error
4989            )),
4990        }
4991    }
4992
4993    /// Execute one isolated runlua request using the current engine snapshots.
4994    /// 使用当前引擎快照执行一次隔离 runlua 请求。
4995    fn execute_runlua_request_inline(&self, request: &RunLuaExecRequest) -> Result<String, String> {
4996        Self::execute_runlua_request_inline_with_runtime(
4997            request,
4998            self.runlua_pool.clone(),
4999            Arc::new(self.skills.clone()),
5000            Arc::new(self.entry_registry.clone()),
5001            self.host_options.clone(),
5002            self.skill_config_store.clone(),
5003            self.lancedb_host.clone(),
5004            self.sqlite_host.clone(),
5005        )
5006    }
5007
5008    /// Resolve one runlua request into concrete source text and optional entry file context.
5009    /// 将一次 runlua 请求解析成具体源代码文本及可选入口文件上下文。
5010    fn resolve_runlua_source(
5011        request: &RunLuaExecRequest,
5012    ) -> Result<(String, Option<PathBuf>), String> {
5013        let inline_code = request
5014            .code
5015            .as_ref()
5016            .map(|value| value.trim())
5017            .filter(|value| !value.is_empty())
5018            .map(|value| value.to_string());
5019        let file_path = request
5020            .file
5021            .as_ref()
5022            .map(|value| value.trim())
5023            .filter(|value| !value.is_empty())
5024            .map(|value| value.to_string());
5025
5026        match (inline_code, file_path) {
5027            (Some(_), Some(_)) => {
5028                Err("luaexec accepts either code or file, but not both".to_string())
5029            }
5030            (None, None) => Err("luaexec requires code or file".to_string()),
5031            (Some(code), None) => Ok((code, None)),
5032            (None, Some(file_text)) => {
5033                validate_path_text(&file_text, "luaexec", "file")
5034                    .map_err(|error| error.to_string())?;
5035                let raw_file_path = PathBuf::from(&file_text);
5036                let file_path = if raw_file_path.is_absolute() {
5037                    raw_file_path
5038                } else {
5039                    std::env::current_dir()
5040                        .map_err(|error| {
5041                            format!("Failed to resolve luaexec relative file path: {}", error)
5042                        })?
5043                        .join(raw_file_path)
5044                };
5045                let source = std::fs::read_to_string(&file_path).map_err(|error| {
5046                    format!(
5047                        "Failed to read luaexec file {}: {}: {}",
5048                        file_path.display(),
5049                        error,
5050                        error
5051                    )
5052                })?;
5053                Ok((source, Some(file_path)))
5054            }
5055        }
5056    }
5057
5058    /// Execute one inline runlua request from raw JSON text.
5059    /// 从原始 JSON 文本执行一次进程内 runlua 请求。
5060    pub fn execute_runlua_request_json_inline(&self, request_json: &str) -> Result<String, String> {
5061        let request: RunLuaExecRequest = serde_json::from_str(request_json)
5062            .map_err(|error| format!("Invalid luaexec request JSON: {}", error))?;
5063        self.execute_runlua_request_inline(&request)
5064    }
5065
5066    /// Execute the runlua wrapper, optionally switching the process current directory to the entry file directory.
5067    /// 执行 runlua 包装器,并在需要时临时切换进程工作目录到入口文件目录。
5068    fn execute_runlua_wrapper(
5069        lua: &Lua,
5070        wrapper: &str,
5071        entry_file: Option<&Path>,
5072    ) -> Result<Table, mlua::Error> {
5073        match entry_file.and_then(Path::parent) {
5074            Some(entry_dir) => {
5075                let _cwd_guard = runlua_cwd_guard()
5076                    .lock()
5077                    .map_err(|_| mlua::Error::runtime("luaexec cwd guard lock poisoned"))?;
5078                let original_dir = std::env::current_dir()
5079                    .map_err(|error| mlua::Error::runtime(format!("luaexec cwd: {}", error)))?;
5080                std::env::set_current_dir(entry_dir)
5081                    .map_err(|error| mlua::Error::runtime(format!("luaexec set cwd: {}", error)))?;
5082                let execution = lua.load(wrapper).eval::<Table>();
5083                let restore_result = std::env::set_current_dir(&original_dir).map_err(|error| {
5084                    mlua::Error::runtime(format!("luaexec restore cwd: {}", error))
5085                });
5086                match (execution, restore_result) {
5087                    (Ok(table), Ok(())) => Ok(table),
5088                    (Err(error), Ok(())) => Err(error),
5089                    (_, Err(error)) => Err(error),
5090                }
5091            }
5092            None => lua.load(wrapper).eval::<Table>(),
5093        }
5094    }
5095
5096    /// Configure the isolated runlua execution VM.
5097    /// 配置隔离 runlua 执行虚拟机的运行时环境。
5098    fn configure_runlua_execution_environment(
5099        lua: &Lua,
5100        captured_output: Arc<Mutex<Vec<String>>>,
5101    ) -> Result<(), String> {
5102        let runtime = get_vulcan_runtime_table(lua)?;
5103        let runtime_lua = get_vulcan_runtime_lua_table(lua)?;
5104        let cache = get_vulcan_table(lua)?
5105            .get::<Table>("cache")
5106            .map_err(|error| format!("Failed to get vulcan.cache: {}", error))?;
5107
5108        let print_capture = captured_output.clone();
5109        let print_fn = lua
5110            .create_function(move |_, args: MultiValue| {
5111                let mut parts = Vec::new();
5112                for value in args.into_iter() {
5113                    parts.push(LuaEngine::render_lua_value_inline(&value));
5114                }
5115                let mut guard = print_capture
5116                    .lock()
5117                    .map_err(|_| mlua::Error::runtime("runlua print capture lock poisoned"))?;
5118                guard.push(parts.join("\t"));
5119                Ok(())
5120            })
5121            .map_err(|error| format!("Failed to create runlua print capture: {}", error))?;
5122        lua.globals()
5123            .set("print", print_fn)
5124            .map_err(|error| format!("Failed to override global print for runlua: {}", error))?;
5125
5126        lua.load(
5127            r#"
5128if jit and type(jit.off) == "function" then
5129    jit.off(true, true)
5130end
5131if jit and type(jit.flush) == "function" then
5132    jit.flush()
5133end
5134"#,
5135        )
5136        .exec()
5137        .map_err(|error| format!("Failed to disable JIT for runlua: {}", error))?;
5138
5139        runtime
5140            .set("log", LuaValue::Nil)
5141            .map_err(|error| format!("Failed to clear vulcan.runtime.log for runlua: {}", error))?;
5142        cache
5143            .set("put", LuaValue::Nil)
5144            .map_err(|error| format!("Failed to clear vulcan.cache.put for runlua: {}", error))?;
5145        cache
5146            .set("get", LuaValue::Nil)
5147            .map_err(|error| format!("Failed to clear vulcan.cache.get for runlua: {}", error))?;
5148        cache.set("delete", LuaValue::Nil).map_err(|error| {
5149            format!("Failed to clear vulcan.cache.delete for runlua: {}", error)
5150        })?;
5151        runtime_lua.set("exec", LuaValue::Nil).map_err(|error| {
5152            format!(
5153                "Failed to clear vulcan.runtime.lua.exec for runlua: {}",
5154                error
5155            )
5156        })?;
5157        Ok(())
5158    }
5159
5160    /// Install a hard timeout guard for the isolated luaexec VM.
5161    /// 为隔离 luaexec 虚拟机安装硬超时保护。
5162    fn install_runlua_timeout_guard(lua: &Lua, timeout_ms: u64) -> mlua::Result<()> {
5163        let deadline = Instant::now() + Duration::from_millis(timeout_ms);
5164        let timeout_text = format!("luaexec execution timed out after {} ms", timeout_ms);
5165
5166        lua.set_hook(
5167            HookTriggers::new().every_nth_instruction(1_000),
5168            move |_, _| {
5169                if Instant::now() >= deadline {
5170                    return Err(mlua::Error::runtime(timeout_text.clone()));
5171                }
5172                Ok(VmState::Continue)
5173            },
5174        )
5175    }
5176
5177    /// Remove the previously installed timeout guard from the isolated luaexec VM.
5178    /// 移除隔离 luaexec 虚拟机上已安装的超时保护。
5179    fn remove_runlua_timeout_guard(lua: &Lua) {
5180        lua.remove_hook();
5181    }
5182
5183    /// Collect packed Lua return values from the isolated runlua wrapper.
5184    /// 从隔离 runlua 包装器返回的打包结果中提取所有返回值。
5185    fn collect_runlua_return_values(
5186        result_table: &Table,
5187    ) -> Result<Vec<RunLuaRenderedValue>, String> {
5188        let value_count = result_table
5189            .get::<i64>("n")
5190            .map_err(|error| format!("Failed to read runlua return count: {}", error))?
5191            .max(0) as usize;
5192
5193        let mut rendered_values = Vec::new();
5194        if value_count == 0 {
5195            rendered_values.push(RunLuaRenderedValue {
5196                format: "json",
5197                content: "null".to_string(),
5198            });
5199            return Ok(rendered_values);
5200        }
5201
5202        for index in 1..=value_count {
5203            let value: LuaValue = result_table.raw_get(index).map_err(|error| {
5204                format!("Failed to read runlua return value {}: {}", index, error)
5205            })?;
5206            rendered_values.push(Self::render_runlua_value(&value));
5207        }
5208
5209        Ok(rendered_values)
5210    }
5211
5212    /// Render one Lua return value into a Markdown-ready block payload.
5213    /// 将单个 Lua 返回值渲染为可直接写入 Markdown 代码块的载荷。
5214    fn render_runlua_value(value: &LuaValue) -> RunLuaRenderedValue {
5215        match value {
5216            LuaValue::String(text) => RunLuaRenderedValue {
5217                format: "text",
5218                content: text
5219                    .to_str()
5220                    .map(|value| value.to_string())
5221                    .unwrap_or_default(),
5222            },
5223            _ => match lua_value_to_json(value) {
5224                Ok(json_value) => RunLuaRenderedValue {
5225                    format: "json",
5226                    content: serde_json::to_string_pretty(&json_value)
5227                        .unwrap_or_else(|_| "null".to_string()),
5228                },
5229                Err(_) => RunLuaRenderedValue {
5230                    format: "text",
5231                    content: Self::render_lua_value_inline(value),
5232                },
5233            },
5234        }
5235    }
5236
5237    /// Render one Lua value into a compact single-line textual form.
5238    /// 将单个 Lua 值渲染为紧凑的单行文本形式。
5239    fn render_lua_value_inline(value: &LuaValue) -> String {
5240        match value {
5241            LuaValue::String(text) => text
5242                .to_str()
5243                .map(|value| value.to_string())
5244                .unwrap_or_default(),
5245            LuaValue::Integer(number) => number.to_string(),
5246            LuaValue::Number(number) => number.to_string(),
5247            LuaValue::Boolean(flag) => flag.to_string(),
5248            LuaValue::Nil => "nil".to_string(),
5249            _ => format!("{:?}", value),
5250        }
5251    }
5252
5253    /// Render a successful runlua execution result into Markdown text.
5254    /// 将成功的 runlua 执行结果渲染为 Markdown 文本。
5255    fn render_runlua_success_markdown(
5256        request: &RunLuaExecRequest,
5257        printed_output: &[String],
5258        rendered_values: &[RunLuaRenderedValue],
5259    ) -> String {
5260        let mut lines = vec![
5261            "# Runtime Execution Result".to_string(),
5262            "".to_string(),
5263            "## Task".to_string(),
5264            if request.task.trim().is_empty() {
5265                "Execute Lua runtime code".to_string()
5266            } else {
5267                request.task.trim().to_string()
5268            },
5269            "".to_string(),
5270            "## Status".to_string(),
5271            "SUCCESS".to_string(),
5272        ];
5273
5274        if !printed_output.is_empty() {
5275            lines.extend([
5276                "".to_string(),
5277                "## Printed Output".to_string(),
5278                "```text".to_string(),
5279                printed_output.join("\n"),
5280                "```".to_string(),
5281            ]);
5282        }
5283
5284        lines.extend(["".to_string(), "## Returned Values".to_string()]);
5285
5286        for (index, value) in rendered_values.iter().enumerate() {
5287            lines.push(format!("{}. ", index + 1));
5288            lines.push(format!("```{}", value.format));
5289            lines.push(value.content.clone());
5290            lines.push("```".to_string());
5291            if index + 1 < rendered_values.len() {
5292                lines.push("".to_string());
5293            }
5294        }
5295
5296        lines.join("\n")
5297    }
5298
5299    /// Render a failed runlua execution result into Markdown text.
5300    /// 将失败的 runlua 执行结果渲染为 Markdown 文本。
5301    fn render_runlua_error_markdown(
5302        request: &RunLuaExecRequest,
5303        printed_output: &[String],
5304        error_text: &str,
5305    ) -> String {
5306        let mut lines = vec![
5307            "# Runtime Execution Error".to_string(),
5308            "".to_string(),
5309            "## Task".to_string(),
5310            if request.task.trim().is_empty() {
5311                "Execute Lua runtime code".to_string()
5312            } else {
5313                request.task.trim().to_string()
5314            },
5315            "".to_string(),
5316            "## Status".to_string(),
5317            "FAILED".to_string(),
5318            "".to_string(),
5319            "## Error".to_string(),
5320            "```text".to_string(),
5321            error_text.to_string(),
5322            "```".to_string(),
5323        ];
5324
5325        if !printed_output.is_empty() {
5326            lines.extend([
5327                "".to_string(),
5328                "## Printed Output".to_string(),
5329                "```text".to_string(),
5330                printed_output.join("\n"),
5331                "```".to_string(),
5332            ]);
5333        }
5334
5335        lines.join("\n")
5336    }
5337
5338    /// Render one help payload from either Markdown or Lua.
5339    /// 从 Markdown 或 Lua 渲染单个帮助载荷。
5340    fn render_help_payload(
5341        &self,
5342        skill: &LoadedSkill,
5343        relative_path: &str,
5344        request_context: Option<&RuntimeRequestContext>,
5345    ) -> Result<String, String> {
5346        if !is_lua_help_file(relative_path) {
5347            return read_skill_text_file(&skill.dir, relative_path, "help");
5348        }
5349
5350        let helper_path = skill.dir.join(relative_path);
5351        let helper_source = std::fs::read_to_string(&helper_path).map_err(|error| {
5352            format!(
5353                "Failed to read help file {}: {}",
5354                helper_path.display(),
5355                error
5356            )
5357        })?;
5358        let mut lease = self.acquire_vm()?;
5359        let scope_guard = LuaVmRequestScopeGuard::new(&mut lease, self.host_options.as_ref())?;
5360        let lua = scope_guard.lua();
5361        let help_invocation_context = LuaInvocationContext::new(
5362            request_context.cloned(),
5363            Value::Object(serde_json::Map::new()),
5364            Value::Object(serde_json::Map::new()),
5365        );
5366        Self::populate_vulcan_request_context(lua, Some(&help_invocation_context))?;
5367        populate_vulcan_internal_execution_context(
5368            lua,
5369            &VulcanInternalExecutionContext {
5370                tool_name: Some("vulcan-help".to_string()),
5371                skill_name: Some(skill.meta.effective_skill_id().to_string()),
5372                luaexec_active: false,
5373                luaexec_caller_tool_name: None,
5374            },
5375        )?;
5376        populate_vulcan_file_context(lua, Some(&skill.dir), Some(&helper_path))?;
5377        populate_vulcan_dependency_context(
5378            lua,
5379            self.host_options.as_ref(),
5380            Some(&skill.dir),
5381            Some(skill.meta.effective_skill_id()),
5382        )?;
5383        Self::populate_vulcan_lancedb_context(
5384            lua,
5385            skill.lancedb_binding.clone(),
5386            Some(skill.meta.effective_skill_id()),
5387        )?;
5388        Self::populate_vulcan_sqlite_context(
5389            lua,
5390            skill.sqlite_binding.clone(),
5391            Some(skill.meta.effective_skill_id()),
5392        )?;
5393
5394        let chunk_name = format!("{}-{}", skill.meta.effective_skill_id(), relative_path);
5395        let chunk = lua.load(&helper_source).set_name(&chunk_name);
5396        let rendered_result = (|| {
5397            let exported: LuaValue = chunk
5398                .into_function()
5399                .map_err(|error| {
5400                    format!(
5401                        "Help compile error for {}: {}",
5402                        helper_path.display(),
5403                        error
5404                    )
5405                })?
5406                .call(())
5407                .map_err(|error| {
5408                    format!("Help init error for {}: {}", helper_path.display(), error)
5409                })?;
5410
5411            let rendered_value = match exported {
5412                LuaValue::Function(function) => function.call(()).map_err(|error| {
5413                    format!(
5414                        "Help runtime error for {}: {}",
5415                        helper_path.display(),
5416                        error
5417                    )
5418                })?,
5419                other => other,
5420            };
5421
5422            match rendered_value {
5423                LuaValue::String(text) => {
5424                    text.to_str()
5425                        .map(|value| value.to_string())
5426                        .map_err(|error| {
5427                            format!(
5428                                "Help {} returned invalid UTF-8 text: {}",
5429                                helper_path.display(),
5430                                error
5431                            )
5432                        })
5433                }
5434                other => Err(format!(
5435                    "Help {} must return a plain string, actual_type='{}'",
5436                    helper_path.display(),
5437                    lua_value_type_name(&other)
5438                )),
5439            }
5440        })();
5441        let cleanup_result = scope_guard.finish();
5442        match (rendered_result, cleanup_result) {
5443            (Ok(rendered), Ok(())) => Ok(rendered),
5444            (Ok(_), Err(cleanup_error)) => Err(cleanup_error),
5445            (Err(render_error), Ok(())) => Err(render_error),
5446            (Err(render_error), Err(cleanup_error)) => Err(format!(
5447                "{}; pooled Lua VM cleanup failed: {}",
5448                render_error, cleanup_error
5449            )),
5450        }
5451    }
5452
5453    /// Populate the vulcan.call function to dispatch to loaded skills.
5454    fn populate_vulcan_call_for_lua(
5455        lua: &Lua,
5456        skills_map: &HashMap<String, LoadedSkill>,
5457        entry_registry: &BTreeMap<String, ResolvedEntryTarget>,
5458        host_options: Arc<LuaRuntimeHostOptions>,
5459        lancedb_host: Option<Arc<LanceDbSkillHost>>,
5460        sqlite_host: Option<Arc<SqliteSkillHost>>,
5461    ) -> Result<(), String> {
5462        let vulcan: Table = lua
5463            .globals()
5464            .get("vulcan")
5465            .map_err(|e| format!("vulcan module not found: {}", e))?;
5466
5467        /// Resolved dispatcher metadata for one strict LuaSkills entry.
5468        /// 单个严格 LuaSkills 入口的已解析分发元数据。
5469        #[derive(Clone)]
5470        struct DispatchEntry {
5471            /// Canonical display name used as the active tool name.
5472            /// 作为当前活动工具名使用的 canonical 显示名称。
5473            display_name: String,
5474            /// Lua module name registered in the VM globals.
5475            /// 注册到虚拟机全局表中的 Lua 模块名。
5476            module_name: String,
5477            /// Owning skill id of the current entry.
5478            /// 当前入口所属的 skill id。
5479            owner_skill_id: String,
5480            /// Stable local entry name declared by the owning skill.
5481            /// 所属 skill 声明的稳定局部入口名称。
5482            local_name: String,
5483            /// Owning skill directory used to restore file context.
5484            /// 用于恢复文件上下文的所属 skill 目录。
5485            owner_skill_dir: String,
5486            /// Absolute entry file path used to restore file context.
5487            /// 用于恢复文件上下文的绝对入口文件路径。
5488            entry_path: String,
5489        }
5490
5491        // Create the call dispatcher
5492        let dispatch_entries: Vec<DispatchEntry> = entry_registry
5493            .values()
5494            .filter_map(|target| {
5495                let skill = skills_map.get(&target.skill_storage_key)?;
5496                let tool = skill.meta.find_tool_by_local_name(&target.local_name)?;
5497                let entry_path = tool_entry_path(&skill.dir, tool);
5498                Some(DispatchEntry {
5499                    display_name: target.canonical_name.clone(),
5500                    module_name: tool.lua_module.clone(),
5501                    owner_skill_id: target.skill_id.clone(),
5502                    local_name: target.local_name.clone(),
5503                    owner_skill_dir: skill.dir.to_string_lossy().to_string(),
5504                    entry_path: entry_path.to_string_lossy().to_string(),
5505                })
5506            })
5507            .collect();
5508
5509        let dispatcher = lua
5510            .create_function(move |lua, (name, args): (LuaValue, LuaValue)| {
5511                let name = require_string_arg(name, "call", "name", false)?;
5512                let args = require_table_arg(args, "call", "args")?;
5513                let dispatch_entry = dispatch_entries
5514                    .iter()
5515                    .find(|entry| entry.display_name == name)
5516                    .ok_or_else(|| mlua::Error::runtime(format!("Skill '{}' not found", name)))?;
5517                let module = &dispatch_entry.module_name;
5518                let owner_skill_name = &dispatch_entry.owner_skill_id;
5519                let func_name = format!("__skill_{}", module);
5520                let func: Function = lua.globals().get(func_name.as_str()).map_err(|_| {
5521                    mlua::Error::runtime(format!("Skill function '{}' not found", module))
5522                })?;
5523                let nested_scope_guard = LuaNestedCallScopeGuard::new(
5524                    lua,
5525                    host_options.clone(),
5526                    lancedb_host.clone(),
5527                    sqlite_host.clone(),
5528                )
5529                .map_err(mlua::Error::runtime)?;
5530                let current_request_context_json =
5531                    lua_value_to_json(&nested_scope_guard.previous_context)
5532                        .map_err(mlua::Error::runtime)?;
5533                let current_request_context = match &current_request_context_json {
5534                    Value::Object(object) if object.is_empty() => None,
5535                    _ => serde_json::from_value::<RuntimeRequestContext>(
5536                        current_request_context_json,
5537                    )
5538                    .ok(),
5539                };
5540                let current_client_budget =
5541                    lua_value_to_json(&nested_scope_guard.previous_client_budget)
5542                        .map_err(mlua::Error::runtime)?;
5543                let current_tool_config =
5544                    lua_value_to_json(&nested_scope_guard.previous_tool_config)
5545                        .map_err(mlua::Error::runtime)?;
5546                if nested_scope_guard.previous_internal_context.luaexec_active {
5547                    if nested_scope_guard
5548                        .previous_internal_context
5549                        .luaexec_caller_tool_name
5550                        .as_deref()
5551                        == Some(dispatch_entry.display_name.as_str())
5552                    {
5553                        return Err(mlua::Error::runtime(format!(
5554                            "vulcan.call cannot call the current luaexec caller tool '{}'",
5555                            dispatch_entry.display_name
5556                        )));
5557                    }
5558                    if dispatch_entry.owner_skill_id == "vulcan-runtime"
5559                        && (dispatch_entry.local_name == "lua-exec"
5560                            || dispatch_entry.local_name == "lua-file")
5561                    {
5562                        return Err(mlua::Error::runtime(format!(
5563                            "vulcan.call cannot invoke '{}' inside luaexec",
5564                            dispatch_entry.display_name
5565                        )));
5566                    }
5567                }
5568                let target_binding = match lancedb_host.as_ref() {
5569                    Some(host) => host
5570                        .binding_for_skill(owner_skill_name)
5571                        .map_err(mlua::Error::runtime)?,
5572                    None => None,
5573                };
5574                let target_sqlite_binding = match sqlite_host.as_ref() {
5575                    Some(host) => host
5576                        .binding_for_skill(owner_skill_name)
5577                        .map_err(mlua::Error::runtime)?,
5578                    None => None,
5579                };
5580                let nested_invocation_context = LuaInvocationContext::new(
5581                    current_request_context,
5582                    current_client_budget,
5583                    current_tool_config,
5584                );
5585                nested_scope_guard
5586                    .enter_nested_call(
5587                        &dispatch_entry.display_name,
5588                        owner_skill_name,
5589                        &dispatch_entry.owner_skill_dir,
5590                        &dispatch_entry.entry_path,
5591                        &nested_invocation_context,
5592                        target_binding,
5593                        target_sqlite_binding,
5594                    )
5595                    .map_err(mlua::Error::runtime)?;
5596                let call_result = func.call::<MultiValue>(args);
5597                let restore_result = nested_scope_guard.finish().map_err(mlua::Error::runtime);
5598                match (call_result, restore_result) {
5599                    (Ok(result), Ok(())) => Ok(result),
5600                    (Ok(_), Err(restore_error)) => Err(restore_error),
5601                    (Err(call_error), Ok(())) => Err(call_error),
5602                    (Err(call_error), Err(restore_error)) => Err(mlua::Error::runtime(format!(
5603                        "{}; nested vulcan.call restore failed: {}",
5604                        call_error, restore_error
5605                    ))),
5606                }
5607            })
5608            .map_err(|e| format!("Failed to create vulcan.call dispatcher: {}", e))?;
5609
5610        vulcan
5611            .set("call", dispatcher)
5612            .map_err(|e| format!("Failed to set vulcan.call: {}", e))?;
5613
5614        Ok(())
5615    }
5616
5617    /// Configure package.path and package.cpath to include project-local luarocks tree.
5618    /// 配置 package.path 与 package.cpath,使其只依赖项目内统一的 lua 目录布局。
5619    ///
5620    /// This keeps runtime resolution aligned with the deployed layout under
5621    /// `lua_packages/share/lua/` and `lua_packages/lib/lua/`, instead of relying on
5622    /// versioned `5.1` subdirectories that may not exist in the shipped bundle.
5623    /// This keeps runtime resolution aligned with the deployed layout under
5624    /// 这会让运行时与已部署的目录布局保持一致,
5625    /// `lua_packages/share/lua/` and `lua_packages/lib/lua/`, instead of relying on
5626    /// 即仅依赖 `lua_packages/share/lua/` 与 `lua_packages/lib/lua/`,
5627    /// versioned `5.1` subdirectories that may not exist in the shipped bundle.
5628    /// 而不再依赖发布包中可能并不存在的带版本 `5.1` 子目录。
5629    fn setup_package_paths(
5630        lua: &Lua,
5631        host_options: &LuaRuntimeHostOptions,
5632    ) -> Result<(), Box<dyn std::error::Error>> {
5633        let Some(lua_packages) = host_options.lua_packages_dir.as_ref() else {
5634            return Ok(());
5635        };
5636        if !lua_packages.exists() {
5637            return Ok(());
5638        }
5639
5640        // Build package.cpath entries for C modules (.dll on Windows)
5641        // 统一使用宿主提供的 lua_packages/lib/lua 目录,不再自行推导可执行文件相对路径。
5642        #[cfg(windows)]
5643        let cpath_pattern = format!(
5644            "{}\\lib\\lua\\?.dll;{}\\lib\\lua\\?\\init.dll;{}\\lib\\lua\\loadall.dll;{}\\?\\?.dll;",
5645            lua_packages.display(),
5646            lua_packages.display(),
5647            lua_packages.display(),
5648            lua_packages.display()
5649        );
5650
5651        // Build package.cpath entries for C modules (.so on Linux)
5652        // Linux 下同样严格依赖宿主传入的 lua_packages 根目录。
5653        #[cfg(target_os = "linux")]
5654        let cpath_pattern = format!(
5655            "{}/lib/lua/?.so;{}/lib/lua/?/init.so;{}/lib/lua/loadall.so;{}/?.so;",
5656            lua_packages.display(),
5657            lua_packages.display(),
5658            lua_packages.display(),
5659            lua_packages.display()
5660        );
5661
5662        // Build package.cpath entries for C modules (.dylib on macOS)
5663        // macOS 下同样严格依赖宿主传入的 lua_packages 根目录。
5664        #[cfg(target_os = "macos")]
5665        let cpath_pattern = format!(
5666            "{}/lib/lua/?.dylib;{}/lib/lua/?/init.dylib;{}/lib/lua/loadall.dylib;{}/?.dylib;",
5667            lua_packages.display(),
5668            lua_packages.display(),
5669            lua_packages.display(),
5670            lua_packages.display()
5671        );
5672
5673        // Build package.path entries for Lua modules
5674        // 统一使用宿主提供的 lua_packages/share/lua 目录。
5675        #[cfg(windows)]
5676        let path_pattern = format!(
5677            "{}\\share\\lua\\?.lua;{}\\share\\lua\\?\\init.lua;{}\\?.lua;",
5678            lua_packages.display(),
5679            lua_packages.display(),
5680            lua_packages.display()
5681        );
5682
5683        // Build package.path entries for Lua modules on Unix-like systems
5684        // 类 Unix 平台同样严格依赖宿主传入的 lua_packages 根目录。
5685        #[cfg(unix)]
5686        let path_pattern = format!(
5687            "{}/share/lua/?.lua;{}/share/lua/?/init.lua;{}/?.lua;",
5688            lua_packages.display(),
5689            lua_packages.display(),
5690            lua_packages.display()
5691        );
5692
5693        // Prepend to existing paths
5694        // 将宿主指定路径前置到现有 package 搜索链,避免覆盖 Lua 默认行为。
5695        let package: Table = lua.globals().get("package")?;
5696        let old_cpath: mlua::String = package.get("cpath")?;
5697        let new_cpath = format!("{}{}", cpath_pattern, old_cpath.to_str()?.to_string());
5698        package.set("cpath", lua.create_string(&new_cpath)?)?;
5699
5700        let old_path: mlua::String = package.get("path")?;
5701        let new_path = format!("{}{}", path_pattern, old_path.to_str()?.to_string());
5702        package.set("path", lua.create_string(&new_path)?)?;
5703        Ok(())
5704    }
5705
5706    /// Register the strict `vulcan` module in the Lua VM.
5707    /// 在 Lua 虚拟机中注册严格版 `vulcan` 模块。
5708    fn register_vulcan_module(
5709        lua: &Lua,
5710        host_options: &LuaRuntimeHostOptions,
5711        skill_config_store: Arc<SkillConfigStore>,
5712    ) -> Result<(), Box<dyn std::error::Error>> {
5713        let vulcan = lua.create_table()?;
5714        let runtime = lua.create_table()?;
5715        let runtime_skills = lua.create_table()?;
5716        let runtime_internal = lua.create_table()?;
5717        let runtime_lua = lua.create_table()?;
5718        let fs = lua.create_table()?;
5719        let path = lua.create_table()?;
5720        let process = lua.create_table()?;
5721        let os = lua.create_table()?;
5722        let json = lua.create_table()?;
5723        let cache = lua.create_table()?;
5724        let config = lua.create_table()?;
5725        let context = lua.create_table()?;
5726        let deps = lua.create_table()?;
5727
5728        let runtime_log_fn = lua.create_function(|_, (level, msg): (LuaValue, LuaValue)| {
5729            let level = require_string_arg(level, "runtime.log", "level", false)?;
5730            let msg = require_string_arg(msg, "runtime.log", "message", true)?;
5731            let normalized_level = level.trim().to_ascii_lowercase();
5732            let rendered = format!("[LuaSkill:{}] {}", level, msg);
5733            if normalized_level.contains("error") || normalized_level.contains("fatal") {
5734                log_error(rendered);
5735            } else if normalized_level.contains("warn") {
5736                log_warn(rendered);
5737            } else {
5738                log_info(rendered);
5739            }
5740            Ok(())
5741        })?;
5742        runtime.set("log", runtime_log_fn)?;
5743
5744        let print_fn = lua.create_function(|_, args: MultiValue| {
5745            let mut parts = Vec::new();
5746            for val in args.into_iter() {
5747                let s = match val {
5748                    LuaValue::String(s) => s.to_str().map(|b| b.to_string()).unwrap_or_default(),
5749                    LuaValue::Integer(i) => i.to_string(),
5750                    LuaValue::Number(f) => f.to_string(),
5751                    LuaValue::Boolean(b) => b.to_string(),
5752                    LuaValue::Nil => "nil".to_string(),
5753                    _ => format!("{:?}", val),
5754                };
5755                parts.push(s);
5756            }
5757            log_info(format!("[LuaSkill:info] {}", parts.join("\t")));
5758            Ok(())
5759        })?;
5760        lua.globals().set("print", print_fn)?;
5761
5762        let fs_list_fn = lua.create_function(|_, dir: LuaValue| {
5763            let dir = require_path_arg(dir, "fs.list", "dir")?;
5764            let mut entries = Vec::new();
5765            for entry in std::fs::read_dir(&dir)
5766                .map_err(|e| mlua::Error::runtime(format!("fs.list: {}", e)))?
5767            {
5768                let entry = entry.map_err(|e| mlua::Error::runtime(format!("fs.list: {}", e)))?;
5769                let file_name = entry.file_name().into_string().map_err(|name| {
5770                    mlua::Error::runtime(format!(
5771                        "fs.list: non-UTF-8 file name under {}: {:?}",
5772                        Path::new(&dir).display(),
5773                        name
5774                    ))
5775                })?;
5776                entries.push(file_name);
5777            }
5778            Ok(entries)
5779        })?;
5780        fs.set("list", fs_list_fn)?;
5781
5782        let fs_read_fn = lua.create_function(|_, path: LuaValue| {
5783            let path = require_path_arg(path, "fs.read", "path")?;
5784            std::fs::read_to_string(&path)
5785                .map_err(|e| mlua::Error::runtime(format!("fs.read: {}", e)))
5786        })?;
5787        fs.set("read", fs_read_fn)?;
5788
5789        let fs_write_fn = lua.create_function(|_, (path, content): (LuaValue, LuaValue)| {
5790            let path = require_path_arg(path, "fs.write", "path")?;
5791            let content = require_string_arg(content, "fs.write", "content", true)?;
5792            std::fs::write(&path, content)
5793                .map_err(|e| mlua::Error::runtime(format!("fs.write: {}", e)))
5794        })?;
5795        fs.set("write", fs_write_fn)?;
5796
5797        let fs_exists_fn = lua.create_function(|_, path: LuaValue| {
5798            let path = require_path_arg(path, "fs.exists", "path")?;
5799            Ok(Path::new(&path).exists())
5800        })?;
5801        fs.set("exists", fs_exists_fn)?;
5802
5803        let fs_is_dir_fn = lua.create_function(|_, path: LuaValue| {
5804            let path = require_path_arg(path, "fs.is_dir", "path")?;
5805            Ok(Path::new(&path).is_dir())
5806        })?;
5807        fs.set("is_dir", fs_is_dir_fn)?;
5808
5809        let path_join_fn = lua.create_function(|lua, parts: MultiValue| {
5810            if parts.is_empty() {
5811                return Err(mlua::Error::runtime(
5812                    "path.join: expected at least one path segment",
5813                ));
5814            }
5815            let mut joined = PathBuf::new();
5816            for (index, val) in parts.into_iter().enumerate() {
5817                let param_name = format!("part[{}]", index + 1);
5818                let part = require_path_arg(val, "path.join", &param_name)?;
5819                joined.push(part);
5820            }
5821            let result = render_host_visible_path(&joined);
5822            lua.create_string(&result)
5823        })?;
5824        path.set("join", path_join_fn)?;
5825
5826        let cwd_fn = lua.create_function(|lua, ()| {
5827            let current_dir = std::env::current_dir()
5828                .map_err(|error| mlua::Error::runtime(format!("runtime.cwd: {}", error)))?;
5829            let current_dir_text = render_host_visible_path(&current_dir);
5830            lua.create_string(&current_dir_text)
5831        })?;
5832        runtime.set("cwd", cwd_fn)?;
5833
5834        match host_options.temp_dir.as_ref() {
5835            Some(path_buf) => runtime.set("temp_dir", render_host_visible_path(path_buf))?,
5836            None => runtime.set("temp_dir", LuaValue::Nil)?,
5837        }
5838
5839        match host_options.resources_dir.as_ref() {
5840            Some(path_buf) => runtime.set("resources_dir", render_host_visible_path(path_buf))?,
5841            None => runtime.set("resources_dir", LuaValue::Nil)?,
5842        }
5843
5844        let exec_fn = lua.create_function(|lua, spec: LuaValue| {
5845            let request = parse_exec_request(spec, "process.exec")?;
5846            let result = execute_exec_request(request);
5847            exec_result_to_lua_table(lua, result)
5848        })?;
5849        process.set("exec", exec_fn)?;
5850
5851        let os_info_fn = lua.create_function(|lua, ()| {
5852            let current_os = match std::env::consts::OS {
5853                "windows" => "windows",
5854                "linux" => "linux",
5855                "macos" => "macos",
5856                _ => std::env::consts::OS,
5857            };
5858            let arch = match std::env::consts::ARCH {
5859                "x86_64" => "x86_64",
5860                "x86" => "i686",
5861                "aarch64" => "aarch64",
5862                "arm" => "armv7l",
5863                _ => std::env::consts::ARCH,
5864            };
5865            let info = lua.create_table()?;
5866            info.set("os", current_os)?;
5867            info.set("arch", arch)?;
5868            Ok(info)
5869        })?;
5870        os.set("info", os_info_fn)?;
5871
5872        let json_encode_fn =
5873            lua.create_function(|lua, val: LuaValue| match lua_value_to_json(&val) {
5874                Ok(value) => lua.create_string(serde_json::to_string(&value).unwrap_or_default()),
5875                Err(error) => Err(mlua::Error::runtime(format!("json.encode: {}", error))),
5876            })?;
5877        json.set("encode", json_encode_fn)?;
5878
5879        let json_decode_fn = lua.create_function(|lua, s: LuaValue| {
5880            let s = require_string_arg(s, "json.decode", "text", false)?;
5881            match serde_json::from_str::<Value>(&s) {
5882                Ok(value) => json_value_to_lua(lua, &value),
5883                Err(error) => Err(mlua::Error::runtime(format!("json.decode: {}", error))),
5884            }
5885        })?;
5886        json.set("decode", json_decode_fn)?;
5887
5888        let cache_put_fn = lua.create_function(|lua, (value, ttl_sec): (LuaValue, LuaValue)| {
5889            let internal = get_vulcan_runtime_internal_table(lua).map_err(mlua::Error::runtime)?;
5890            let tool_name: Option<String> =
5891                internal.get("tool_name").map_err(mlua::Error::runtime)?;
5892            let skill_name: Option<String> =
5893                internal.get("skill_name").map_err(mlua::Error::runtime)?;
5894            let scope = tool_name
5895                .or(skill_name)
5896                .unwrap_or_else(|| "__runtime".to_string());
5897            let ttl_secs = optional_u64_arg(ttl_sec, "cache.put", "ttl_sec")?;
5898            let payload = lua_value_to_json(&value)
5899                .map_err(|error| mlua::Error::runtime(format!("cache.put: {}", error)))?;
5900            Ok(global_tool_cache().create(&scope, payload, ttl_secs))
5901        })?;
5902        cache.set("put", cache_put_fn)?;
5903
5904        let cache_get_fn = lua.create_function(|lua, cache_id: LuaValue| {
5905            let internal = get_vulcan_runtime_internal_table(lua).map_err(mlua::Error::runtime)?;
5906            let tool_name: Option<String> =
5907                internal.get("tool_name").map_err(mlua::Error::runtime)?;
5908            let skill_name: Option<String> =
5909                internal.get("skill_name").map_err(mlua::Error::runtime)?;
5910            let scope = tool_name
5911                .or(skill_name)
5912                .unwrap_or_else(|| "__runtime".to_string());
5913            let cache_id = require_string_arg(cache_id, "cache.get", "cache_id", false)?;
5914            match global_tool_cache().get(&scope, &cache_id) {
5915                Some(value) => json_value_to_lua(lua, &value),
5916                None => Ok(LuaValue::Nil),
5917            }
5918        })?;
5919        cache.set("get", cache_get_fn)?;
5920
5921        let cache_delete_fn = lua.create_function(|lua, cache_id: LuaValue| {
5922            let internal = get_vulcan_runtime_internal_table(lua).map_err(mlua::Error::runtime)?;
5923            let tool_name: Option<String> =
5924                internal.get("tool_name").map_err(mlua::Error::runtime)?;
5925            let skill_name: Option<String> =
5926                internal.get("skill_name").map_err(mlua::Error::runtime)?;
5927            let scope = tool_name
5928                .or(skill_name)
5929                .unwrap_or_else(|| "__runtime".to_string());
5930            let cache_id = require_string_arg(cache_id, "cache.delete", "cache_id", false)?;
5931            Ok(global_tool_cache().delete(&scope, &cache_id))
5932        })?;
5933        cache.set("delete", cache_delete_fn)?;
5934
5935        let config_get_store = skill_config_store.clone();
5936        let config_get_fn = lua.create_function(move |lua, key: LuaValue| {
5937            let key = require_string_arg(key, "config.get", "key", false)?;
5938            let skill_id = current_vulcan_config_skill_id(lua, "vulcan.config.get")?;
5939            match config_get_store
5940                .get_value(&skill_id, &key)
5941                .map_err(mlua::Error::runtime)?
5942            {
5943                Some(value) => Ok(LuaValue::String(
5944                    lua.create_string(&value).map_err(mlua::Error::runtime)?,
5945                )),
5946                None => Ok(LuaValue::Nil),
5947            }
5948        })?;
5949        config.set("get", config_get_fn)?;
5950
5951        let config_has_store = skill_config_store.clone();
5952        let config_has_fn = lua.create_function(move |lua, key: LuaValue| {
5953            let key = require_string_arg(key, "config.has", "key", false)?;
5954            let skill_id = current_vulcan_config_skill_id(lua, "vulcan.config.has")?;
5955            config_has_store
5956                .has_value(&skill_id, &key)
5957                .map_err(mlua::Error::runtime)
5958        })?;
5959        config.set("has", config_has_fn)?;
5960
5961        let config_set_store = skill_config_store.clone();
5962        let config_set_fn =
5963            lua.create_function(move |lua, (key, value): (LuaValue, LuaValue)| {
5964                let key = require_string_arg(key, "config.set", "key", false)?;
5965                let value = require_string_arg(value, "config.set", "value", true)?;
5966                let skill_id = current_vulcan_config_skill_id(lua, "vulcan.config.set")?;
5967                config_set_store
5968                    .set_value(&skill_id, &key, &value)
5969                    .map_err(mlua::Error::runtime)?;
5970                Ok(true)
5971            })?;
5972        config.set("set", config_set_fn)?;
5973
5974        let config_delete_store = skill_config_store.clone();
5975        let config_delete_fn = lua.create_function(move |lua, key: LuaValue| {
5976            let key = require_string_arg(key, "config.delete", "key", false)?;
5977            let skill_id = current_vulcan_config_skill_id(lua, "vulcan.config.delete")?;
5978            config_delete_store
5979                .delete_value(&skill_id, &key)
5980                .map_err(mlua::Error::runtime)
5981        })?;
5982        config.set("delete", config_delete_fn)?;
5983
5984        let config_list_store = skill_config_store.clone();
5985        let config_list_fn = lua.create_function(move |lua, ()| {
5986            let skill_id = current_vulcan_config_skill_id(lua, "vulcan.config.list")?;
5987            let items = config_list_store
5988                .list_skill_values(&skill_id)
5989                .map_err(mlua::Error::runtime)?;
5990            let table = lua.create_table().map_err(mlua::Error::runtime)?;
5991            for (key, value) in items {
5992                table
5993                    .set(
5994                        key,
5995                        LuaValue::String(lua.create_string(&value).map_err(mlua::Error::runtime)?),
5996                    )
5997                    .map_err(mlua::Error::runtime)?;
5998            }
5999            Ok(LuaValue::Table(table))
6000        })?;
6001        config.set("list", config_list_fn)?;
6002
6003        context.set("request", lua.create_table()?)?;
6004        context.set("client_info", LuaValue::Nil)?;
6005        context.set("client_capabilities", lua.create_table()?)?;
6006        context.set("client_budget", lua.create_table()?)?;
6007        context.set("tool_config", lua.create_table()?)?;
6008        context.set("skill_dir", LuaValue::Nil)?;
6009        context.set("entry_dir", LuaValue::Nil)?;
6010        context.set("entry_file", LuaValue::Nil)?;
6011        deps.set("tools_path", LuaValue::Nil)?;
6012        deps.set("lua_path", LuaValue::Nil)?;
6013        deps.set("ffi_path", LuaValue::Nil)?;
6014
6015        let skill_management_enabled = host_options.capabilities.enable_skill_management_bridge;
6016        runtime_skills.set("enabled", skill_management_enabled)?;
6017
6018        let runtime_skills_status_fn = lua.create_function(move |lua, ()| {
6019            let status = lua.create_table()?;
6020            let callback_registered =
6021                try_has_skill_management_callback().map_err(mlua::Error::runtime)?;
6022            status.set("enabled", skill_management_enabled)?;
6023            status.set("callback_registered", callback_registered)?;
6024            status.set("mode", "host_callback")?;
6025            let message = if !skill_management_enabled {
6026                "Skill management bridge is disabled by host policy"
6027            } else if callback_registered {
6028                "Skill management bridge is enabled and ready"
6029            } else {
6030                "Skill management bridge is enabled but no host callback is registered"
6031            };
6032            status.set("message", message)?;
6033            Ok(status)
6034        })?;
6035        runtime_skills.set("status", runtime_skills_status_fn)?;
6036        runtime_skills.set(
6037            "install",
6038            create_runtime_skill_management_bridge_fn(
6039                lua,
6040                skill_management_enabled,
6041                RuntimeSkillManagementAction::Install,
6042                "install",
6043            )?,
6044        )?;
6045        runtime_skills.set(
6046            "update",
6047            create_runtime_skill_management_bridge_fn(
6048                lua,
6049                skill_management_enabled,
6050                RuntimeSkillManagementAction::Update,
6051                "update",
6052            )?,
6053        )?;
6054        runtime_skills.set(
6055            "uninstall",
6056            create_runtime_skill_management_bridge_fn(
6057                lua,
6058                skill_management_enabled,
6059                RuntimeSkillManagementAction::Uninstall,
6060                "uninstall",
6061            )?,
6062        )?;
6063        runtime_skills.set(
6064            "enable",
6065            create_runtime_skill_management_bridge_fn(
6066                lua,
6067                skill_management_enabled,
6068                RuntimeSkillManagementAction::Enable,
6069                "enable",
6070            )?,
6071        )?;
6072        runtime_skills.set(
6073            "disable",
6074            create_runtime_skill_management_bridge_fn(
6075                lua,
6076                skill_management_enabled,
6077                RuntimeSkillManagementAction::Disable,
6078                "disable",
6079            )?,
6080        )?;
6081
6082        let overflow_type = lua.create_table()?;
6083        overflow_type.set("truncate", "truncate")?;
6084        overflow_type.set("page", "page")?;
6085        runtime.set("overflow_type", overflow_type)?;
6086
6087        runtime_internal.set("tool_name", LuaValue::Nil)?;
6088        runtime_internal.set("skill_name", LuaValue::Nil)?;
6089        runtime_internal.set("luaexec_active", false)?;
6090        runtime_internal.set("luaexec_caller_tool_name", LuaValue::Nil)?;
6091        runtime.set("internal", runtime_internal)?;
6092        runtime.set("skills", runtime_skills)?;
6093        runtime.set("lua", runtime_lua)?;
6094
6095        let call_stub = lua.create_function(|_, _: (LuaValue, LuaValue)| {
6096            Err::<(), _>(mlua::Error::runtime("vulcan.call not initialized"))
6097        })?;
6098        vulcan.set("call", call_stub)?;
6099        vulcan.set("runtime", runtime)?;
6100        vulcan.set("fs", fs)?;
6101        vulcan.set("path", path)?;
6102        vulcan.set("process", process)?;
6103        vulcan.set("os", os)?;
6104        vulcan.set("json", json)?;
6105        vulcan.set("cache", cache)?;
6106        vulcan.set("config", config)?;
6107        vulcan.set("context", context)?;
6108        vulcan.set("deps", deps)?;
6109
6110        lua.globals().set("vulcan", vulcan)?;
6111        Ok(())
6112    }
6113}
6114
6115// ============================================================
6116// JSON ↔ Lua Value conversion
6117// ============================================================
6118
6119fn json_to_lua_table(lua: &Lua, json: &Value) -> Result<Table, String> {
6120    json_to_lua_table_inner(lua, json).map_err(|e| e.to_string())
6121}
6122
6123fn json_to_lua_table_inner(lua: &Lua, json: &Value) -> mlua::Result<Table> {
6124    let table = lua.create_table()?;
6125    if let Value::Object(obj) = json {
6126        for (k, v) in obj {
6127            table.set(k.as_str(), json_value_to_lua(lua, v)?)?;
6128        }
6129    } else if let Value::Array(arr) = json {
6130        for (i, v) in arr.iter().enumerate() {
6131            table.set(i + 1, json_value_to_lua(lua, v)?)?;
6132        }
6133    }
6134    Ok(table)
6135}
6136
6137fn json_value_to_lua(lua: &Lua, json: &Value) -> mlua::Result<LuaValue> {
6138    match json {
6139        Value::Null => Ok(LuaValue::Nil),
6140        Value::Bool(b) => Ok(LuaValue::Boolean(*b)),
6141        Value::Number(n) => {
6142            if let Some(i) = n.as_i64() {
6143                Ok(LuaValue::Integer(i))
6144            } else {
6145                Ok(LuaValue::Number(n.as_f64().unwrap_or(0.0)))
6146            }
6147        }
6148        Value::String(s) => Ok(LuaValue::String(lua.create_string(s)?)),
6149        Value::Array(_) | Value::Object(_) => {
6150            Ok(LuaValue::Table(json_to_lua_table_inner(lua, json)?))
6151        }
6152    }
6153}
6154
6155fn lua_value_to_json(val: &LuaValue) -> Result<Value, String> {
6156    match val {
6157        LuaValue::Nil => Ok(Value::Null),
6158        LuaValue::Boolean(b) => Ok(Value::Bool(*b)),
6159        LuaValue::Integer(i) => Ok(Value::Number((*i).into())),
6160        LuaValue::Number(f) => {
6161            if let Some(n) = serde_json::Number::from_f64(*f) {
6162                Ok(Value::Number(n))
6163            } else {
6164                Ok(Value::Null)
6165            }
6166        }
6167        LuaValue::String(s) => Ok(Value::String(
6168            s.to_str().map(|b| b.to_string()).unwrap_or_default(),
6169        )),
6170        LuaValue::Table(t) => {
6171            // Heuristic: if raw_len() > 0, treat as array. Otherwise as object.
6172            if t.raw_len() > 0 {
6173                let arr = lua_table_to_array(t)?;
6174                Ok(Value::Array(arr))
6175            } else {
6176                lua_table_to_object(t)
6177            }
6178        }
6179        LuaValue::Function(_) => Err("Cannot convert Lua function to JSON".to_string()),
6180        LuaValue::Thread(_) => Err("Cannot convert Lua thread to JSON".to_string()),
6181        LuaValue::UserData(_) => Err("Cannot convert Lua userdata to JSON".to_string()),
6182        LuaValue::LightUserData(_) => Err("Cannot convert light userdata to JSON".to_string()),
6183        _ => Err("Unknown Lua value type".to_string()),
6184    }
6185}
6186
6187fn lua_table_to_array(t: &Table) -> Result<Vec<Value>, String> {
6188    let len = t.raw_len();
6189    if len == 0 {
6190        // Could be empty object or empty array, default to array
6191        return Ok(Vec::new());
6192    }
6193    let mut arr = Vec::with_capacity(len);
6194    for i in 1..=len {
6195        let val: LuaValue = t.get(i).map_err(|e| format!("Array index {}: {}", i, e))?;
6196        arr.push(lua_value_to_json(&val)?);
6197    }
6198    Ok(arr)
6199}
6200
6201fn lua_table_to_object(t: &Table) -> Result<Value, String> {
6202    let mut obj = serde_json::Map::new();
6203    for pair in t.pairs::<String, LuaValue>() {
6204        let (k, v) = pair.map_err(|e| format!("Table key: {}", e))?;
6205        obj.insert(k, lua_value_to_json(&v)?);
6206    }
6207    // Empty Lua table has no distinction between array and object.
6208    // If there are no string keys, treat as empty array.
6209    if obj.is_empty() && t.raw_len() == 0 {
6210        return Ok(Value::Array(Vec::new()));
6211    }
6212    Ok(Value::Object(obj))
6213}
6214
6215#[cfg(test)]
6216mod tests {
6217    use super::{
6218        LoadedSkill, LuaEngine, LuaVmPool, LuaVmPoolConfig, LuaVmPoolState, LuaVmRequestScopeGuard,
6219        SkillConfigStore, VulcanInternalExecutionContext, default_runlua_vm_pool_config,
6220        get_vulcan_context_table, get_vulcan_deps_table, get_vulcan_runtime_internal_table,
6221        get_vulcan_table, json_to_lua_table, normalize_host_visible_path_text,
6222        populate_vulcan_dependency_context, populate_vulcan_file_context,
6223        populate_vulcan_internal_execution_context,
6224    };
6225    use crate::host::database::RuntimeDatabaseProviderCallbacks;
6226    use crate::lua_skill::SkillMeta;
6227    use crate::runtime_options::LuaRuntimeRunLuaPoolConfig;
6228    use crate::{LuaEngineOptions, LuaRuntimeHostOptions};
6229    use mlua::{Table, Value as LuaValue};
6230    use serde_json::json;
6231    use std::collections::HashMap;
6232    use std::fs;
6233    use std::path::Path;
6234    use std::path::PathBuf;
6235    use std::sync::{Arc, Condvar, Mutex};
6236
6237    /// Build one minimal loaded skill for collision-index tests.
6238    /// 为冲突编号测试构造一个最小已加载 skill。
6239    fn make_loaded_skill(
6240        directory_name: &str,
6241        skill_id: &str,
6242        local_entry_name: &str,
6243        lua_module: &str,
6244    ) -> LoadedSkill {
6245        let mut meta: SkillMeta = serde_yaml::from_str(&format!("name: {skill_id}\nversion: 0.1.0\nenable: true\ndebug: false\nentries:\n  - name: {local_entry_name}\n    lua_entry: runtime/test.lua\n    lua_module: {lua_module}\n"))
6246            .expect("deserialize minimal skill meta");
6247        meta.bind_directory_skill_id(skill_id.to_string());
6248        LoadedSkill {
6249            meta,
6250            dir: PathBuf::from(format!("D:/tests/{directory_name}")),
6251            root_name: "ROOT".to_string(),
6252            lancedb_binding: None,
6253            sqlite_binding: None,
6254            resolved_entry_names: HashMap::new(),
6255        }
6256    }
6257
6258    /// Verify host-visible path normalization strips the Windows drive-letter verbatim prefix.
6259    /// 验证对宿主可见的路径归一化会去掉 Windows 盘符 verbatim 前缀。
6260    #[cfg(windows)]
6261    #[test]
6262    fn normalize_host_visible_path_text_strips_windows_drive_verbatim_prefix() {
6263        assert_eq!(
6264            normalize_host_visible_path_text(r"\\?\C:\runtime-test-root\skill.lua"),
6265            r"C:\runtime-test-root\skill.lua"
6266        );
6267    }
6268
6269    /// Verify host-visible path normalization strips the Windows UNC verbatim prefix.
6270    /// 验证对宿主可见的路径归一化会去掉 Windows UNC verbatim 前缀。
6271    #[cfg(windows)]
6272    #[test]
6273    fn normalize_host_visible_path_text_strips_windows_unc_verbatim_prefix() {
6274        assert_eq!(
6275            normalize_host_visible_path_text(r"\\?\UNC\server\share\skill.lua"),
6276            r"\\server\share\skill.lua"
6277        );
6278    }
6279
6280    /// Build one minimal engine instance used only for registry tests.
6281    /// 构造仅用于入口注册表测试的最小引擎实例。
6282    fn make_test_engine(skills: HashMap<String, LoadedSkill>) -> LuaEngine {
6283        LuaEngine {
6284            skills,
6285            entry_registry: Default::default(),
6286            pool: Arc::new(LuaVmPool {
6287                config: LuaVmPoolConfig {
6288                    min_size: 1,
6289                    max_size: 1,
6290                    idle_ttl_secs: 60,
6291                },
6292                state: Mutex::new(LuaVmPoolState {
6293                    available: Vec::new(),
6294                    total_count: 0,
6295                }),
6296                condvar: Condvar::new(),
6297            }),
6298            runlua_pool: Arc::new(LuaVmPool::new(default_runlua_vm_pool_config())),
6299            skill_config_store: Arc::new(
6300                SkillConfigStore::new(None).expect("create runtime test skill config store"),
6301            ),
6302            lancedb_host: None,
6303            sqlite_host: None,
6304            database_provider_callbacks: Arc::new(RuntimeDatabaseProviderCallbacks::default()),
6305            host_options: Arc::new(LuaRuntimeHostOptions::default()),
6306        }
6307    }
6308
6309    /// Build one minimal runtime engine that can execute pooled-VM isolation tests.
6310    /// 构造一个可用于池化虚拟机隔离测试的最小运行时引擎。
6311    fn make_runtime_test_engine() -> LuaEngine {
6312        LuaEngine::new(LuaEngineOptions {
6313            host_options: LuaRuntimeHostOptions::default(),
6314            pool_config: LuaVmPoolConfig {
6315                min_size: 1,
6316                max_size: 1,
6317                idle_ttl_secs: 60,
6318            },
6319        })
6320        .expect("create runtime test engine")
6321    }
6322
6323    /// Build one temporary runtime root path for one isolated skill-config test case.
6324    /// 为单个隔离技能配置测试用例构造一条临时运行时根目录路径。
6325    fn make_temp_runtime_root(label: &str) -> PathBuf {
6326        std::env::temp_dir().join(format!(
6327            "vulcan_luaskills_{}_{}_{}",
6328            label,
6329            std::process::id(),
6330            label.len()
6331        ))
6332    }
6333
6334    /// Create one minimal runtime directory layout used by skill-config tests.
6335    /// 创建技能配置测试使用的最小运行时目录结构。
6336    fn create_runtime_test_layout(runtime_root: &Path) {
6337        for relative_path in [
6338            "skills",
6339            "temp",
6340            "resources",
6341            "lua_packages",
6342            "bin/tools",
6343            "libs",
6344        ] {
6345            fs::create_dir_all(runtime_root.join(relative_path))
6346                .expect("create runtime test layout path");
6347        }
6348    }
6349
6350    /// Write one minimal skill fixture that reads one value from `vulcan.config`.
6351    /// 写入一个最小技能夹具,用于从 `vulcan.config` 读取单个值。
6352    fn write_skill_config_test_skill(runtime_root: &Path, skill_id: &str) -> PathBuf {
6353        let skill_dir = runtime_root.join("skills").join(skill_id);
6354        fs::create_dir_all(skill_dir.join("runtime")).expect("create config test runtime dir");
6355        fs::write(
6356            skill_dir.join("skill.yaml"),
6357            format!(
6358                "name: {skill_id}\nversion: 0.1.0\nenable: true\ndebug: false\nentries:\n  - name: ping\n    description: Config ping entry.\n    lua_entry: runtime/ping.lua\n    lua_module: {skill_id}.ping\n"
6359            ),
6360        )
6361        .expect("write config test skill yaml");
6362        fs::write(
6363            skill_dir.join("runtime").join("ping.lua"),
6364            "return function(args)\n  local value = vulcan.config.get(\"api_token\")\n  if value == nil then\n    return \"missing\"\n  end\n  return value\nend\n",
6365        )
6366        .expect("write config test runtime entry");
6367        skill_dir
6368    }
6369
6370    /// Verify the isolated runlua pool uses the documented default sizing when the host does not override it.
6371    /// 验证宿主未覆盖时隔离 runlua 池会使用文档声明的默认容量配置。
6372    #[test]
6373    fn runlua_pool_uses_default_config_when_host_does_not_override() {
6374        let engine = make_runtime_test_engine();
6375        assert_eq!(engine.runlua_pool.config.min_size, 1);
6376        assert_eq!(engine.runlua_pool.config.max_size, 4);
6377        assert_eq!(engine.runlua_pool.config.idle_ttl_secs, 60);
6378    }
6379
6380    /// Verify the host can override the isolated runlua pool sizing with the same shape as the main VM pool.
6381    /// 验证宿主可以使用与主虚拟机池相同的参数形状覆盖隔离 runlua 池容量。
6382    #[test]
6383    fn runlua_pool_honors_host_override_config() {
6384        let mut host_options = LuaRuntimeHostOptions::default();
6385        host_options.runlua_pool_config = Some(LuaRuntimeRunLuaPoolConfig {
6386            min_size: 2,
6387            max_size: 5,
6388            idle_ttl_secs: 90,
6389        });
6390        let engine = LuaEngine::new(LuaEngineOptions {
6391            host_options,
6392            pool_config: LuaVmPoolConfig {
6393                min_size: 1,
6394                max_size: 1,
6395                idle_ttl_secs: 60,
6396            },
6397        })
6398        .expect("create runtime test engine with custom runlua pool");
6399        assert_eq!(engine.runlua_pool.config.min_size, 2);
6400        assert_eq!(engine.runlua_pool.config.max_size, 5);
6401        assert_eq!(engine.runlua_pool.config.idle_ttl_secs, 90);
6402    }
6403
6404    /// Verify the engine host API persists string skill config values into one explicit config file.
6405    /// 验证引擎宿主 API 会把字符串技能配置值持久化到显式配置文件中。
6406    #[test]
6407    fn skill_config_engine_api_persists_values_into_explicit_file() {
6408        let runtime_root = make_temp_runtime_root("skill_config_explicit_path");
6409        if runtime_root.exists() {
6410            let _ = fs::remove_dir_all(&runtime_root);
6411        }
6412        create_runtime_test_layout(&runtime_root);
6413        let config_file = runtime_root.join("custom").join("skill_config.json");
6414
6415        let mut host_options = LuaRuntimeHostOptions::default();
6416        host_options.skill_config_file_path = Some(config_file.clone());
6417        let mut engine = LuaEngine::new(LuaEngineOptions {
6418            host_options,
6419            pool_config: LuaVmPoolConfig {
6420                min_size: 1,
6421                max_size: 1,
6422                idle_ttl_secs: 60,
6423            },
6424        })
6425        .expect("create skill config test engine");
6426
6427        engine
6428            .set_skill_config_value("demo-skill", "api_token", "sk-explicit")
6429            .expect("set explicit skill config");
6430        assert_eq!(
6431            engine
6432                .get_skill_config_value("demo-skill", "api_token")
6433                .expect("read explicit skill config"),
6434            Some("sk-explicit".to_string())
6435        );
6436        let entries = engine
6437            .list_skill_config_entries(Some("demo-skill"))
6438            .expect("list explicit skill config");
6439        assert_eq!(entries.len(), 1);
6440        assert_eq!(entries[0].skill_id, "demo-skill");
6441        assert_eq!(entries[0].key, "api_token");
6442        assert_eq!(entries[0].value, "sk-explicit");
6443        assert!(config_file.exists());
6444
6445        let deleted = engine
6446            .delete_skill_config_value("demo-skill", "api_token")
6447            .expect("delete explicit skill config");
6448        assert!(deleted);
6449        assert_eq!(
6450            engine
6451                .get_skill_config_value("demo-skill", "api_token")
6452                .expect("read deleted explicit skill config"),
6453            None
6454        );
6455
6456        let _ = fs::remove_dir_all(&runtime_root);
6457    }
6458
6459    /// Verify the unified skill config store falls back to `<runtime_root>/config/skill_config.json` after roots load.
6460    /// 验证统一技能配置存储会在加载根目录后回退到 `<runtime_root>/config/skill_config.json`。
6461    #[test]
6462    fn skill_config_store_uses_default_runtime_config_file_after_load() {
6463        let runtime_root = make_temp_runtime_root("skill_config_default_path");
6464        if runtime_root.exists() {
6465            let _ = fs::remove_dir_all(&runtime_root);
6466        }
6467        create_runtime_test_layout(&runtime_root);
6468
6469        let mut engine = LuaEngine::new(LuaEngineOptions {
6470            host_options: LuaRuntimeHostOptions::default(),
6471            pool_config: LuaVmPoolConfig {
6472                min_size: 1,
6473                max_size: 1,
6474                idle_ttl_secs: 60,
6475            },
6476        })
6477        .expect("create default skill config test engine");
6478
6479        engine
6480            .load_from_roots(&[crate::host::options::RuntimeSkillRoot {
6481                name: "ROOT".to_string(),
6482                skills_dir: runtime_root.join("skills"),
6483            }])
6484            .expect("load empty roots for default skill config path");
6485
6486        let expected_path = runtime_root.join("config").join("skill_config.json");
6487        assert_eq!(
6488            engine
6489                .skill_config_store
6490                .file_path()
6491                .expect("resolve default skill config file path"),
6492            expected_path
6493        );
6494
6495        engine
6496            .set_skill_config_value("demo-skill", "endpoint", "https://example.test")
6497            .expect("write default skill config");
6498        assert!(expected_path.exists());
6499
6500        let _ = fs::remove_dir_all(&runtime_root);
6501    }
6502
6503    /// Verify the unified skill config store resolves the default config path even before the skills directory exists.
6504    /// 验证统一技能配置存储会在技能目录尚未创建前解析默认配置路径。
6505    #[test]
6506    fn skill_config_store_initializes_default_path_before_skills_dir_exists() {
6507        let runtime_root = make_temp_runtime_root("skill_config_without_skills_dir");
6508        if runtime_root.exists() {
6509            let _ = fs::remove_dir_all(&runtime_root);
6510        }
6511        fs::create_dir_all(&runtime_root).expect("create runtime root without skills dir");
6512
6513        let missing_skills_dir = runtime_root.join("skills");
6514        let mut engine = LuaEngine::new(LuaEngineOptions {
6515            host_options: LuaRuntimeHostOptions::default(),
6516            pool_config: LuaVmPoolConfig {
6517                min_size: 1,
6518                max_size: 1,
6519                idle_ttl_secs: 60,
6520            },
6521        })
6522        .expect("create config path initialization test engine");
6523
6524        engine
6525            .load_from_roots(&[crate::host::options::RuntimeSkillRoot {
6526                name: "ROOT".to_string(),
6527                skills_dir: missing_skills_dir,
6528            }])
6529            .expect("load roots without an existing skills directory");
6530
6531        let expected_path = runtime_root.join("config").join("skill_config.json");
6532        assert_eq!(
6533            engine
6534                .skill_config_store
6535                .file_path()
6536                .expect("resolve config path without skills directory"),
6537            expected_path
6538        );
6539
6540        engine
6541            .set_skill_config_value("demo-skill", "api_token", "sk-before-install")
6542            .expect("write config before any skills directory exists");
6543        assert!(expected_path.exists());
6544
6545        let _ = fs::remove_dir_all(&runtime_root);
6546    }
6547
6548    /// Verify reloading a different runtime root updates the default unified skill-config path.
6549    /// 验证重新加载另一套运行时根目录时会同步更新默认统一技能配置路径。
6550    #[test]
6551    fn reload_from_roots_updates_default_skill_config_path() {
6552        let first_runtime_root = make_temp_runtime_root("skill_config_reload_first");
6553        let second_runtime_root = make_temp_runtime_root("skill_config_reload_second");
6554        if first_runtime_root.exists() {
6555            let _ = fs::remove_dir_all(&first_runtime_root);
6556        }
6557        if second_runtime_root.exists() {
6558            let _ = fs::remove_dir_all(&second_runtime_root);
6559        }
6560        create_runtime_test_layout(&first_runtime_root);
6561        create_runtime_test_layout(&second_runtime_root);
6562
6563        let mut engine = LuaEngine::new(LuaEngineOptions {
6564            host_options: LuaRuntimeHostOptions::default(),
6565            pool_config: LuaVmPoolConfig {
6566                min_size: 1,
6567                max_size: 1,
6568                idle_ttl_secs: 60,
6569            },
6570        })
6571        .expect("create reload skill config test engine");
6572
6573        engine
6574            .load_from_roots(&[crate::host::options::RuntimeSkillRoot {
6575                name: "ROOT_FIRST".to_string(),
6576                skills_dir: first_runtime_root.join("skills"),
6577            }])
6578            .expect("load first runtime root");
6579        assert_eq!(
6580            engine
6581                .skill_config_store
6582                .file_path()
6583                .expect("resolve first config path"),
6584            first_runtime_root.join("config").join("skill_config.json")
6585        );
6586
6587        engine
6588            .reload_from_roots(&[crate::host::options::RuntimeSkillRoot {
6589                name: "ROOT_SECOND".to_string(),
6590                skills_dir: second_runtime_root.join("skills"),
6591            }])
6592            .expect("reload second runtime root");
6593        assert_eq!(
6594            engine
6595                .skill_config_store
6596                .file_path()
6597                .expect("resolve second config path"),
6598            second_runtime_root.join("config").join("skill_config.json")
6599        );
6600
6601        let _ = fs::remove_dir_all(&first_runtime_root);
6602        let _ = fs::remove_dir_all(&second_runtime_root);
6603    }
6604
6605    /// Verify explicit unified config file paths bypass ambiguous runtime-root inference.
6606    /// 验证显式统一配置文件路径会绕过歧义运行时根目录推导。
6607    #[test]
6608    fn load_from_roots_accepts_explicit_skill_config_path_for_ambiguous_runtime_roots() {
6609        let first_runtime_root = make_temp_runtime_root("skill_config_explicit_ambiguous_first");
6610        let second_runtime_root = make_temp_runtime_root("skill_config_explicit_ambiguous_second");
6611        if first_runtime_root.exists() {
6612            let _ = fs::remove_dir_all(&first_runtime_root);
6613        }
6614        if second_runtime_root.exists() {
6615            let _ = fs::remove_dir_all(&second_runtime_root);
6616        }
6617        fs::create_dir_all(&first_runtime_root)
6618            .expect("create first explicit ambiguous runtime root");
6619        fs::create_dir_all(&second_runtime_root)
6620            .expect("create second explicit ambiguous runtime root");
6621        let explicit_config_file = first_runtime_root.join("custom").join("skill_config.json");
6622
6623        let mut host_options = LuaRuntimeHostOptions::default();
6624        host_options.skill_config_file_path = Some(explicit_config_file.clone());
6625        let mut engine = LuaEngine::new(LuaEngineOptions {
6626            host_options,
6627            pool_config: LuaVmPoolConfig {
6628                min_size: 1,
6629                max_size: 1,
6630                idle_ttl_secs: 60,
6631            },
6632        })
6633        .expect("create explicit ambiguous root test engine");
6634
6635        engine
6636            .load_from_roots(&[
6637                crate::host::options::RuntimeSkillRoot {
6638                    name: "ROOT_A".to_string(),
6639                    skills_dir: first_runtime_root.join("skills"),
6640                },
6641                crate::host::options::RuntimeSkillRoot {
6642                    name: "ROOT_B".to_string(),
6643                    skills_dir: second_runtime_root.join("skills"),
6644                },
6645            ])
6646            .expect("explicit config path should bypass ambiguous runtime roots");
6647
6648        assert_eq!(
6649            engine
6650                .skill_config_store
6651                .file_path()
6652                .expect("resolve explicit config path"),
6653            explicit_config_file
6654        );
6655
6656        let _ = fs::remove_dir_all(&first_runtime_root);
6657        let _ = fs::remove_dir_all(&second_runtime_root);
6658    }
6659
6660    /// Verify divergent runtime roots require one explicit unified skill config file path.
6661    /// 验证运行时根目录分叉时必须显式提供统一技能配置文件路径。
6662    #[test]
6663    fn load_from_roots_rejects_ambiguous_default_skill_config_runtime_root() {
6664        let first_runtime_root = make_temp_runtime_root("skill_config_ambiguous_first");
6665        let second_runtime_root = make_temp_runtime_root("skill_config_ambiguous_second");
6666        if first_runtime_root.exists() {
6667            let _ = fs::remove_dir_all(&first_runtime_root);
6668        }
6669        if second_runtime_root.exists() {
6670            let _ = fs::remove_dir_all(&second_runtime_root);
6671        }
6672        fs::create_dir_all(&first_runtime_root).expect("create first ambiguous runtime root");
6673        fs::create_dir_all(&second_runtime_root).expect("create second ambiguous runtime root");
6674
6675        let mut engine = LuaEngine::new(LuaEngineOptions {
6676            host_options: LuaRuntimeHostOptions::default(),
6677            pool_config: LuaVmPoolConfig {
6678                min_size: 1,
6679                max_size: 1,
6680                idle_ttl_secs: 60,
6681            },
6682        })
6683        .expect("create ambiguous root test engine");
6684
6685        let error = engine
6686            .load_from_roots(&[
6687                crate::host::options::RuntimeSkillRoot {
6688                    name: "ROOT_A".to_string(),
6689                    skills_dir: first_runtime_root.join("skills"),
6690                },
6691                crate::host::options::RuntimeSkillRoot {
6692                    name: "ROOT_B".to_string(),
6693                    skills_dir: second_runtime_root.join("skills"),
6694                },
6695            ])
6696            .expect_err("ambiguous runtime roots should require an explicit config file path");
6697        assert!(
6698            error
6699                .to_string()
6700                .contains("set host_options.skill_config_file_path explicitly"),
6701            "unexpected ambiguous root error: {error}"
6702        );
6703
6704        let _ = fs::remove_dir_all(&first_runtime_root);
6705        let _ = fs::remove_dir_all(&second_runtime_root);
6706    }
6707
6708    /// Verify lexically equivalent runtime roots do not get misclassified as ambiguous.
6709    /// 验证词法等价的运行时根目录不会被误判为歧义根目录。
6710    #[test]
6711    fn canonical_skill_config_runtime_root_normalizes_equivalent_runtime_roots() {
6712        let runtime_root = make_temp_runtime_root("skill_config_equivalent_runtime_root");
6713        if runtime_root.exists() {
6714            let _ = fs::remove_dir_all(&runtime_root);
6715        }
6716        create_runtime_test_layout(&runtime_root);
6717
6718        let engine = LuaEngine::new(LuaEngineOptions {
6719            host_options: LuaRuntimeHostOptions::default(),
6720            pool_config: LuaVmPoolConfig {
6721                min_size: 1,
6722                max_size: 1,
6723                idle_ttl_secs: 60,
6724            },
6725        })
6726        .expect("create equivalent runtime root test engine");
6727
6728        let equivalent_root = runtime_root.join("nested").join("..").join("skills");
6729        let resolved_runtime_root = engine
6730            .canonical_skill_config_runtime_root(&[
6731                crate::host::options::RuntimeSkillRoot {
6732                    name: "ROOT_CANONICAL".to_string(),
6733                    skills_dir: runtime_root.join("skills"),
6734                },
6735                crate::host::options::RuntimeSkillRoot {
6736                    name: "ROOT_EQUIVALENT".to_string(),
6737                    skills_dir: equivalent_root,
6738                },
6739            ])
6740            .expect("equivalent runtime roots should resolve to one canonical root");
6741
6742        assert_eq!(resolved_runtime_root, runtime_root);
6743
6744        let _ = fs::remove_dir_all(&runtime_root);
6745    }
6746
6747    /// Verify one loaded skill can read its own namespaced config through `vulcan.config.get`.
6748    /// 验证单个已加载技能可以通过 `vulcan.config.get` 读取自己的命名空间配置。
6749    #[test]
6750    fn call_skill_reads_own_skill_config_namespace() {
6751        let runtime_root = make_temp_runtime_root("skill_config_call_skill");
6752        if runtime_root.exists() {
6753            let _ = fs::remove_dir_all(&runtime_root);
6754        }
6755        create_runtime_test_layout(&runtime_root);
6756        write_skill_config_test_skill(&runtime_root, "demo-skill");
6757
6758        let mut engine = LuaEngine::new(LuaEngineOptions {
6759            host_options: LuaRuntimeHostOptions::default(),
6760            pool_config: LuaVmPoolConfig {
6761                min_size: 1,
6762                max_size: 1,
6763                idle_ttl_secs: 60,
6764            },
6765        })
6766        .expect("create call_skill config test engine");
6767        engine
6768            .load_from_roots(&[crate::host::options::RuntimeSkillRoot {
6769                name: "ROOT".to_string(),
6770                skills_dir: runtime_root.join("skills"),
6771            }])
6772            .expect("load config test skill");
6773        engine
6774            .set_skill_config_value("demo-skill", "api_token", "sk-from-config")
6775            .expect("seed skill config value");
6776
6777        let result = engine
6778            .call_skill("demo-skill-ping", &json!({}), None)
6779            .expect("call skill with config");
6780        assert_eq!(result.content, "sk-from-config");
6781
6782        let _ = fs::remove_dir_all(&runtime_root);
6783    }
6784
6785    /// Verify `vulcan.config.*` rejects calls that execute without one active skill context.
6786    /// 验证 `vulcan.config.*` 会拒绝在没有活动技能上下文时执行的调用。
6787    #[test]
6788    fn run_lua_config_api_requires_active_skill_context() {
6789        let engine = make_runtime_test_engine();
6790        let error = engine
6791            .run_lua("return vulcan.config.get('api_token')", &json!({}), None)
6792            .expect_err("run_lua config access should require active skill context");
6793        assert!(error.contains("vulcan.config.get requires one active skill context"));
6794    }
6795
6796    /// Assert that one pooled Lua VM has returned to the neutral request baseline.
6797    /// 断言单个池化 Lua 虚拟机已经回到中性的请求基线状态。
6798    fn assert_vm_scope_is_clean(lua: &mlua::Lua) {
6799        let context = get_vulcan_context_table(lua).expect("get vulcan.context");
6800        let request: Table = context.get("request").expect("get request table");
6801        assert_eq!(request.raw_len(), 0);
6802        assert_eq!(request.pairs::<String, LuaValue>().count(), 0);
6803        assert!(matches!(
6804            context
6805                .get::<LuaValue>("client_info")
6806                .expect("get client_info"),
6807            LuaValue::Nil
6808        ));
6809        assert!(matches!(
6810            context
6811                .get::<LuaValue>("client_capabilities")
6812                .expect("get client_capabilities"),
6813            LuaValue::Table(_)
6814        ));
6815        assert!(matches!(
6816            context
6817                .get::<LuaValue>("client_budget")
6818                .expect("get client_budget"),
6819            LuaValue::Table(_)
6820        ));
6821        assert!(matches!(
6822            context
6823                .get::<LuaValue>("tool_config")
6824                .expect("get tool_config"),
6825            LuaValue::Table(_)
6826        ));
6827        assert!(matches!(
6828            context.get::<LuaValue>("skill_dir").expect("get skill_dir"),
6829            LuaValue::Nil
6830        ));
6831        assert!(matches!(
6832            context.get::<LuaValue>("entry_dir").expect("get entry_dir"),
6833            LuaValue::Nil
6834        ));
6835        assert!(matches!(
6836            context
6837                .get::<LuaValue>("entry_file")
6838                .expect("get entry_file"),
6839            LuaValue::Nil
6840        ));
6841
6842        let deps = get_vulcan_deps_table(lua).expect("get vulcan.deps");
6843        assert!(matches!(
6844            deps.get::<LuaValue>("tools_path").expect("get tools_path"),
6845            LuaValue::Nil
6846        ));
6847        assert!(matches!(
6848            deps.get::<LuaValue>("lua_path").expect("get lua_path"),
6849            LuaValue::Nil
6850        ));
6851        assert!(matches!(
6852            deps.get::<LuaValue>("ffi_path").expect("get ffi_path"),
6853            LuaValue::Nil
6854        ));
6855
6856        let internal = get_vulcan_runtime_internal_table(lua).expect("get runtime internal");
6857        assert!(matches!(
6858            internal
6859                .get::<LuaValue>("tool_name")
6860                .expect("get tool_name"),
6861            LuaValue::Nil
6862        ));
6863        assert!(matches!(
6864            internal
6865                .get::<LuaValue>("skill_name")
6866                .expect("get skill_name"),
6867            LuaValue::Nil
6868        ));
6869        assert!(
6870            !internal
6871                .get::<bool>("luaexec_active")
6872                .expect("get luaexec_active")
6873        );
6874        assert!(matches!(
6875            internal
6876                .get::<LuaValue>("luaexec_caller_tool_name")
6877                .expect("get luaexec_caller_tool_name"),
6878            LuaValue::Nil
6879        ));
6880
6881        let vulcan = get_vulcan_table(lua).expect("get vulcan");
6882        let lancedb: Table = vulcan.get("lancedb").expect("get lancedb");
6883        assert!(!lancedb.get::<bool>("enabled").expect("get lancedb enabled"));
6884        let sqlite: Table = vulcan.get("sqlite").expect("get sqlite");
6885        assert!(!sqlite.get::<bool>("enabled").expect("get sqlite enabled"));
6886        assert!(matches!(
6887            lua.globals()
6888                .get::<LuaValue>("__runlua_args")
6889                .expect("get __runlua_args"),
6890            LuaValue::Nil
6891        ));
6892    }
6893
6894    /// Verify that skill manifests must not declare skill_id explicitly.
6895    /// 验证 skill 清单不允许再显式声明 skill_id 字段。
6896    #[test]
6897    fn load_from_roots_rejects_explicit_skill_id_field() {
6898        let temp_root = std::env::temp_dir().join(format!(
6899            "vulcan_luaskills_reject_skill_id_test_{}",
6900            std::process::id()
6901        ));
6902        if temp_root.exists() {
6903            let _ = fs::remove_dir_all(&temp_root);
6904        }
6905        let skill_root = temp_root.join("skills");
6906        let skill_dir = skill_root.join("vulcan-codekit");
6907        fs::create_dir_all(skill_dir.join("runtime")).expect("create runtime dir");
6908        fs::write(
6909            skill_dir.join("skill.yaml"),
6910            "name: vulcan-codekit\nversion: 0.1.0\nskill_id: vulcan-codekit\nentries:\n  - name: ast-tree\n    lua_entry: runtime/test.lua\n    lua_module: vulcan-codekit.ast-tree\n",
6911        )
6912        .expect("write skill yaml");
6913        fs::write(skill_dir.join("runtime").join("test.lua"), "return 'ok'\n")
6914            .expect("write runtime entry");
6915
6916        let mut engine = LuaEngine::new(LuaEngineOptions {
6917            host_options: LuaRuntimeHostOptions::default(),
6918            pool_config: LuaVmPoolConfig {
6919                min_size: 1,
6920                max_size: 1,
6921                idle_ttl_secs: 60,
6922            },
6923        })
6924        .expect("create engine");
6925
6926        let error = engine
6927            .load_from_roots(&[crate::host::options::RuntimeSkillRoot {
6928                name: "ROOT".to_string(),
6929                skills_dir: skill_root,
6930            }])
6931            .expect_err("explicit skill_id should be rejected");
6932        let rendered = error.to_string();
6933        assert!(rendered.contains("must not declare skill_id"));
6934
6935        let _ = fs::remove_dir_all(&temp_root);
6936    }
6937
6938    /// Verify that host-ignored skills are skipped before dependency, database, or entry setup.
6939    /// 验证宿主忽略的 skill 会在依赖、数据库与入口初始化之前被跳过。
6940    #[test]
6941    fn load_from_roots_skips_host_ignored_skill_before_resource_setup() {
6942        let temp_root = std::env::temp_dir().join(format!(
6943            "vulcan_luaskills_ignored_skill_test_{}",
6944            std::process::id()
6945        ));
6946        if temp_root.exists() {
6947            let _ = fs::remove_dir_all(&temp_root);
6948        }
6949        let skill_root = temp_root.join("skills");
6950        let skill_dir = skill_root.join("grpc-memory");
6951        fs::create_dir_all(skill_dir.join("runtime")).expect("create runtime dir");
6952        fs::write(
6953            skill_dir.join("skill.yaml"),
6954            "name: grpc-memory\nversion: 0.1.0\nenable: true\ndebug: false\nsqlite:\n  enable: true\nlancedb:\n  enable: true\nentries:\n  - name: remember\n    lua_entry: runtime/remember.lua\n    lua_module: grpc-memory.remember\n",
6955        )
6956        .expect("write skill yaml");
6957        fs::write(
6958            skill_dir.join("runtime").join("remember.lua"),
6959            "return function(args)\n  return 'unexpected-load'\nend\n",
6960        )
6961        .expect("write runtime entry");
6962
6963        let mut host_options = LuaRuntimeHostOptions::default();
6964        host_options.dependency_dir_name = "dependencies".to_string();
6965        host_options.state_dir_name = "state".to_string();
6966        host_options.database_dir_name = "databases".to_string();
6967        host_options.ignored_skill_ids = vec!["grpc-memory".to_string()];
6968        let mut engine = LuaEngine::new(LuaEngineOptions {
6969            host_options,
6970            pool_config: LuaVmPoolConfig {
6971                min_size: 1,
6972                max_size: 1,
6973                idle_ttl_secs: 60,
6974            },
6975        })
6976        .expect("create engine");
6977
6978        engine
6979            .load_from_roots(&[crate::host::options::RuntimeSkillRoot {
6980                name: "ROOT".to_string(),
6981                skills_dir: skill_root,
6982            }])
6983            .expect("ignored skill should not fail loading");
6984
6985        assert!(engine.skills.is_empty());
6986        assert!(engine.entry_registry.is_empty());
6987        assert!(!temp_root.join("dependencies").exists());
6988        assert!(!temp_root.join("state").exists());
6989        assert!(!temp_root.join("databases").exists());
6990
6991        let _ = fs::remove_dir_all(&temp_root);
6992    }
6993
6994    /// Verify that colliding `skill-entry` names receive deterministic numeric suffixes.
6995    /// 验证发生冲突的 `skill-entry` 名称会收到稳定且可预测的数字后缀。
6996    #[test]
6997    fn rebuild_entry_registry_appends_numeric_suffixes_for_collisions() {
6998        let mut skills = HashMap::new();
6999        skills.insert(
7000            "alpha".to_string(),
7001            make_loaded_skill("alpha", "foo-bar", "baz", "alpha_module"),
7002        );
7003        skills.insert(
7004            "beta".to_string(),
7005            make_loaded_skill("beta", "foo", "bar-baz", "beta_module"),
7006        );
7007        skills.insert(
7008            "gamma".to_string(),
7009            make_loaded_skill("gamma", "foo-bar", "baz", "gamma_module"),
7010        );
7011
7012        let mut engine = make_test_engine(skills);
7013        engine
7014            .rebuild_entry_registry()
7015            .expect("entry registry should rebuild successfully");
7016
7017        assert!(engine.entry_registry.contains_key("foo-bar-baz"));
7018        assert!(engine.entry_registry.contains_key("foo-bar-baz-2"));
7019        assert!(engine.entry_registry.contains_key("foo-bar-baz-3"));
7020
7021        let alpha_skill = engine
7022            .skills
7023            .get("alpha")
7024            .expect("alpha skill should exist");
7025        let beta_skill = engine.skills.get("beta").expect("beta skill should exist");
7026        let gamma_skill = engine
7027            .skills
7028            .get("gamma")
7029            .expect("gamma skill should exist");
7030
7031        assert_eq!(alpha_skill.resolved_tool_name("baz"), Some("foo-bar-baz"));
7032        assert_eq!(
7033            beta_skill.resolved_tool_name("bar-baz"),
7034            Some("foo-bar-baz-2")
7035        );
7036        assert_eq!(gamma_skill.resolved_tool_name("baz"), Some("foo-bar-baz-3"));
7037    }
7038
7039    /// Verify that host-reserved public tool names are treated as occupied during canonical-name generation.
7040    /// 验证宿主保留的公开工具名称会在 canonical 名称生成阶段被视为已占用名称。
7041    #[test]
7042    fn rebuild_entry_registry_skips_host_reserved_names() {
7043        let mut skills = HashMap::new();
7044        skills.insert(
7045            "alpha".to_string(),
7046            make_loaded_skill("alpha", "vulcan", "help-list", "alpha_module"),
7047        );
7048
7049        let mut engine = make_test_engine(skills);
7050        Arc::get_mut(&mut engine.host_options)
7051            .expect("host options should be uniquely owned in test")
7052            .reserved_entry_names = vec!["vulcan-help-list".to_string()];
7053
7054        engine
7055            .rebuild_entry_registry()
7056            .expect("entry registry should rebuild successfully");
7057
7058        assert!(!engine.entry_registry.contains_key("vulcan-help-list"));
7059        assert!(engine.entry_registry.contains_key("vulcan-help-list-2"));
7060
7061        let alpha_skill = engine
7062            .skills
7063            .get("alpha")
7064            .expect("alpha skill should exist");
7065        assert_eq!(
7066            alpha_skill.resolved_tool_name("help-list"),
7067            Some("vulcan-help-list-2")
7068        );
7069    }
7070
7071    /// Verify that the pooled VM scope guard clears request state even when setup exits early.
7072    /// 验证池化虚拟机作用域守卫即使在安装阶段提前退出也会清理请求状态。
7073    #[test]
7074    fn pooled_vm_scope_guard_cleans_state_after_early_exit() {
7075        let engine = make_runtime_test_engine();
7076        let scope_result: Result<(), String> = (|| {
7077            let mut lease = engine.acquire_vm()?;
7078            let _scope_guard =
7079                LuaVmRequestScopeGuard::new(&mut lease, engine.host_options.as_ref())?;
7080            let lua = _scope_guard.lua();
7081            LuaEngine::populate_vulcan_request_context(
7082                lua,
7083                Some(&crate::runtime_options::LuaInvocationContext::new(
7084                    None,
7085                    json!({"budget":"test"}),
7086                    json!({"tool":"config"}),
7087                )),
7088            )?;
7089            populate_vulcan_internal_execution_context(
7090                lua,
7091                &VulcanInternalExecutionContext {
7092                    tool_name: Some("test-tool".to_string()),
7093                    skill_name: Some("test-skill".to_string()),
7094                    luaexec_active: false,
7095                    luaexec_caller_tool_name: None,
7096                },
7097            )?;
7098            let skill_dir = Path::new("D:/runtime-test-root/skills/test-skill");
7099            let entry_file = Path::new("D:/runtime-test-root/skills/test-skill/runtime/test.lua");
7100            populate_vulcan_file_context(lua, Some(skill_dir), Some(entry_file))?;
7101            populate_vulcan_dependency_context(
7102                lua,
7103                engine.host_options.as_ref(),
7104                Some(skill_dir),
7105                Some("test-skill"),
7106            )?;
7107            lua.globals()
7108                .set(
7109                    "__runlua_args",
7110                    json_to_lua_table(lua, &json!({"stale":"value"}))
7111                        .expect("build runlua args table"),
7112                )
7113                .expect("set stale runlua args");
7114            Err("simulated setup failure".to_string())
7115        })();
7116        assert_eq!(
7117            scope_result.expect_err("scope should fail"),
7118            "simulated setup failure"
7119        );
7120
7121        let lease = engine.acquire_vm().expect("reacquire pooled vm");
7122        assert_vm_scope_is_clean(lease.lua());
7123    }
7124
7125    /// Verify that a pooled VM with broken core tables is discarded before it can be reused.
7126    /// 验证当池化虚拟机的核心表被破坏时,该实例会在复用前被直接丢弃。
7127    #[test]
7128    fn pooled_vm_scope_guard_discards_vm_when_entry_reset_fails() {
7129        let engine = make_runtime_test_engine();
7130        {
7131            let lease = engine.acquire_vm().expect("borrow pooled vm");
7132            let vulcan = get_vulcan_table(lease.lua()).expect("get vulcan");
7133            vulcan
7134                .set("context", LuaValue::Nil)
7135                .expect("break vulcan.context");
7136        }
7137
7138        let mut broken_lease = engine.acquire_vm().expect("reacquire broken pooled vm");
7139        let error =
7140            match LuaVmRequestScopeGuard::new(&mut broken_lease, engine.host_options.as_ref()) {
7141                Ok(_) => panic!("broken pooled vm should fail normalization"),
7142                Err(error) => error,
7143            };
7144        assert!(error.contains("vulcan.context"));
7145
7146        let mut fresh_lease = engine.acquire_vm().expect("borrow fresh pooled vm");
7147        let fresh_scope =
7148            LuaVmRequestScopeGuard::new(&mut fresh_lease, engine.host_options.as_ref())
7149                .expect("normalize fresh pooled vm");
7150        assert_vm_scope_is_clean(fresh_scope.lua());
7151    }
7152
7153    /// Verify that cleanup failures retire the current pooled VM instead of returning dirty state.
7154    /// 验证当清理阶段失败时,当前池化虚拟机会被退役,而不是带着脏状态返回池中。
7155    #[test]
7156    fn pooled_vm_scope_guard_discards_vm_when_exit_reset_fails() {
7157        let engine = make_runtime_test_engine();
7158        let mut lease = engine.acquire_vm().expect("borrow pooled vm");
7159        let scope_guard = LuaVmRequestScopeGuard::new(&mut lease, engine.host_options.as_ref())
7160            .expect("normalize pooled vm");
7161        let vulcan = get_vulcan_table(scope_guard.lua()).expect("get vulcan");
7162        vulcan
7163            .set("context", LuaValue::Nil)
7164            .expect("break vulcan.context");
7165        let error = scope_guard
7166            .finish()
7167            .expect_err("cleanup should fail after context corruption");
7168        assert!(error.contains("vulcan.context"));
7169
7170        let mut fresh_lease = engine.acquire_vm().expect("borrow fresh pooled vm");
7171        let fresh_scope =
7172            LuaVmRequestScopeGuard::new(&mut fresh_lease, engine.host_options.as_ref())
7173                .expect("normalize fresh pooled vm");
7174        assert_vm_scope_is_clean(fresh_scope.lua());
7175    }
7176
7177    /// Verify that run_lua clears transient args after one successful execution.
7178    /// 验证 run_lua 在成功执行后会清理临时参数状态。
7179    #[test]
7180    fn run_lua_clears_args_after_success() {
7181        let engine = make_runtime_test_engine();
7182        let result = engine
7183            .run_lua("return args.value", &json!({"value":"hello"}), None)
7184            .expect("run_lua should succeed");
7185        assert_eq!(result, json!("hello"));
7186
7187        let lease = engine.acquire_vm().expect("reacquire pooled vm");
7188        assert_vm_scope_is_clean(lease.lua());
7189    }
7190
7191    /// Verify isolated `vulcan.runtime.lua.exec` calls reuse the dedicated runlua VM pool.
7192    /// 验证隔离 `vulcan.runtime.lua.exec` 调用会复用独立的 runlua 虚拟机池。
7193    #[test]
7194    fn execute_runlua_request_inline_reuses_dedicated_pool() {
7195        let engine = make_runtime_test_engine();
7196        assert_eq!(engine.runlua_pool.total_count(), 0);
7197
7198        let first = engine
7199            .execute_runlua_request_json_inline(r#"{"code":"return 1"}"#)
7200            .expect("first inline runlua should succeed");
7201        assert!(!first.trim().is_empty());
7202        assert_eq!(engine.runlua_pool.total_count(), 1);
7203
7204        let second = engine
7205            .execute_runlua_request_json_inline(r#"{"code":"return 2"}"#)
7206            .expect("second inline runlua should succeed");
7207        assert!(!second.trim().is_empty());
7208        assert_eq!(engine.runlua_pool.total_count(), 1);
7209    }
7210
7211    /// Verify that run_lua clears transient args after one failed execution.
7212    /// 验证 run_lua 在失败执行后同样会清理临时参数状态。
7213    #[test]
7214    fn run_lua_clears_args_after_failure() {
7215        let engine = make_runtime_test_engine();
7216        let error = engine
7217            .run_lua("error('boom')", &json!({"value":"hello"}), None)
7218            .expect_err("run_lua should fail");
7219        assert!(error.contains("Lua run_lua error"));
7220
7221        let lease = engine.acquire_vm().expect("reacquire pooled vm");
7222        assert_vm_scope_is_clean(lease.lua());
7223    }
7224
7225    /// Verify that `vulcan.call` restores the outer execution context even when the nested skill corrupts it.
7226    /// 验证当嵌套技能破坏上下文时,`vulcan.call` 仍会恢复外层执行上下文。
7227    #[test]
7228    fn vulcan_call_restores_outer_context_after_nested_failure() {
7229        let temp_root = std::env::temp_dir().join(format!(
7230            "vulcan_luaskills_nested_call_restore_test_{}",
7231            std::process::id()
7232        ));
7233        if temp_root.exists() {
7234            let _ = fs::remove_dir_all(&temp_root);
7235        }
7236        let skill_root = temp_root.join("skills");
7237        let skill_dir = skill_root.join("test-skill");
7238        fs::create_dir_all(skill_dir.join("runtime")).expect("create runtime dir");
7239        fs::write(
7240            skill_dir.join("skill.yaml"),
7241            "name: test-skill\nversion: 0.1.0\nenable: true\ndebug: false\nentries:\n  - name: outer\n    lua_entry: runtime/outer.lua\n    lua_module: test-skill.outer\n  - name: nested\n    lua_entry: runtime/nested.lua\n    lua_module: test-skill.nested\n",
7242        )
7243        .expect("write skill yaml");
7244        fs::write(
7245            skill_dir.join("runtime").join("outer.lua"),
7246            "return function(args)\n  local ok, err = pcall(vulcan.call, \"test-skill-nested\", {})\n  if ok then\n    return \"nested-call-unexpected-success\"\n  end\n  local tool_name = (vulcan.runtime and vulcan.runtime.internal and vulcan.runtime.internal.tool_name) or \"tool-nil\"\n  local entry_file = (vulcan.context and vulcan.context.entry_file) or \"entry-nil\"\n  local deps_path = (vulcan.deps and vulcan.deps.lua_path) or \"deps-nil\"\n  return tool_name .. \"|\" .. entry_file .. \"|\" .. deps_path\nend\n",
7247        )
7248        .expect("write outer runtime entry");
7249        fs::write(
7250            skill_dir.join("runtime").join("nested.lua"),
7251            "return function(args)\n  vulcan.runtime = nil\n  vulcan.context = nil\n  vulcan.deps = nil\n  error(\"boom\")\nend\n",
7252        )
7253        .expect("write nested runtime entry");
7254
7255        let mut engine = LuaEngine::new(LuaEngineOptions {
7256            host_options: LuaRuntimeHostOptions::default(),
7257            pool_config: LuaVmPoolConfig {
7258                min_size: 1,
7259                max_size: 1,
7260                idle_ttl_secs: 60,
7261            },
7262        })
7263        .expect("create engine");
7264        engine
7265            .load_from_roots(&[crate::host::options::RuntimeSkillRoot {
7266                name: "ROOT".to_string(),
7267                skills_dir: skill_root.clone(),
7268            }])
7269            .expect("load nested-call test skill");
7270
7271        let result = engine
7272            .call_skill("test-skill-outer", &json!({}), None)
7273            .expect("outer skill should succeed after nested failure");
7274        assert!(result.content.starts_with("test-skill-outer|"));
7275        assert!(result.content.contains("outer.lua"));
7276        assert!(!result.content.contains("|entry-nil|"));
7277        assert!(!result.content.ends_with("|deps-nil"));
7278        assert!(result.content.contains("test-skill"));
7279
7280        let _ = fs::remove_dir_all(&temp_root);
7281    }
7282}