use mlua::{Function, HookTriggers, Lua, MultiValue, Table, Value as LuaValue, VmState};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fs;
use std::io::{Read, Write};
use std::path::{Component, Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Condvar, Mutex, OnceLock, TryLockError};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::dependency::manager::{DependencyManager, DependencyManagerConfig, ensure_directory};
use crate::entry_descriptor::{RuntimeEntryDescriptor, RuntimeEntryParameterDescriptor};
use crate::host::callbacks::{
RuntimeEntryRegistryDelta, RuntimeHostToolAction, RuntimeHostToolRequest, RuntimeModelCaller,
RuntimeModelEmbedRequest, RuntimeModelEmbedResponse, RuntimeModelError, RuntimeModelErrorCode,
RuntimeModelLlmRequest, RuntimeModelLlmResponse, RuntimeModelUsage, RuntimeSkillLifecycleEvent,
RuntimeSkillManagementAction, RuntimeSkillManagementRequest, dispatch_host_tool_request,
dispatch_model_embed_request, dispatch_model_llm_request, dispatch_skill_management_request,
try_has_host_tool_callback, try_has_model_embed_callback, try_has_model_llm_callback,
try_has_skill_management_callback,
};
use crate::host::database::RuntimeDatabaseProviderCallbacks;
use crate::lancedb_host::{LanceDbSkillBinding, LanceDbSkillHost, disabled_skill_status_json};
use crate::lua_skill::{SkillMeta, validate_luaskills_identifier, validate_luaskills_version};
use crate::runtime::config::{SkillConfigEntry, SkillConfigStore};
use crate::runtime::encoding::{
RuntimeTextEncoding, decode_runtime_text, default_runtime_text_encoding, encode_runtime_text,
};
use crate::runtime::managed_io::{create_vulcan_io_table, install_managed_io_compat};
use crate::runtime::process_session::create_process_session_table;
use crate::runtime_context::{RuntimeClientInfo, RuntimeRequestContext};
use crate::runtime_help::{
RuntimeHelpDetail, RuntimeHelpNodeDescriptor, RuntimeSkillHelpDescriptor,
};
use crate::runtime_logging::{error as log_error, info as log_info, warn as log_warn};
use crate::runtime_options::{LuaInvocationContext, LuaRuntimeHostOptions, RuntimeSkillRoot};
use crate::runtime_result::RuntimeInvocationResult;
use crate::skill::dependencies::SkillDependencyManifest;
use crate::skill::manager::{
PreparedSkillApply, ResolvedSkillInstance, SkillApplyResult, SkillInstallRequest,
SkillManagementAuthority, SkillManager, SkillManagerConfig, SkillOperationPlane,
SkillUninstallOptions, SkillUninstallResult, collect_effective_skill_instances_from_roots,
resolve_declared_skill_instance_from_roots, resolve_effective_skill_instance_from_roots,
resolve_requested_skill_id,
};
use crate::sqlite_host::{
SqliteSkillBinding, SqliteSkillHost,
disabled_skill_status_json as disabled_sqlite_skill_status_json,
};
use crate::tool_cache::{ToolCacheConfig, configure_global_tool_cache, global_tool_cache};
mod bridge;
mod host_result;
mod lease;
mod runlua;
use self::bridge::{
create_host_tool_call_fn, create_host_tool_has_fn, create_host_tool_list_fn,
create_model_embed_fn, create_model_has_fn, create_model_llm_fn, create_model_status_fn,
create_runtime_skill_layers_fn, create_runtime_skill_management_bridge_fn,
};
use self::host_result::{
host_result_capability_to_json_value, parse_tool_call_output, resolve_host_result_capability,
};
use self::lease::RuntimeSessionManager;
use self::runlua::{
exec_result_to_lua_table, execute_exec_request, optional_u64_arg, parse_exec_request,
require_path_arg, require_string_arg, require_table_arg, resolve_host_default_text_encoding,
};
#[derive(Clone)]
struct LoadedSkill {
meta: SkillMeta,
dir: std::path::PathBuf,
root_name: String,
lancedb_binding: Option<Arc<LanceDbSkillBinding>>,
sqlite_binding: Option<Arc<SqliteSkillBinding>>,
resolved_entry_names: HashMap<String, String>,
}
fn normalize_host_visible_path_text(rendered: &str) -> String {
#[cfg(windows)]
{
if let Some(stripped) = rendered.strip_prefix(r"\\?\UNC\") {
return format!(r"\\{}", stripped);
}
if let Some(stripped) = rendered.strip_prefix(r"\\?\") {
return stripped.to_string();
}
}
rendered.to_string()
}
fn render_host_visible_path(path: &Path) -> String {
normalize_host_visible_path_text(&path.to_string_lossy())
}
fn render_log_friendly_path(path: &Path) -> String {
render_host_visible_path(path)
}
fn normalize_runtime_root_path(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
let mut can_pop_normal = false;
for component in path.components() {
match component {
Component::Prefix(prefix) => {
normalized.push(prefix.as_os_str());
can_pop_normal = false;
}
Component::RootDir => {
normalized.push(component.as_os_str());
can_pop_normal = false;
}
Component::CurDir => {}
Component::ParentDir => {
if can_pop_normal && normalized.pop() {
can_pop_normal = !matches!(
normalized.components().next_back(),
Some(Component::Prefix(_)) | Some(Component::RootDir) | None
);
} else if !path.is_absolute() {
normalized.push(component.as_os_str());
can_pop_normal = false;
}
}
Component::Normal(part) => {
normalized.push(part);
can_pop_normal = true;
}
}
}
normalized
}
#[derive(Debug, Deserialize)]
struct RuntimePackagesManifestPaths {
install_manifest: String,
compat_lua_packages_txt: String,
platform_support: String,
third_party_licenses: String,
third_party_notices: String,
help_index: String,
package_help_root: String,
module_help_root: String,
license_index: String,
}
#[derive(Debug, Deserialize)]
struct RuntimePackagesManifest {
schema_version: u32,
layout: String,
paths: RuntimePackagesManifestPaths,
}
fn validate_runtime_relative_manifest_path(label: &str, relative_path: &str) -> Result<(), String> {
let candidate = Path::new(relative_path);
if candidate.is_absolute() {
return Err(format!(
"packaged runtime is invalid: {} must be runtime-relative, got '{}'",
label, relative_path
));
}
if candidate.components().next().is_none() {
return Err(format!("packaged runtime is invalid: {} is empty", label));
}
Ok(())
}
fn validate_packaged_runtime_target(
runtime_root: &Path,
label: &str,
relative_path: &str,
) -> Result<(), String> {
validate_runtime_relative_manifest_path(label, relative_path)?;
let candidate = runtime_root.join(relative_path);
if !candidate.exists() {
return Err(format!(
"packaged runtime is invalid: missing {}",
render_log_friendly_path(&candidate)
));
}
Ok(())
}
fn validate_packaged_runtime_packages_layout(resources_dir: &Path) -> Result<(), String> {
let runtime_manifest_path = resources_dir.join("lua-runtime-manifest.json");
if !runtime_manifest_path.exists() {
return Ok(());
}
let runtime_root = resources_dir.parent().ok_or_else(|| {
format!(
"packaged runtime is invalid: resources directory has no parent: {}",
render_log_friendly_path(resources_dir)
)
})?;
let packages_manifest_path = resources_dir.join("luaskills-packages-manifest.json");
if !packages_manifest_path.exists() {
return Err(format!(
"packaged runtime is incomplete: missing {}",
render_log_friendly_path(&packages_manifest_path)
));
}
let manifest_text = fs::read_to_string(&packages_manifest_path).map_err(|error| {
format!(
"packaged runtime is invalid: failed to read {}: {}",
render_log_friendly_path(&packages_manifest_path),
error
)
})?;
let manifest: RuntimePackagesManifest =
serde_json::from_str(&manifest_text).map_err(|error| {
format!(
"packaged runtime is invalid: failed to parse {}: {}",
render_log_friendly_path(&packages_manifest_path),
error
)
})?;
if manifest.schema_version != 1 {
return Err(format!(
"packaged runtime is invalid: unsupported luaskills-packages manifest schema_version {}",
manifest.schema_version
));
}
if manifest.layout != "luaskills-packages-runtime-v1" {
return Err(format!(
"packaged runtime is invalid: unsupported luaskills-packages layout '{}'",
manifest.layout
));
}
validate_packaged_runtime_target(
runtime_root,
"install_manifest",
&manifest.paths.install_manifest,
)?;
validate_packaged_runtime_target(
runtime_root,
"compat_lua_packages_txt",
&manifest.paths.compat_lua_packages_txt,
)?;
validate_packaged_runtime_target(
runtime_root,
"platform_support",
&manifest.paths.platform_support,
)?;
validate_packaged_runtime_target(
runtime_root,
"third_party_licenses",
&manifest.paths.third_party_licenses,
)?;
validate_packaged_runtime_target(
runtime_root,
"third_party_notices",
&manifest.paths.third_party_notices,
)?;
validate_packaged_runtime_target(runtime_root, "help_index", &manifest.paths.help_index)?;
validate_packaged_runtime_target(
runtime_root,
"package_help_root",
&manifest.paths.package_help_root,
)?;
validate_packaged_runtime_target(
runtime_root,
"module_help_root",
&manifest.paths.module_help_root,
)?;
validate_packaged_runtime_target(runtime_root, "license_index", &manifest.paths.license_index)?;
Ok(())
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct LuaVmPoolConfig {
pub min_size: usize,
pub max_size: usize,
pub idle_ttl_secs: u64,
}
impl LuaVmPoolConfig {
fn normalized(self) -> Self {
let min_size = self.min_size.max(1);
let max_size = self.max_size.max(min_size);
let idle_ttl_secs = self.idle_ttl_secs.max(1);
Self {
min_size,
max_size,
idle_ttl_secs,
}
}
}
fn default_runlua_vm_pool_config() -> LuaVmPoolConfig {
LuaVmPoolConfig {
min_size: 1,
max_size: 4,
idle_ttl_secs: 60,
}
}
struct LuaVm {
lua: Lua,
last_used_at: Instant,
}
struct LuaVmPoolState {
available: Vec<LuaVm>,
total_count: usize,
}
struct LuaVmPool {
config: LuaVmPoolConfig,
state: Mutex<LuaVmPoolState>,
condvar: Condvar,
}
pub struct LuaEngine {
skills: HashMap<String, LoadedSkill>,
entry_registry: BTreeMap<String, ResolvedEntryTarget>,
runtime_skill_roots: Vec<RuntimeSkillRoot>,
pool: Arc<LuaVmPool>,
runlua_pool: Arc<LuaVmPool>,
runtime_sessions: Arc<RuntimeSessionManager>,
skill_config_store: Arc<SkillConfigStore>,
lancedb_host: Option<Arc<LanceDbSkillHost>>,
sqlite_host: Option<Arc<SqliteSkillHost>>,
database_provider_callbacks: Arc<RuntimeDatabaseProviderCallbacks>,
host_options: Arc<LuaRuntimeHostOptions>,
}
#[derive(Debug, Clone)]
struct ResolvedEntryTarget {
canonical_name: String,
skill_storage_key: String,
skill_id: String,
local_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LuaEngineOptions {
pub pool_config: LuaVmPoolConfig,
pub host_options: LuaRuntimeHostOptions,
}
impl LuaEngineOptions {
pub fn new(pool_config: LuaVmPoolConfig, host_options: LuaRuntimeHostOptions) -> Self {
Self {
pool_config,
host_options,
}
}
}
impl LoadedSkill {
fn resolved_tool_name(&self, local_name: &str) -> Option<&str> {
self.resolved_entry_names
.get(local_name)
.map(String::as_str)
}
}
fn lua_value_type_name(value: &LuaValue) -> &'static str {
match value {
LuaValue::Nil => "nil",
LuaValue::Boolean(_) => "boolean",
LuaValue::LightUserData(_) => "lightuserdata",
LuaValue::Integer(_) => "integer",
LuaValue::Number(_) => "number",
LuaValue::String(_) => "string",
LuaValue::Table(_) => "table",
LuaValue::Function(_) => "function",
LuaValue::Thread(_) => "thread",
LuaValue::UserData(_) => "userdata",
LuaValue::Error(_) => "error",
LuaValue::Other(_) => "other",
}
}
fn validate_skill_relative_path(
relative_path: &str,
expected_prefix: &str,
field_label: &str,
) -> Result<(), String> {
let trimmed = relative_path.trim();
if trimmed.is_empty() {
return Err(format!("{field_label} must not be empty"));
}
let path = Path::new(trimmed);
if path.is_absolute() {
return Err(format!(
"{field_label} must be a relative path under {expected_prefix}"
));
}
let normalized = trimmed.replace('\\', "/");
let required_prefix = format!("{expected_prefix}/");
if !normalized.starts_with(&required_prefix) {
return Err(format!("{field_label} must start with {required_prefix}"));
}
for component in path.components() {
if !matches!(component, std::path::Component::Normal(_)) {
return Err(format!("{field_label} must not contain parent"));
}
}
Ok(())
}
fn tool_entry_path(skill_dir: &Path, tool: &crate::lua_skill::SkillToolMeta) -> PathBuf {
skill_dir.join(&tool.lua_entry)
}
#[derive(Debug, Clone, Default)]
struct VulcanInternalExecutionContext {
tool_name: Option<String>,
skill_name: Option<String>,
entry_name: Option<String>,
root_name: Option<String>,
luaexec_active: bool,
luaexec_caller_tool_name: Option<String>,
}
fn capture_vulcan_file_context(
lua: &Lua,
) -> Result<(Option<String>, Option<String>, Option<String>), String> {
let context = get_vulcan_context_table(lua)?;
let skill_dir: Option<String> = context
.get("skill_dir")
.map_err(|error| format!("Failed to read vulcan.context.skill_dir: {}", error))?;
let entry_dir: Option<String> = context
.get("entry_dir")
.map_err(|error| format!("Failed to read vulcan.context.entry_dir: {}", error))?;
let entry_file: Option<String> = context
.get("entry_file")
.map_err(|error| format!("Failed to read vulcan.context.entry_file: {}", error))?;
Ok((skill_dir, entry_dir, entry_file))
}
fn populate_vulcan_file_context(
lua: &Lua,
skill_dir: Option<&Path>,
entry_file: Option<&Path>,
) -> Result<(), String> {
let context = get_vulcan_context_table(lua)?;
match skill_dir {
Some(path) => context
.set("skill_dir", render_host_visible_path(path))
.map_err(|error| format!("Failed to set vulcan.context.skill_dir: {}", error))?,
None => context
.set("skill_dir", LuaValue::Nil)
.map_err(|error| format!("Failed to clear vulcan.context.skill_dir: {}", error))?,
}
match entry_file {
Some(path) => {
let entry_dir = path.parent().unwrap_or(path);
context
.set("entry_dir", render_host_visible_path(entry_dir))
.map_err(|error| format!("Failed to set vulcan.context.entry_dir: {}", error))?;
context
.set("entry_file", render_host_visible_path(path))
.map_err(|error| format!("Failed to set vulcan.context.entry_file: {}", error))?;
}
None => {
context
.set("entry_dir", LuaValue::Nil)
.map_err(|error| format!("Failed to clear vulcan.context.entry_dir: {}", error))?;
context
.set("entry_file", LuaValue::Nil)
.map_err(|error| format!("Failed to clear vulcan.context.entry_file: {}", error))?;
}
}
Ok(())
}
fn populate_vulcan_dependency_context(
lua: &Lua,
host_options: &LuaRuntimeHostOptions,
skill_dir: Option<&Path>,
skill_id: Option<&str>,
) -> Result<(), String> {
let deps = get_vulcan_deps_table(lua)?;
let clear_paths = || -> Result<(), String> {
deps.set("tools_path", LuaValue::Nil)
.map_err(|error| format!("Failed to clear vulcan.deps.tools_path: {}", error))?;
deps.set("lua_path", LuaValue::Nil)
.map_err(|error| format!("Failed to clear vulcan.deps.lua_path: {}", error))?;
deps.set("ffi_path", LuaValue::Nil)
.map_err(|error| format!("Failed to clear vulcan.deps.ffi_path: {}", error))?;
Ok(())
};
let Some(skill_dir) = skill_dir else {
return clear_paths();
};
let Some(skill_id) = skill_id.filter(|value| !value.trim().is_empty()) else {
return clear_paths();
};
let skills_root = skill_dir.parent().ok_or_else(|| {
format!(
"Failed to derive skills root from skill directory {}",
skill_dir.display()
)
})?;
let runtime_root = skills_root.parent().ok_or_else(|| {
format!(
"Failed to derive runtime root from skill directory {}",
skill_dir.display()
)
})?;
let dependency_root = runtime_root.join(host_options.dependency_dir_name.as_str());
deps.set(
"tools_path",
render_host_visible_path(&dependency_root.join("tools").join(skill_id)),
)
.map_err(|error| format!("Failed to set vulcan.deps.tools_path: {}", error))?;
deps.set(
"lua_path",
render_host_visible_path(&dependency_root.join("lua").join(skill_id)),
)
.map_err(|error| format!("Failed to set vulcan.deps.lua_path: {}", error))?;
deps.set(
"ffi_path",
render_host_visible_path(&dependency_root.join("ffi").join(skill_id)),
)
.map_err(|error| format!("Failed to set vulcan.deps.ffi_path: {}", error))?;
Ok(())
}
fn capture_vulcan_internal_execution_context(
lua: &Lua,
) -> Result<VulcanInternalExecutionContext, String> {
let internal = get_vulcan_runtime_internal_table(lua)?;
let tool_name: Option<String> = internal.get("tool_name").map_err(|error| {
format!(
"Failed to read vulcan.runtime.internal.tool_name: {}",
error
)
})?;
let skill_name: Option<String> = internal.get("skill_name").map_err(|error| {
format!(
"Failed to read vulcan.runtime.internal.skill_name: {}",
error
)
})?;
let entry_name: Option<String> = internal.get("entry_name").map_err(|error| {
format!(
"Failed to read vulcan.runtime.internal.entry_name: {}",
error
)
})?;
let root_name: Option<String> = internal.get("root_name").map_err(|error| {
format!(
"Failed to read vulcan.runtime.internal.root_name: {}",
error
)
})?;
let luaexec_active: bool = internal.get("luaexec_active").map_err(|error| {
format!(
"Failed to read vulcan.runtime.internal.luaexec_active: {}",
error
)
})?;
let luaexec_caller_tool_name: Option<String> =
internal.get("luaexec_caller_tool_name").map_err(|error| {
format!(
"Failed to read vulcan.runtime.internal.luaexec_caller_tool_name: {}",
error
)
})?;
Ok(VulcanInternalExecutionContext {
tool_name,
skill_name,
entry_name,
root_name,
luaexec_active,
luaexec_caller_tool_name,
})
}
fn populate_vulcan_internal_execution_context(
lua: &Lua,
context: &VulcanInternalExecutionContext,
) -> Result<(), String> {
let internal = get_vulcan_runtime_internal_table(lua)?;
match context.tool_name.as_deref() {
Some(tool_name) => internal.set("tool_name", tool_name).map_err(|error| {
format!("Failed to set vulcan.runtime.internal.tool_name: {}", error)
})?,
None => internal.set("tool_name", LuaValue::Nil).map_err(|error| {
format!(
"Failed to clear vulcan.runtime.internal.tool_name: {}",
error
)
})?,
}
match context.skill_name.as_deref() {
Some(skill_name) => internal.set("skill_name", skill_name).map_err(|error| {
format!(
"Failed to set vulcan.runtime.internal.skill_name: {}",
error
)
})?,
None => internal.set("skill_name", LuaValue::Nil).map_err(|error| {
format!(
"Failed to clear vulcan.runtime.internal.skill_name: {}",
error
)
})?,
}
match context.entry_name.as_deref() {
Some(entry_name) => internal.set("entry_name", entry_name).map_err(|error| {
format!(
"Failed to set vulcan.runtime.internal.entry_name: {}",
error
)
})?,
None => internal.set("entry_name", LuaValue::Nil).map_err(|error| {
format!(
"Failed to clear vulcan.runtime.internal.entry_name: {}",
error
)
})?,
}
match context.root_name.as_deref() {
Some(root_name) => internal.set("root_name", root_name).map_err(|error| {
format!("Failed to set vulcan.runtime.internal.root_name: {}", error)
})?,
None => internal.set("root_name", LuaValue::Nil).map_err(|error| {
format!(
"Failed to clear vulcan.runtime.internal.root_name: {}",
error
)
})?,
}
internal
.set("luaexec_active", context.luaexec_active)
.map_err(|error| {
format!(
"Failed to set vulcan.runtime.internal.luaexec_active: {}",
error
)
})?;
match context.luaexec_caller_tool_name.as_deref() {
Some(tool_name) => internal
.set("luaexec_caller_tool_name", tool_name)
.map_err(|error| {
format!(
"Failed to set vulcan.runtime.internal.luaexec_caller_tool_name: {}",
error
)
})?,
None => internal
.set("luaexec_caller_tool_name", LuaValue::Nil)
.map_err(|error| {
format!(
"Failed to clear vulcan.runtime.internal.luaexec_caller_tool_name: {}",
error
)
})?,
}
Ok(())
}
fn current_vulcan_config_skill_id(lua: &Lua, api_name: &str) -> Result<String, mlua::Error> {
let internal = get_vulcan_runtime_internal_table(lua)
.map_err(|error| mlua::Error::runtime(format!("{}: {}", api_name, error)))?;
let skill_name: Option<String> = internal
.get("skill_name")
.map_err(|error| mlua::Error::runtime(format!("{}: {}", api_name, error)))?;
skill_name
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| {
mlua::Error::runtime(format!("{} requires one active skill context", api_name))
})
}
fn is_lua_help_file(relative_path: &str) -> bool {
Path::new(relative_path)
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.eq_ignore_ascii_case("lua"))
.unwrap_or(false)
}
fn read_skill_text_file(
skill_dir: &Path,
relative_path: &str,
label: &str,
) -> Result<String, String> {
let file_path = skill_dir.join(relative_path);
std::fs::read_to_string(&file_path).map_err(|error| {
format!(
"Failed to read {label} file {}: {}",
file_path.display(),
error
)
})
}
fn get_vulcan_table(lua: &Lua) -> Result<Table, String> {
lua.globals()
.get("vulcan")
.map_err(|error| format!("Failed to get vulcan module: {}", error))
}
fn get_vulcan_context_table(lua: &Lua) -> Result<Table, String> {
let vulcan = get_vulcan_table(lua)?;
vulcan
.get("context")
.map_err(|error| format!("Failed to get vulcan.context: {}", error))
}
fn get_vulcan_deps_table(lua: &Lua) -> Result<Table, String> {
let vulcan = get_vulcan_table(lua)?;
vulcan
.get("deps")
.map_err(|error| format!("Failed to get vulcan.deps: {}", error))
}
fn get_vulcan_runtime_table(lua: &Lua) -> Result<Table, String> {
let vulcan = get_vulcan_table(lua)?;
vulcan
.get("runtime")
.map_err(|error| format!("Failed to get vulcan.runtime: {}", error))
}
fn get_vulcan_runtime_internal_table(lua: &Lua) -> Result<Table, String> {
let runtime = get_vulcan_runtime_table(lua)?;
runtime
.get("internal")
.map_err(|error| format!("Failed to get vulcan.runtime.internal: {}", error))
}
fn get_vulcan_runtime_lua_table(lua: &Lua) -> Result<Table, String> {
let runtime = get_vulcan_runtime_table(lua)?;
runtime
.get("lua")
.map_err(|error| format!("Failed to get vulcan.runtime.lua: {}", error))
}
#[derive(Clone)]
struct VulcanCoreModuleState {
vulcan: Table,
call: Function,
runtime: Table,
runtime_skills: Table,
runtime_internal: Table,
runtime_lua: Table,
fs: Table,
io: Table,
path: Table,
process: Table,
os: Table,
json: Table,
cache: Table,
context: Table,
deps: Table,
models: Table,
}
impl VulcanCoreModuleState {
fn capture(lua: &Lua) -> Result<Self, String> {
let vulcan = get_vulcan_table(lua)?;
let runtime = get_vulcan_runtime_table(lua)?;
Ok(Self {
call: vulcan
.get("call")
.map_err(|error| format!("Failed to get vulcan.call: {}", error))?,
runtime_skills: runtime
.get("skills")
.map_err(|error| format!("Failed to get vulcan.runtime.skills: {}", error))?,
runtime_internal: runtime
.get("internal")
.map_err(|error| format!("Failed to get vulcan.runtime.internal: {}", error))?,
runtime_lua: runtime
.get("lua")
.map_err(|error| format!("Failed to get vulcan.runtime.lua: {}", error))?,
fs: vulcan
.get("fs")
.map_err(|error| format!("Failed to get vulcan.fs: {}", error))?,
io: vulcan
.get("io")
.map_err(|error| format!("Failed to get vulcan.io: {}", error))?,
path: vulcan
.get("path")
.map_err(|error| format!("Failed to get vulcan.path: {}", error))?,
process: vulcan
.get("process")
.map_err(|error| format!("Failed to get vulcan.process: {}", error))?,
os: vulcan
.get("os")
.map_err(|error| format!("Failed to get vulcan.os: {}", error))?,
json: vulcan
.get("json")
.map_err(|error| format!("Failed to get vulcan.json: {}", error))?,
cache: vulcan
.get("cache")
.map_err(|error| format!("Failed to get vulcan.cache: {}", error))?,
models: vulcan
.get("models")
.map_err(|error| format!("Failed to get vulcan.models: {}", error))?,
context: vulcan
.get("context")
.map_err(|error| format!("Failed to get vulcan.context: {}", error))?,
deps: vulcan
.get("deps")
.map_err(|error| format!("Failed to get vulcan.deps: {}", error))?,
vulcan,
runtime,
})
}
fn restore(&self, lua: &Lua) -> Result<(), String> {
self.runtime
.set("skills", self.runtime_skills.clone())
.map_err(|error| format!("Failed to restore vulcan.runtime.skills: {}", error))?;
self.runtime
.set("internal", self.runtime_internal.clone())
.map_err(|error| format!("Failed to restore vulcan.runtime.internal: {}", error))?;
self.runtime
.set("lua", self.runtime_lua.clone())
.map_err(|error| format!("Failed to restore vulcan.runtime.lua: {}", error))?;
self.vulcan
.set("call", self.call.clone())
.map_err(|error| format!("Failed to restore vulcan.call: {}", error))?;
self.vulcan
.set("runtime", self.runtime.clone())
.map_err(|error| format!("Failed to restore vulcan.runtime: {}", error))?;
self.vulcan
.set("fs", self.fs.clone())
.map_err(|error| format!("Failed to restore vulcan.fs: {}", error))?;
self.vulcan
.set("io", self.io.clone())
.map_err(|error| format!("Failed to restore vulcan.io: {}", error))?;
self.vulcan
.set("path", self.path.clone())
.map_err(|error| format!("Failed to restore vulcan.path: {}", error))?;
self.vulcan
.set("process", self.process.clone())
.map_err(|error| format!("Failed to restore vulcan.process: {}", error))?;
self.vulcan
.set("os", self.os.clone())
.map_err(|error| format!("Failed to restore vulcan.os: {}", error))?;
self.vulcan
.set("json", self.json.clone())
.map_err(|error| format!("Failed to restore vulcan.json: {}", error))?;
self.vulcan
.set("cache", self.cache.clone())
.map_err(|error| format!("Failed to restore vulcan.cache: {}", error))?;
self.vulcan
.set("models", self.models.clone())
.map_err(|error| format!("Failed to restore vulcan.models: {}", error))?;
self.vulcan
.set("context", self.context.clone())
.map_err(|error| format!("Failed to restore vulcan.context: {}", error))?;
self.vulcan
.set("deps", self.deps.clone())
.map_err(|error| format!("Failed to restore vulcan.deps: {}", error))?;
lua.globals()
.set("vulcan", self.vulcan.clone())
.map_err(|error| format!("Failed to restore global vulcan module: {}", error))?;
Ok(())
}
}
fn non_empty_skill_name(value: &str) -> Option<&str> {
if value.trim().is_empty() {
None
} else {
Some(value)
}
}
fn clear_runlua_args_global(lua: &Lua) -> Result<(), String> {
lua.globals()
.set("__runlua_args", LuaValue::Nil)
.map_err(|error| format!("Failed to clear __runlua_args: {}", error))
}
fn reset_pooled_vm_request_scope(
lua: &Lua,
host_options: &LuaRuntimeHostOptions,
) -> Result<(), String> {
LuaEngine::populate_vulcan_request_context(lua, None)?;
populate_vulcan_internal_execution_context(lua, &VulcanInternalExecutionContext::default())?;
populate_vulcan_file_context(lua, None, None)?;
populate_vulcan_dependency_context(lua, host_options, None, None)?;
LuaEngine::populate_vulcan_lancedb_context(lua, None, None)?;
LuaEngine::populate_vulcan_sqlite_context(lua, None, None)?;
clear_runlua_args_global(lua)?;
Ok(())
}
struct LuaVmRequestScopeGuard<'a> {
lease: &'a mut LuaVmLease,
host_options: &'a LuaRuntimeHostOptions,
active: bool,
}
impl<'a> LuaVmRequestScopeGuard<'a> {
fn new(
lease: &'a mut LuaVmLease,
host_options: &'a LuaRuntimeHostOptions,
) -> Result<Self, String> {
let mut guard = Self {
lease,
host_options,
active: true,
};
if let Err(error) = reset_pooled_vm_request_scope(guard.lua(), host_options) {
guard.lease.discard();
guard.active = false;
return Err(error);
}
Ok(guard)
}
fn lua(&self) -> &Lua {
self.lease.lua()
}
fn finish(mut self) -> Result<(), String> {
let cleanup_result = reset_pooled_vm_request_scope(self.lua(), self.host_options);
if let Err(error) = cleanup_result {
self.lease.discard();
self.active = false;
return Err(error);
}
self.active = false;
Ok(())
}
}
impl Drop for LuaVmRequestScopeGuard<'_> {
fn drop(&mut self) {
if !self.active {
return;
}
if let Err(error) = reset_pooled_vm_request_scope(self.lua(), self.host_options) {
log_error(format!(
"[LuaSkill:error] Failed to reset pooled Lua VM request scope: {}",
error
));
self.lease.discard();
}
}
}
struct LuaNestedCallScopeGuard {
lua: Lua,
host_options: Arc<LuaRuntimeHostOptions>,
lancedb_host: Option<Arc<LanceDbSkillHost>>,
sqlite_host: Option<Arc<SqliteSkillHost>>,
core_state: VulcanCoreModuleState,
previous_context: LuaValue,
previous_client_info: LuaValue,
previous_client_capabilities: LuaValue,
previous_client_budget: LuaValue,
previous_tool_config: LuaValue,
previous_host_result: LuaValue,
previous_lancedb_skill_name: String,
previous_sqlite_skill_name: String,
previous_internal_context: VulcanInternalExecutionContext,
previous_file_context: (Option<String>, Option<String>, Option<String>),
active: bool,
}
impl LuaNestedCallScopeGuard {
fn new(
lua: &Lua,
host_options: Arc<LuaRuntimeHostOptions>,
lancedb_host: Option<Arc<LanceDbSkillHost>>,
sqlite_host: Option<Arc<SqliteSkillHost>>,
) -> Result<Self, String> {
let vulcan = get_vulcan_table(lua)?;
let context_table = get_vulcan_context_table(lua)?;
Ok(Self {
lua: lua.clone(),
host_options,
lancedb_host,
sqlite_host,
core_state: VulcanCoreModuleState::capture(lua)?,
previous_context: context_table
.get("request")
.map_err(|error| format!("Failed to read vulcan.context.request: {}", error))?,
previous_client_info: context_table
.get("client_info")
.map_err(|error| format!("Failed to read vulcan.context.client_info: {}", error))?,
previous_client_capabilities: context_table.get("client_capabilities").map_err(
|error| {
format!(
"Failed to read vulcan.context.client_capabilities: {}",
error
)
},
)?,
previous_client_budget: context_table.get("client_budget").map_err(|error| {
format!("Failed to read vulcan.context.client_budget: {}", error)
})?,
previous_tool_config: context_table
.get("tool_config")
.map_err(|error| format!("Failed to read vulcan.context.tool_config: {}", error))?,
previous_host_result: context_table
.get("host_result")
.map_err(|error| format!("Failed to read vulcan.context.host_result: {}", error))?,
previous_lancedb_skill_name: vulcan.get("__lancedb_skill_name").unwrap_or_default(),
previous_sqlite_skill_name: vulcan.get("__sqlite_skill_name").unwrap_or_default(),
previous_internal_context: capture_vulcan_internal_execution_context(lua)?,
previous_file_context: capture_vulcan_file_context(lua)?,
active: true,
})
}
fn enter_nested_call(
&self,
dispatch_entry_display_name: &str,
owner_skill_name: &str,
owner_local_name: &str,
owner_root_name: &str,
owner_skill_dir: &str,
entry_path: &str,
nested_invocation_context: &LuaInvocationContext,
target_lancedb_binding: Option<Arc<LanceDbSkillBinding>>,
target_sqlite_binding: Option<Arc<SqliteSkillBinding>>,
) -> Result<(), String> {
LuaEngine::populate_vulcan_request_context(&self.lua, Some(nested_invocation_context))?;
populate_vulcan_internal_execution_context(
&self.lua,
&VulcanInternalExecutionContext {
tool_name: Some(dispatch_entry_display_name.to_string()),
skill_name: Some(owner_skill_name.to_string()),
entry_name: Some(owner_local_name.to_string()),
root_name: Some(owner_root_name.to_string()),
luaexec_active: self.previous_internal_context.luaexec_active,
luaexec_caller_tool_name: self
.previous_internal_context
.luaexec_caller_tool_name
.clone(),
},
)?;
populate_vulcan_file_context(
&self.lua,
Some(Path::new(owner_skill_dir)),
Some(Path::new(entry_path)),
)?;
populate_vulcan_dependency_context(
&self.lua,
self.host_options.as_ref(),
Some(Path::new(owner_skill_dir)),
Some(owner_skill_name),
)?;
LuaEngine::populate_vulcan_lancedb_context(
&self.lua,
target_lancedb_binding,
Some(owner_skill_name),
)?;
LuaEngine::populate_vulcan_sqlite_context(
&self.lua,
target_sqlite_binding,
Some(owner_skill_name),
)?;
Ok(())
}
fn restore_previous_state(&self) -> Result<(), String> {
self.core_state.restore(&self.lua)?;
let restore_lancedb_binding = match non_empty_skill_name(&self.previous_lancedb_skill_name)
{
Some(skill_name) => self
.lancedb_host
.as_ref()
.map(|host| host.binding_for_skill(skill_name))
.transpose()?
.flatten(),
None => None,
};
let restore_sqlite_binding = match non_empty_skill_name(&self.previous_sqlite_skill_name) {
Some(skill_name) => self
.sqlite_host
.as_ref()
.map(|host| host.binding_for_skill(skill_name))
.transpose()?
.flatten(),
None => None,
};
LuaEngine::populate_vulcan_lancedb_context(
&self.lua,
restore_lancedb_binding,
non_empty_skill_name(&self.previous_lancedb_skill_name),
)?;
LuaEngine::populate_vulcan_sqlite_context(
&self.lua,
restore_sqlite_binding,
non_empty_skill_name(&self.previous_sqlite_skill_name),
)?;
let context_table = get_vulcan_context_table(&self.lua)?;
context_table
.set("request", self.previous_context.clone())
.map_err(|error| format!("Failed to restore vulcan.context.request: {}", error))?;
context_table
.set("client_info", self.previous_client_info.clone())
.map_err(|error| format!("Failed to restore vulcan.context.client_info: {}", error))?;
context_table
.set(
"client_capabilities",
self.previous_client_capabilities.clone(),
)
.map_err(|error| {
format!(
"Failed to restore vulcan.context.client_capabilities: {}",
error
)
})?;
context_table
.set("client_budget", self.previous_client_budget.clone())
.map_err(|error| {
format!("Failed to restore vulcan.context.client_budget: {}", error)
})?;
context_table
.set("tool_config", self.previous_tool_config.clone())
.map_err(|error| format!("Failed to restore vulcan.context.tool_config: {}", error))?;
context_table
.set("host_result", self.previous_host_result.clone())
.map_err(|error| format!("Failed to restore vulcan.context.host_result: {}", error))?;
populate_vulcan_internal_execution_context(&self.lua, &self.previous_internal_context)?;
populate_vulcan_file_context(
&self.lua,
self.previous_file_context.0.as_deref().map(Path::new),
self.previous_file_context.2.as_deref().map(Path::new),
)?;
populate_vulcan_dependency_context(
&self.lua,
self.host_options.as_ref(),
self.previous_file_context.0.as_deref().map(Path::new),
self.previous_internal_context.skill_name.as_deref(),
)?;
Ok(())
}
fn finish(mut self) -> Result<(), String> {
let restore_result = self.restore_previous_state();
self.active = false;
restore_result
}
}
impl Drop for LuaNestedCallScopeGuard {
fn drop(&mut self) {
if !self.active {
return;
}
if let Err(error) = self.restore_previous_state() {
log_error(format!(
"[LuaSkill:error] Failed to restore nested vulcan.call context: {}",
error
));
}
}
}
struct LuaVmLease {
pool: Arc<LuaVmPool>,
vm: Option<LuaVm>,
}
impl LuaVmLease {
fn lua(&self) -> &Lua {
&self.vm.as_ref().expect("lua vm lease missing instance").lua
}
fn discard(&mut self) {
if let Some(vm) = self.vm.take() {
self.pool.discard(vm);
}
}
}
impl Drop for LuaVmLease {
fn drop(&mut self) {
if let Some(mut vm) = self.vm.take() {
vm.last_used_at = Instant::now();
self.pool.release(vm);
}
}
}
impl LuaVmPool {
fn new(config: LuaVmPoolConfig) -> Self {
Self {
config: config.normalized(),
state: Mutex::new(LuaVmPoolState {
available: Vec::new(),
total_count: 0,
}),
condvar: Condvar::new(),
}
}
fn prewarm<F>(&self, mut factory: F) -> Result<(), String>
where
F: FnMut() -> Result<LuaVm, String>,
{
while self.total_count() < self.config.min_size {
{
let mut state = self.state.lock().unwrap();
state.total_count += 1;
}
match factory() {
Ok(vm) => self.release(vm),
Err(error) => {
let mut state = self.state.lock().unwrap();
state.total_count = state.total_count.saturating_sub(1);
return Err(error);
}
}
}
Ok(())
}
fn acquire<F>(self: &Arc<Self>, mut factory: F) -> Result<LuaVmLease, String>
where
F: FnMut() -> Result<LuaVm, String>,
{
loop {
let mut state = self.state.lock().unwrap();
self.reap_idle_locked(&mut state);
if let Some(mut vm) = state.available.pop() {
vm.last_used_at = Instant::now();
return Ok(LuaVmLease {
pool: self.clone(),
vm: Some(vm),
});
}
if state.total_count < self.config.max_size {
state.total_count += 1;
drop(state);
match factory() {
Ok(vm) => {
return Ok(LuaVmLease {
pool: self.clone(),
vm: Some(vm),
});
}
Err(error) => {
let mut state = self.state.lock().unwrap();
state.total_count = state.total_count.saturating_sub(1);
self.condvar.notify_one();
return Err(error);
}
}
}
let _guard = self.condvar.wait(state).unwrap();
}
}
fn release(&self, vm: LuaVm) {
let mut state = self.state.lock().unwrap();
state.available.push(vm);
self.reap_idle_locked(&mut state);
self.condvar.notify_one();
}
fn discard(&self, _vm: LuaVm) {
let mut state = self.state.lock().unwrap();
if state.total_count > 0 {
state.total_count -= 1;
}
self.condvar.notify_one();
}
fn total_count(&self) -> usize {
self.state.lock().unwrap().total_count
}
fn reap_idle_locked(&self, state: &mut LuaVmPoolState) {
if state.total_count <= self.config.min_size {
return;
}
let idle_limit = Duration::from_secs(self.config.idle_ttl_secs);
let now = Instant::now();
let mut index = 0usize;
while index < state.available.len() && state.total_count > self.config.min_size {
let should_remove = now
.checked_duration_since(state.available[index].last_used_at)
.map(|idle| idle >= idle_limit)
.unwrap_or(false);
if should_remove {
state.available.swap_remove(index);
state.total_count = state.total_count.saturating_sub(1);
} else {
index += 1;
}
}
}
}
impl LuaEngine {
fn normalized_skill_root_name(root_name: &str) -> String {
root_name.trim().to_ascii_uppercase()
}
fn normalized_skill_root_label(root: &RuntimeSkillRoot) -> String {
Self::normalized_skill_root_name(&root.name)
}
fn formal_skill_root_rank(label: &str) -> Option<usize> {
match label {
"ROOT" => Some(0),
"PROJECT" => Some(1),
"USER" => Some(2),
_ => None,
}
}
fn is_root_skill_root(root: &RuntimeSkillRoot) -> bool {
Self::normalized_skill_root_label(root) == "ROOT"
}
fn is_user_mutable_skill_root(root: &RuntimeSkillRoot) -> bool {
matches!(
Self::normalized_skill_root_label(root).as_str(),
"PROJECT" | "USER"
)
}
fn validate_formal_skill_root_chain(skill_roots: &[RuntimeSkillRoot]) -> Result<(), String> {
if skill_roots.is_empty() {
return Err(
"ROOT skill root is required; pass a ROOT layer before starting LuaSkills"
.to_string(),
);
}
let mut previous_rank = None;
let mut seen_labels = BTreeSet::new();
for root in skill_roots {
let label = Self::normalized_skill_root_label(root);
let rank = Self::formal_skill_root_rank(&label).ok_or_else(|| {
format!(
"unsupported skill root label '{}'; expected one of ROOT, PROJECT, USER",
root.name
)
})?;
if !seen_labels.insert(label.clone()) {
return Err(format!(
"duplicate skill root label '{}'; only one ROOT, PROJECT, and USER root is supported",
label
));
}
if previous_rank
.map(|previous_rank| rank < previous_rank)
.unwrap_or(false)
{
return Err(
"skill roots must be ordered by fixed priority ROOT -> PROJECT -> USER"
.to_string(),
);
}
previous_rank = Some(rank);
}
if !seen_labels.contains("ROOT") {
return Err(
"ROOT skill root is required; pass a ROOT layer before starting LuaSkills"
.to_string(),
);
}
Ok(())
}
fn find_skill_root_by_label<'a>(
skill_roots: &'a [RuntimeSkillRoot],
label: &str,
) -> Option<&'a RuntimeSkillRoot> {
skill_roots
.iter()
.find(|root| Self::normalized_skill_root_label(root) == label)
}
fn default_install_skill_root<'a>(
&self,
plane: SkillOperationPlane,
skill_roots: &'a [RuntimeSkillRoot],
) -> Result<&'a RuntimeSkillRoot, String> {
match plane {
SkillOperationPlane::Skills => Self::find_skill_root_by_label(skill_roots, "USER")
.or_else(|| Self::find_skill_root_by_label(skill_roots, "PROJECT"))
.filter(|root| Self::is_user_mutable_skill_root(root))
.ok_or_else(|| {
"ordinary skills plane requires a PROJECT or USER skill root; ROOT is system-controlled"
.to_string()
}),
SkillOperationPlane::System => {
if let Some(root) = Self::find_skill_root_by_label(skill_roots, "ROOT") {
Ok(root)
} else {
Err(
"system install requires a configured ROOT skill root; ordinary PROJECT/USER layers must be managed through the skills plane"
.to_string(),
)
}
}
}
}
fn operation_plane_for_authority(authority: SkillManagementAuthority) -> SkillOperationPlane {
match authority {
SkillManagementAuthority::System => SkillOperationPlane::System,
SkillManagementAuthority::DelegatedTool => SkillOperationPlane::Skills,
}
}
fn canonical_skill_config_runtime_root(
&self,
skill_roots: &[RuntimeSkillRoot],
) -> Result<PathBuf, String> {
let mut candidates: Vec<PathBuf> = Vec::new();
for skill_root in skill_roots {
let candidate = normalize_runtime_root_path(&self.runtime_root_for(skill_root));
if !candidates.iter().any(|existing| existing == &candidate) {
candidates.push(candidate);
}
}
match candidates.len() {
0 => Err("at least one skill root is required to resolve the unified skill config path".to_string()),
1 => Ok(candidates.remove(0)),
_ => Err(
"multiple runtime roots map to different parents; set host_options.skill_config_file_path explicitly".to_string()
),
}
}
pub fn new(options: LuaEngineOptions) -> Result<Self, Box<dyn std::error::Error>> {
let _default_text_encoding = resolve_host_default_text_encoding(&options.host_options)
.map_err(std::io::Error::other)?;
let runlua_pool_config = options
.host_options
.runlua_pool_config
.map(|config| LuaVmPoolConfig {
min_size: config.min_size,
max_size: config.max_size,
idle_ttl_secs: config.idle_ttl_secs,
})
.unwrap_or_else(default_runlua_vm_pool_config);
configure_global_tool_cache(
options
.host_options
.cache_config
.clone()
.unwrap_or_else(ToolCacheConfig::default),
);
let database_provider_callbacks = Arc::new(
RuntimeDatabaseProviderCallbacks::capture_process_defaults()
.map_err(std::io::Error::other)?,
);
Ok(Self {
skills: HashMap::new(),
entry_registry: BTreeMap::new(),
runtime_skill_roots: Vec::new(),
pool: Arc::new(LuaVmPool::new(options.pool_config)),
runlua_pool: Arc::new(LuaVmPool::new(runlua_pool_config)),
runtime_sessions: Arc::new(RuntimeSessionManager::new()),
skill_config_store: Arc::new(
SkillConfigStore::new(options.host_options.skill_config_file_path.clone())
.map_err(std::io::Error::other)?,
),
lancedb_host: None,
sqlite_host: None,
database_provider_callbacks,
host_options: Arc::new(options.host_options),
})
}
fn runtime_root_for(&self, skill_root: &RuntimeSkillRoot) -> PathBuf {
skill_root
.skills_dir
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| skill_root.skills_dir.clone())
}
fn packaged_runtime_resources_dirs(&self, skill_roots: &[RuntimeSkillRoot]) -> Vec<PathBuf> {
let mut deduped = BTreeSet::new();
if let Some(resources_dir) = self.host_options.resources_dir.as_ref() {
deduped.insert(normalize_runtime_root_path(resources_dir));
} else {
for skill_root in skill_roots {
deduped.insert(normalize_runtime_root_path(
&self.runtime_root_for(skill_root).join("resources"),
));
}
}
deduped.into_iter().collect()
}
fn validate_packaged_runtime_resources(
&self,
skill_roots: &[RuntimeSkillRoot],
) -> Result<(), String> {
for resources_dir in self.packaged_runtime_resources_dirs(skill_roots) {
validate_packaged_runtime_packages_layout(&resources_dir)?;
}
Ok(())
}
fn state_root_for(&self, skill_root: &RuntimeSkillRoot) -> PathBuf {
self.runtime_root_for(skill_root)
.join(self.host_options.state_dir_name.as_str())
}
fn dependency_root_for(&self, skill_root: &RuntimeSkillRoot) -> PathBuf {
self.runtime_root_for(skill_root)
.join(self.host_options.dependency_dir_name.as_str())
}
fn is_host_ignored_skill(&self, skill_id: &str) -> bool {
self.host_options
.ignored_skill_ids
.iter()
.any(|ignored| ignored.trim() == skill_id)
}
fn database_root_for(&self, skill_root: &RuntimeSkillRoot) -> PathBuf {
self.runtime_root_for(skill_root)
.join(self.host_options.database_dir_name.as_str())
}
fn refresh_skill_config_runtime_root(
&self,
skill_roots: &[RuntimeSkillRoot],
) -> Result<(), String> {
if self.skill_config_store.has_explicit_file_path() {
return Ok(());
}
let runtime_root = self.canonical_skill_config_runtime_root(skill_roots)?;
self.skill_config_store
.set_default_runtime_root(&runtime_root)
}
fn empty_reload_candidate(&self) -> Result<Self, Box<dyn std::error::Error>> {
let explicit_skill_config_file_path = if self.skill_config_store.has_explicit_file_path() {
Some(
self.skill_config_store
.file_path()
.map_err(std::io::Error::other)?,
)
} else {
None
};
Ok(Self {
skills: HashMap::new(),
entry_registry: BTreeMap::new(),
runtime_skill_roots: Vec::new(),
pool: Arc::new(LuaVmPool::new(self.pool.config)),
runlua_pool: Arc::new(LuaVmPool::new(self.runlua_pool.config)),
runtime_sessions: Arc::new(RuntimeSessionManager::new()),
skill_config_store: Arc::new(
SkillConfigStore::new(explicit_skill_config_file_path)
.map_err(std::io::Error::other)?,
),
lancedb_host: None,
sqlite_host: None,
database_provider_callbacks: self.database_provider_callbacks.clone(),
host_options: self.host_options.clone(),
})
}
fn replace_runtime_state_from(&mut self, next: LuaEngine) {
self.skills = next.skills;
self.entry_registry = next.entry_registry;
self.runtime_skill_roots = next.runtime_skill_roots;
self.pool = next.pool;
self.runlua_pool = next.runlua_pool;
self.runtime_sessions = next.runtime_sessions;
self.skill_config_store = next.skill_config_store;
self.lancedb_host = next.lancedb_host;
self.sqlite_host = next.sqlite_host;
self.database_provider_callbacks = next.database_provider_callbacks;
self.host_options = next.host_options;
}
fn dependency_manager_config_for(
&self,
skill_root: &RuntimeSkillRoot,
) -> Result<DependencyManagerConfig, String> {
let runtime_root = skill_root
.skills_dir
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| skill_root.skills_dir.clone());
let dependency_root = self.dependency_root_for(skill_root);
let tool_root = dependency_root.join("tools");
let host_tool_root = self
.host_options
.host_provided_tool_root
.clone()
.unwrap_or_else(|| runtime_root.join("bin").join("tools"));
let lua_root = dependency_root.join("lua");
let host_lua_root = self
.host_options
.host_provided_lua_root
.clone()
.or_else(|| self.host_options.lua_packages_dir.clone())
.unwrap_or_else(|| runtime_root.join("lua_packages"));
let ffi_root = dependency_root.join("ffi");
let host_ffi_root = self
.host_options
.host_provided_ffi_root
.clone()
.or_else(|| {
self.host_options
.lancedb_library_path
.as_ref()
.and_then(|path| path.parent().map(Path::to_path_buf))
})
.or_else(|| {
self.host_options
.sqlite_library_path
.as_ref()
.and_then(|path| path.parent().map(Path::to_path_buf))
})
.unwrap_or_else(|| runtime_root.join("libs"));
let download_cache_root = self
.host_options
.download_cache_root
.clone()
.unwrap_or_else(|| runtime_root.join("temp").join("downloads"));
ensure_directory(&tool_root)?;
ensure_directory(&host_tool_root)?;
ensure_directory(&lua_root)?;
ensure_directory(&host_lua_root)?;
ensure_directory(&ffi_root)?;
ensure_directory(&host_ffi_root)?;
ensure_directory(&download_cache_root)?;
Ok(DependencyManagerConfig {
tool_root,
host_tool_root,
lua_root,
host_lua_root,
ffi_root,
host_ffi_root,
download_cache_root,
allow_network_download: self.host_options.allow_network_download,
github_base_url: self.host_options.github_base_url.clone(),
github_api_base_url: self.host_options.github_api_base_url.clone(),
})
}
fn skill_manager_for(&self, skill_root: &RuntimeSkillRoot) -> Result<SkillManager, String> {
let state_root = self.state_root_for(skill_root);
let dependency_config = self.dependency_manager_config_for(skill_root)?;
ensure_directory(&state_root)?;
Ok(SkillManager::new(SkillManagerConfig {
skill_root: skill_root.clone(),
lifecycle_root: state_root,
download_cache_root: dependency_config.download_cache_root,
allow_network_download: dependency_config.allow_network_download,
github_base_url: dependency_config.github_base_url,
github_api_base_url: dependency_config.github_api_base_url,
}))
}
fn ensure_skill_dependencies(
&self,
skill_root: &RuntimeSkillRoot,
skill_dir: &Path,
) -> Result<(), String> {
let dependencies_path = skill_dir.join("dependencies.yaml");
if !dependencies_path.exists() {
return Ok(());
}
let manifest = SkillDependencyManifest::load_from_path(&dependencies_path)?;
if manifest.is_empty() {
return Ok(());
}
let skill_name = skill_dir
.file_name()
.and_then(|value| value.to_str())
.unwrap_or("unknown-skill");
let manager = DependencyManager::new(self.dependency_manager_config_for(skill_root)?);
manager.ensure_skill_dependencies(skill_name, &manifest)
}
fn load_skill_dependency_manifest(
&self,
skill_dir: &Path,
) -> Result<Option<SkillDependencyManifest>, String> {
let dependencies_path = skill_dir.join("dependencies.yaml");
if !dependencies_path.exists() {
return Ok(None);
}
SkillDependencyManifest::load_from_path(&dependencies_path).map(Some)
}
fn runtime_skill_roots_match(left: &RuntimeSkillRoot, right: &RuntimeSkillRoot) -> bool {
left.name == right.name && left.skills_dir == right.skills_dir
}
fn runtime_skill_root_index(
skill_roots: &[RuntimeSkillRoot],
target_root: &RuntimeSkillRoot,
) -> Result<usize, String> {
skill_roots
.iter()
.position(|root| Self::runtime_skill_roots_match(root, target_root))
.ok_or_else(|| {
format!(
"target root '{}' at {} is not part of the full runtime root chain",
target_root.name,
target_root.skills_dir.display()
)
})
}
fn validate_ordinary_target_root(
skill_roots: &[RuntimeSkillRoot],
target_root: &RuntimeSkillRoot,
action: crate::skill::manager::SkillLifecycleAction,
) -> Result<(), String> {
Self::runtime_skill_root_index(skill_roots, target_root)?;
if Self::is_root_skill_root(target_root) {
return Err(format!(
"ordinary skills plane cannot {:?} the system-controlled ROOT skill root",
action
));
}
if !Self::is_user_mutable_skill_root(target_root) {
return Err(format!(
"ordinary skills plane can only {:?} PROJECT or USER skill roots; got '{}'",
action, target_root.name
));
}
Ok(())
}
fn validate_authority_for_target_root(
authority: SkillManagementAuthority,
target_root: &RuntimeSkillRoot,
action: crate::skill::manager::SkillLifecycleAction,
) -> Result<(), String> {
if authority == SkillManagementAuthority::DelegatedTool
&& Self::is_root_skill_root(target_root)
{
return Err(format!(
"DelegatedTool authority cannot {:?} the system-controlled ROOT skill root",
action
));
}
Ok(())
}
fn resolve_root_declared_skill_instance(
skill_roots: &[RuntimeSkillRoot],
skill_id: &str,
) -> Result<Option<ResolvedSkillInstance>, String> {
let Some(root) = Self::find_skill_root_by_label(skill_roots, "ROOT") else {
return Ok(None);
};
resolve_declared_skill_instance_from_roots(&[root.clone()], skill_id)
}
fn ensure_root_skill_id_is_not_system_occupied(
skill_roots: &[RuntimeSkillRoot],
target_root: &RuntimeSkillRoot,
skill_id: &str,
action: crate::skill::manager::SkillLifecycleAction,
) -> Result<(), String> {
if !matches!(
action,
crate::skill::manager::SkillLifecycleAction::Install
| crate::skill::manager::SkillLifecycleAction::Update
) || !Self::is_user_mutable_skill_root(target_root)
{
return Ok(());
}
if let Some(root_instance) =
Self::resolve_root_declared_skill_instance(skill_roots, skill_id)?
{
return Err(format!(
"skill '{}' is managed by the ROOT system layer at {}; {:?} in '{}' is not allowed until the ROOT skill is removed",
skill_id,
root_instance.actual_dir.display(),
action,
target_root.name
));
}
Ok(())
}
fn ensure_explicit_apply_target_will_be_effective(
skill_roots: &[RuntimeSkillRoot],
target_root: Option<&RuntimeSkillRoot>,
skill_id: &str,
) -> Result<(), String> {
let Some(target_root) = target_root else {
return Ok(());
};
let target_index = Self::runtime_skill_root_index(skill_roots, target_root)?;
let Some(effective_instance) =
resolve_declared_skill_instance_from_roots(skill_roots, skill_id)?
else {
return Ok(());
};
let effective_root = RuntimeSkillRoot {
name: effective_instance.root_name.clone(),
skills_dir: effective_instance.skills_root.clone(),
};
let effective_index = Self::runtime_skill_root_index(skill_roots, &effective_root)?;
if effective_index < target_index {
return Err(format!(
"skill '{}' in target root '{}' is shadowed by higher-priority root '{}'; update the higher-priority layer or remove that override before changing this fallback root",
skill_id, target_root.name, effective_instance.root_name
));
}
Ok(())
}
fn rebuild_entry_registry(&mut self) -> Result<(), String> {
#[derive(Clone)]
struct EntrySeed {
skill_storage_key: String,
skill_id: String,
local_name: String,
base_name: String,
directory_name: String,
module_name: String,
}
let mut seeds = Vec::new();
for (skill_storage_key, skill) in &self.skills {
for tool in skill.meta.entries() {
let local_name = tool.name.trim().to_string();
if seeds.iter().any(|seed: &EntrySeed| {
seed.skill_storage_key == *skill_storage_key && seed.local_name == local_name
}) {
return Err(format!(
"skill '{}' declares duplicate local entry name '{}'",
skill.meta.effective_skill_id(),
local_name
));
}
let directory_name = skill
.dir
.file_name()
.and_then(|value| value.to_str())
.unwrap_or_default()
.to_string();
seeds.push(EntrySeed {
skill_storage_key: skill_storage_key.clone(),
skill_id: skill.meta.effective_skill_id().to_string(),
local_name: local_name.clone(),
base_name: skill.meta.tool_base_name(tool),
directory_name,
module_name: tool.lua_module.clone(),
});
}
}
seeds.sort_by(|left, right| {
(
left.base_name.as_str(),
left.directory_name.as_str(),
left.skill_id.as_str(),
left.local_name.as_str(),
left.module_name.as_str(),
)
.cmp(&(
right.base_name.as_str(),
right.directory_name.as_str(),
right.skill_id.as_str(),
right.local_name.as_str(),
right.module_name.as_str(),
))
});
for skill in self.skills.values_mut() {
skill.resolved_entry_names.clear();
}
let mut registry = BTreeMap::new();
let mut base_name_counters = HashMap::<String, usize>::new();
let mut occupied_names = self
.host_options
.reserved_entry_names
.iter()
.cloned()
.collect::<HashSet<String>>();
for seed in seeds {
let mut duplicate_index = *base_name_counters.get(&seed.base_name).unwrap_or(&0usize);
let canonical_name = loop {
duplicate_index += 1;
let candidate_name = if duplicate_index == 1 {
seed.base_name.clone()
} else {
format!("{}-{}", seed.base_name, duplicate_index)
};
if !occupied_names.contains(&candidate_name) {
break candidate_name;
}
};
base_name_counters.insert(seed.base_name.clone(), duplicate_index);
occupied_names.insert(canonical_name.clone());
let resolved_target = ResolvedEntryTarget {
canonical_name: canonical_name.clone(),
skill_storage_key: seed.skill_storage_key.clone(),
skill_id: seed.skill_id.clone(),
local_name: seed.local_name.clone(),
};
registry.insert(canonical_name.clone(), resolved_target);
let skill = self
.skills
.get_mut(&seed.skill_storage_key)
.ok_or_else(|| {
format!(
"internal error: missing loaded skill '{}' while building entry registry",
seed.skill_storage_key
)
})?;
skill
.resolved_entry_names
.insert(seed.local_name.clone(), canonical_name);
}
self.entry_registry = registry;
Ok(())
}
pub fn load_from_roots(
&mut self,
skill_roots: &[RuntimeSkillRoot],
) -> Result<(), Box<dyn std::error::Error>> {
Self::validate_formal_skill_root_chain(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
self.runtime_skill_roots = skill_roots.to_vec();
if !skill_roots.is_empty() {
self.refresh_skill_config_runtime_root(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
self.validate_packaged_runtime_resources(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
}
if skill_roots.iter().all(|root| !root.skills_dir.exists()) {
return Ok(());
}
for resolved_instance in collect_effective_skill_instances_from_roots(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
{
let skill_name = resolved_instance.skill_id;
if self.is_host_ignored_skill(&skill_name) {
log_info(format!(
"[LuaSkill] Skipped host-ignored skill '{}'",
skill_name
));
continue;
}
let resolved_root = RuntimeSkillRoot {
name: resolved_instance.root_name.clone(),
skills_dir: resolved_instance.skills_root.clone(),
};
let resolved_skill_manager = self
.skill_manager_for(&resolved_root)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
if !resolved_skill_manager.is_skill_enabled(&skill_name)? {
log_warn(format!(
"[LuaSkill] Skipped disabled skill '{}'",
skill_name
));
continue;
}
let actual_dir = resolved_instance.actual_dir;
log_info(format!(
"[LuaSkill] Loaded '{}' from root '{}'",
skill_name, resolved_instance.root_name
));
if let Err(error) = self.ensure_skill_dependencies(&resolved_root, &actual_dir) {
log_error(format!(
"[LuaSkill] Failed to prepare dependencies for {}: {}",
skill_name, error
));
continue;
}
if let Err(e) = self.load_single_skill(&actual_dir, &resolved_instance.root_name) {
log_error(format!("[LuaSkill] Failed to load {}: {}", skill_name, e));
}
}
self.rebuild_entry_registry()
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
self.pool
.prewarm(|| self.create_vm())
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
self.runlua_pool
.prewarm(|| {
Self::create_runlua_vm(
&self.skills,
&self.entry_registry,
self.host_options.clone(),
self.skill_config_store.clone(),
self.runtime_skill_roots.clone(),
self.lancedb_host.clone(),
self.sqlite_host.clone(),
)
})
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
log_info(format!("[LuaSkill] {} skills loaded", self.skills.len()));
Ok(())
}
pub fn reload_from_roots(
&mut self,
skill_roots: &[RuntimeSkillRoot],
) -> Result<(), Box<dyn std::error::Error>> {
Self::validate_formal_skill_root_chain(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
let previous_entries = self.list_entries();
let mut next = self.empty_reload_candidate()?;
next.load_from_roots(skill_roots)?;
self.replace_runtime_state_from(next);
self.emit_entry_registry_delta(previous_entries);
Ok(())
}
fn mutate_skill_state_and_reload(
&mut self,
plane: SkillOperationPlane,
action: crate::skill::manager::SkillLifecycleAction,
skill_roots: &[RuntimeSkillRoot],
skill_id: &str,
reason: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
Self::validate_formal_skill_root_chain(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
validate_luaskills_identifier(skill_id, "skill_id")
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
let resolved_instance = resolve_declared_skill_instance_from_roots(skill_roots, skill_id)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
.ok_or_else(|| -> Box<dyn std::error::Error> {
format!("declared skill instance '{}' not found", skill_id).into()
})?;
let resolved_root = RuntimeSkillRoot {
name: resolved_instance.root_name.clone(),
skills_dir: resolved_instance.skills_root.clone(),
};
let manager = self
.skill_manager_for(&resolved_root)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
let removed_dependency_manifest =
if action == crate::skill::manager::SkillLifecycleAction::Uninstall {
let dependencies_path = resolved_instance.actual_dir.join("dependencies.yaml");
if dependencies_path.exists() {
Some(
SkillDependencyManifest::load_from_path(&dependencies_path)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
)
} else {
None
}
} else {
None
};
if let Err(error) = manager.guard_operation(plane, action, skill_id) {
self.emit_skill_lifecycle_event(
plane,
action,
skill_id,
Some(resolved_instance.root_name.clone()),
Some(resolved_instance.actual_dir.display().to_string()),
"blocked",
Some(error.clone()),
);
return Err(error.into());
}
let action_result = match action {
crate::skill::manager::SkillLifecycleAction::Disable => manager
.disable_skill_in_plane(plane, skill_id, reason)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() }),
crate::skill::manager::SkillLifecycleAction::Enable => manager
.enable_skill_in_plane(plane, skill_id)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() }),
crate::skill::manager::SkillLifecycleAction::Uninstall => manager
.uninstall_skill_at_path_in_plane(plane, skill_id, &resolved_instance.actual_dir)
.map(|_| ())
.map_err(|error| -> Box<dyn std::error::Error> { error.into() }),
_ => {
return Err(format!("unsupported state mutation action {:?}", action).into());
}
};
if let Err(error) = action_result {
let message = error.to_string();
self.emit_skill_lifecycle_event(
plane,
action,
skill_id,
Some(resolved_instance.root_name.clone()),
Some(resolved_instance.actual_dir.display().to_string()),
"failed",
Some(message),
);
return Err(error);
}
if action == crate::skill::manager::SkillLifecycleAction::Uninstall {
let dependency_manager = DependencyManager::new(
self.dependency_manager_config_for(&resolved_root)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
);
dependency_manager
.cleanup_uninstalled_skill_dependencies_from_roots(
skill_roots,
skill_id,
removed_dependency_manifest.as_ref(),
)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
}
self.reload_from_roots(skill_roots)?;
self.emit_skill_lifecycle_event(
plane,
action,
skill_id,
Some(resolved_instance.root_name.clone()),
Some(resolved_instance.actual_dir.display().to_string()),
"completed",
None,
);
Ok(())
}
fn remove_skill_database_dir(
&self,
database_root: &Path,
skill_id: &str,
remove_requested: bool,
database_label: &str,
) -> Result<(bool, bool), Box<dyn std::error::Error>> {
if !remove_requested {
return Ok((false, true));
}
let database_dir = database_root.join(database_label).join(skill_id);
if !database_dir.exists() {
return Ok((false, false));
}
fs::remove_dir_all(&database_dir).map_err(|error| {
format!(
"failed to remove {database_label} directory {}: {}",
database_dir.display(),
error
)
})?;
Ok((true, false))
}
fn uninstall_skill_and_reload(
&mut self,
plane: SkillOperationPlane,
skill_roots: &[RuntimeSkillRoot],
skill_id: &str,
options: &SkillUninstallOptions,
) -> Result<SkillUninstallResult, Box<dyn std::error::Error>> {
self.uninstall_skill_and_reload_in_root(plane, skill_roots, None, skill_id, options)
}
fn uninstall_skill_and_reload_in_root(
&mut self,
plane: SkillOperationPlane,
skill_roots: &[RuntimeSkillRoot],
target_root: Option<&RuntimeSkillRoot>,
skill_id: &str,
options: &SkillUninstallOptions,
) -> Result<SkillUninstallResult, Box<dyn std::error::Error>> {
Self::validate_formal_skill_root_chain(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
validate_luaskills_identifier(skill_id, "skill_id")
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
if let Some(target_root) = target_root {
Self::runtime_skill_root_index(skill_roots, target_root)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
}
let target_roots = target_root.map(|root| vec![root.clone()]);
let resolution_roots = target_roots.as_deref().unwrap_or(skill_roots);
let resolved_instance = if target_root.is_some() {
resolve_declared_skill_instance_from_roots(resolution_roots, skill_id)
} else {
resolve_effective_skill_instance_from_roots(resolution_roots, skill_id)
}
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
.ok_or_else(|| -> Box<dyn std::error::Error> {
match target_root {
Some(root) => format!(
"skill instance '{}' not found in target root '{}'",
skill_id, root.name
)
.into(),
None => format!("effective skill instance '{}' not found", skill_id).into(),
}
})?;
let resolved_root = RuntimeSkillRoot {
name: resolved_instance.root_name.clone(),
skills_dir: resolved_instance.skills_root.clone(),
};
let manager = self
.skill_manager_for(&resolved_root)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
let dependencies_path = resolved_instance.actual_dir.join("dependencies.yaml");
let removed_dependency_manifest = if dependencies_path.exists() {
Some(
SkillDependencyManifest::load_from_path(&dependencies_path)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
)
} else {
None
};
if let Err(error) = manager.guard_operation(
plane,
crate::skill::manager::SkillLifecycleAction::Uninstall,
skill_id,
) {
self.emit_skill_lifecycle_event(
plane,
crate::skill::manager::SkillLifecycleAction::Uninstall,
skill_id,
Some(resolved_instance.root_name.clone()),
Some(resolved_instance.actual_dir.display().to_string()),
"blocked",
Some(error.clone()),
);
return Err(error.into());
}
let prepared_uninstall = match manager.prepare_uninstall_skill_at_path_in_plane(
plane,
skill_id,
&resolved_instance.actual_dir,
) {
Ok(prepared) => prepared,
Err(error) => {
let message = error.to_string();
self.emit_skill_lifecycle_event(
plane,
crate::skill::manager::SkillLifecycleAction::Uninstall,
skill_id,
Some(resolved_instance.root_name.clone()),
Some(resolved_instance.actual_dir.display().to_string()),
"failed",
Some(message),
);
return Err(error.into());
}
};
if let Err(reload_error) = self.reload_from_roots(skill_roots) {
let rollback_error = manager.rollback_prepared_skill_uninstall(&prepared_uninstall);
let restore_error = self.reload_from_roots(skill_roots);
let rollback_message = rollback_error
.err()
.map(|error| format!(" rollback failed: {}", error))
.unwrap_or_default();
let restore_message = restore_error
.err()
.map(|error| format!(" runtime restore failed: {}", error))
.unwrap_or_default();
let message = format!(
"Failed to reload LuaSkills after uninstall: {}.{}{}",
reload_error, rollback_message, restore_message
);
self.emit_skill_lifecycle_event(
plane,
crate::skill::manager::SkillLifecycleAction::Uninstall,
skill_id,
Some(resolved_instance.root_name.clone()),
Some(resolved_instance.actual_dir.display().to_string()),
"failed",
Some(message.clone()),
);
return Err(message.into());
}
let mut result = match manager.commit_prepared_skill_uninstall(&prepared_uninstall) {
Ok(result) => result,
Err(error) => {
let rollback_error = manager.rollback_prepared_skill_uninstall(&prepared_uninstall);
let restore_error = self.reload_from_roots(skill_roots);
let rollback_message = rollback_error
.err()
.map(|rollback| format!(" rollback failed: {}", rollback))
.unwrap_or_default();
let restore_message = restore_error
.err()
.map(|restore| format!(" runtime restore failed: {}", restore))
.unwrap_or_default();
let message = format!(
"Failed to finalize uninstall: {}.{}{}",
error, rollback_message, restore_message
);
self.emit_skill_lifecycle_event(
plane,
crate::skill::manager::SkillLifecycleAction::Uninstall,
skill_id,
Some(resolved_instance.root_name.clone()),
Some(resolved_instance.actual_dir.display().to_string()),
"failed",
Some(message.clone()),
);
return Err(message.into());
}
};
let dependency_manager = DependencyManager::new(
self.dependency_manager_config_for(&resolved_root)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
);
if let Err(error) = dependency_manager.cleanup_uninstalled_skill_dependencies_from_roots(
skill_roots,
skill_id,
removed_dependency_manifest.as_ref(),
) {
log_warn(format!(
"[LuaSkills:uninstall] Stale dependency cleanup warning for skill '{}': {}",
skill_id, error
));
result.message = format!(
"{} (warning: stale dependency cleanup failed: {})",
result.message, error
);
}
let (sqlite_removed, sqlite_retained) = match self.remove_skill_database_dir(
&self.database_root_for(&resolved_root),
skill_id,
options.remove_sqlite,
"sqlite",
) {
Ok(result) => result,
Err(error) => {
log_warn(format!(
"[LuaSkills:uninstall] SQLite cleanup warning for skill '{}': {}",
skill_id, error
));
result.message = format!(
"{} (warning: sqlite cleanup failed: {})",
result.message, error
);
(false, false)
}
};
let (lancedb_removed, lancedb_retained) = match self.remove_skill_database_dir(
&self.database_root_for(&resolved_root),
skill_id,
options.remove_lancedb,
"lancedb",
) {
Ok(result) => result,
Err(error) => {
log_warn(format!(
"[LuaSkills:uninstall] LanceDB cleanup warning for skill '{}': {}",
skill_id, error
));
result.message = format!(
"{} (warning: lancedb cleanup failed: {})",
result.message, error
);
(false, false)
}
};
result.sqlite_removed = sqlite_removed;
result.sqlite_retained = sqlite_retained;
result.lancedb_removed = lancedb_removed;
result.lancedb_retained = lancedb_retained;
let summary = format!(
"skill package removed={} sqlite_removed={} sqlite_retained={} lancedb_removed={} lancedb_retained={}",
result.skill_removed,
result.sqlite_removed,
result.sqlite_retained,
result.lancedb_removed,
result.lancedb_retained
);
result.message = if result.message.is_empty() {
summary
} else {
format!("{}; {}", summary, result.message)
};
self.emit_skill_lifecycle_event(
plane,
crate::skill::manager::SkillLifecycleAction::Uninstall,
skill_id,
Some(resolved_instance.root_name.clone()),
Some(resolved_instance.actual_dir.display().to_string()),
"completed",
Some(result.message.clone()),
);
Ok(result)
}
fn apply_skill_request(
&mut self,
plane: SkillOperationPlane,
action: crate::skill::manager::SkillLifecycleAction,
skill_roots: &[RuntimeSkillRoot],
request: &SkillInstallRequest,
) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
self.apply_skill_request_in_root(plane, action, skill_roots, None, request)
}
fn apply_skill_request_in_root(
&mut self,
plane: SkillOperationPlane,
action: crate::skill::manager::SkillLifecycleAction,
skill_roots: &[RuntimeSkillRoot],
target_root: Option<&RuntimeSkillRoot>,
request: &SkillInstallRequest,
) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
if !matches!(
action,
crate::skill::manager::SkillLifecycleAction::Install
| crate::skill::manager::SkillLifecycleAction::Update
) {
return Err(format!("unsupported apply action {:?}", action).into());
}
Self::validate_formal_skill_root_chain(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
let explicit_target_root = target_root;
if let Some(target_root) = explicit_target_root {
Self::runtime_skill_root_index(skill_roots, target_root)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
}
let requested_skill_id = resolve_requested_skill_id(request)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
let explicit_target_roots = explicit_target_root.map(|root| vec![root.clone()]);
let update_resolution_roots = explicit_target_roots.as_deref().unwrap_or(skill_roots);
let target_root = match action {
crate::skill::manager::SkillLifecycleAction::Install => {
if let Some(target_root) = explicit_target_root {
target_root.clone()
} else {
self.default_install_skill_root(plane, skill_roots)?.clone()
}
}
crate::skill::manager::SkillLifecycleAction::Update => {
let resolved_instance = resolve_declared_skill_instance_from_roots(
update_resolution_roots,
&requested_skill_id,
)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
.ok_or_else(|| -> Box<dyn std::error::Error> {
match explicit_target_root {
Some(root) => format!(
"skill '{}' is not installed in target root '{}'",
requested_skill_id, root.name
)
.into(),
None => format!("skill '{}' is not installed", requested_skill_id).into(),
}
})?;
RuntimeSkillRoot {
name: resolved_instance.root_name,
skills_dir: resolved_instance.skills_root,
}
}
_ => unreachable!("unsupported apply action should have returned early"),
};
Self::ensure_root_skill_id_is_not_system_occupied(
skill_roots,
&target_root,
&requested_skill_id,
action,
)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
if explicit_target_root.is_some() {
Self::ensure_explicit_apply_target_will_be_effective(
skill_roots,
explicit_target_root,
&requested_skill_id,
)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
}
let operation_roots_owned = if let Some(target_root) = explicit_target_root {
Some(vec![target_root.clone()])
} else if action == crate::skill::manager::SkillLifecycleAction::Install
&& plane == SkillOperationPlane::System
&& Self::is_root_skill_root(&target_root)
{
Some(vec![target_root.clone()])
} else {
None
};
let operation_roots = operation_roots_owned.as_deref().unwrap_or(skill_roots);
let previous_dependency_manifest =
if action == crate::skill::manager::SkillLifecycleAction::Update {
resolve_declared_skill_instance_from_roots(operation_roots, &requested_skill_id)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
.and_then(|resolved| {
self.load_skill_dependency_manifest(&resolved.actual_dir)
.transpose()
})
.transpose()
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
} else {
None
};
let manager = self
.skill_manager_for(&target_root)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
let prepared = match action {
crate::skill::manager::SkillLifecycleAction::Install => manager
.prepare_install_skill(plane, operation_roots, request)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
crate::skill::manager::SkillLifecycleAction::Update => manager
.prepare_update_skill(plane, operation_roots, request)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
_ => unreachable!("unsupported apply action should have returned early"),
};
let mut result = match &prepared {
PreparedSkillApply::Immediate(result) => result.clone(),
PreparedSkillApply::Install(_) | PreparedSkillApply::Update(_) => {
if let Err(reload_error) = self.reload_from_roots(skill_roots) {
let rollback_error = manager.rollback_prepared_skill_apply(&prepared);
let restore_error = self.reload_from_roots(skill_roots);
let rollback_message = rollback_error
.err()
.map(|error| format!(" rollback failed: {}", error))
.unwrap_or_default();
let restore_message = restore_error
.err()
.map(|error| format!(" runtime restore failed: {}", error))
.unwrap_or_default();
return Err(format!(
"Failed to reload LuaSkills after {:?}: {}.{}{}",
action, reload_error, rollback_message, restore_message
)
.into());
}
let committed = manager.commit_prepared_skill_apply(&prepared).map_err(
|error| -> Box<dyn std::error::Error> {
let rollback_error = manager.rollback_prepared_skill_apply(&prepared);
let restore_error = self.reload_from_roots(skill_roots);
let rollback_message = rollback_error
.err()
.map(|rollback| format!(" rollback failed: {}", rollback))
.unwrap_or_default();
let restore_message = restore_error
.err()
.map(|restore| format!(" runtime restore failed: {}", restore))
.unwrap_or_default();
format!(
"Failed to finalize {:?}: {}.{}{}",
action, error, rollback_message, restore_message
)
.into()
},
)?;
committed
}
};
if action == crate::skill::manager::SkillLifecycleAction::Update
&& result.status == "updated"
{
let current_dependency_manifest =
resolve_declared_skill_instance_from_roots(operation_roots, &result.skill_id)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
.and_then(|resolved| {
self.load_skill_dependency_manifest(&resolved.actual_dir)
.transpose()
})
.transpose()
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
let dependency_manager = DependencyManager::new(
self.dependency_manager_config_for(&target_root)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?,
);
if let Err(error) = dependency_manager.cleanup_updated_skill_dependencies(
&result.skill_id,
previous_dependency_manifest.as_ref(),
current_dependency_manifest.as_ref(),
) {
log_warn(format!(
"[LuaSkills:update] Stale dependency cleanup warning for skill '{}': {}",
result.skill_id, error
));
result.message = format!(
"{} (warning: stale dependency cleanup failed: {})",
result.message, error
);
}
}
let resolved_instance =
resolve_declared_skill_instance_from_roots(operation_roots, &result.skill_id)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
self.emit_skill_lifecycle_event(
plane,
action,
&result.skill_id,
resolved_instance
.as_ref()
.map(|instance| instance.root_name.clone()),
resolved_instance
.as_ref()
.map(|instance| instance.actual_dir.display().to_string()),
&result.status,
Some(result.message.clone()),
);
Ok(result)
}
pub fn disable_skill_in_roots(
&mut self,
skill_roots: &[RuntimeSkillRoot],
skill_id: &str,
reason: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
self.mutate_skill_state_and_reload(
SkillOperationPlane::Skills,
crate::skill::manager::SkillLifecycleAction::Disable,
skill_roots,
skill_id,
reason,
)
}
pub fn system_disable_skill_in_roots(
&mut self,
skill_roots: &[RuntimeSkillRoot],
authority: SkillManagementAuthority,
skill_id: &str,
reason: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
self.mutate_skill_state_and_reload(
Self::operation_plane_for_authority(authority),
crate::skill::manager::SkillLifecycleAction::Disable,
skill_roots,
skill_id,
reason,
)
}
pub fn enable_skill(
&mut self,
skill_roots: &[RuntimeSkillRoot],
skill_id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
self.mutate_skill_state_and_reload(
SkillOperationPlane::Skills,
crate::skill::manager::SkillLifecycleAction::Enable,
skill_roots,
skill_id,
None,
)
}
pub fn system_enable_skill(
&mut self,
skill_roots: &[RuntimeSkillRoot],
authority: SkillManagementAuthority,
skill_id: &str,
) -> Result<(), Box<dyn std::error::Error>> {
self.mutate_skill_state_and_reload(
Self::operation_plane_for_authority(authority),
crate::skill::manager::SkillLifecycleAction::Enable,
skill_roots,
skill_id,
None,
)
}
pub fn uninstall_skill(
&mut self,
skill_roots: &[RuntimeSkillRoot],
skill_id: &str,
options: &SkillUninstallOptions,
) -> Result<SkillUninstallResult, Box<dyn std::error::Error>> {
self.uninstall_skill_and_reload(SkillOperationPlane::Skills, skill_roots, skill_id, options)
}
pub fn uninstall_skill_in_root(
&mut self,
skill_roots: &[RuntimeSkillRoot],
target_root: &RuntimeSkillRoot,
skill_id: &str,
options: &SkillUninstallOptions,
) -> Result<SkillUninstallResult, Box<dyn std::error::Error>> {
Self::validate_formal_skill_root_chain(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
Self::validate_ordinary_target_root(
skill_roots,
target_root,
crate::skill::manager::SkillLifecycleAction::Uninstall,
)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
self.uninstall_skill_and_reload_in_root(
SkillOperationPlane::Skills,
skill_roots,
Some(target_root),
skill_id,
options,
)
}
pub fn system_uninstall_skill(
&mut self,
skill_roots: &[RuntimeSkillRoot],
authority: SkillManagementAuthority,
skill_id: &str,
options: &SkillUninstallOptions,
) -> Result<SkillUninstallResult, Box<dyn std::error::Error>> {
self.uninstall_skill_and_reload(
Self::operation_plane_for_authority(authority),
skill_roots,
skill_id,
options,
)
}
pub fn system_uninstall_skill_in_root(
&mut self,
skill_roots: &[RuntimeSkillRoot],
target_root: &RuntimeSkillRoot,
authority: SkillManagementAuthority,
skill_id: &str,
options: &SkillUninstallOptions,
) -> Result<SkillUninstallResult, Box<dyn std::error::Error>> {
Self::validate_formal_skill_root_chain(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
Self::runtime_skill_root_index(skill_roots, target_root)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
Self::validate_authority_for_target_root(
authority,
target_root,
crate::skill::manager::SkillLifecycleAction::Uninstall,
)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
let plane = Self::operation_plane_for_authority(authority);
self.uninstall_skill_and_reload_in_root(
plane,
skill_roots,
Some(target_root),
skill_id,
options,
)
}
pub fn install_skill(
&mut self,
skill_roots: &[RuntimeSkillRoot],
request: &SkillInstallRequest,
) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
self.apply_skill_request(
SkillOperationPlane::Skills,
crate::skill::manager::SkillLifecycleAction::Install,
skill_roots,
request,
)
}
pub fn install_skill_in_root(
&mut self,
skill_roots: &[RuntimeSkillRoot],
target_root: &RuntimeSkillRoot,
request: &SkillInstallRequest,
) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
Self::validate_formal_skill_root_chain(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
Self::validate_ordinary_target_root(
skill_roots,
target_root,
crate::skill::manager::SkillLifecycleAction::Install,
)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
self.apply_skill_request_in_root(
SkillOperationPlane::Skills,
crate::skill::manager::SkillLifecycleAction::Install,
skill_roots,
Some(target_root),
request,
)
}
pub fn system_install_skill(
&mut self,
skill_roots: &[RuntimeSkillRoot],
authority: SkillManagementAuthority,
request: &SkillInstallRequest,
) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
self.apply_skill_request(
Self::operation_plane_for_authority(authority),
crate::skill::manager::SkillLifecycleAction::Install,
skill_roots,
request,
)
}
pub fn system_install_skill_in_root(
&mut self,
skill_roots: &[RuntimeSkillRoot],
target_root: &RuntimeSkillRoot,
authority: SkillManagementAuthority,
request: &SkillInstallRequest,
) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
Self::validate_formal_skill_root_chain(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
Self::runtime_skill_root_index(skill_roots, target_root)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
Self::validate_authority_for_target_root(
authority,
target_root,
crate::skill::manager::SkillLifecycleAction::Install,
)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
let plane = Self::operation_plane_for_authority(authority);
self.apply_skill_request_in_root(
plane,
crate::skill::manager::SkillLifecycleAction::Install,
skill_roots,
Some(target_root),
request,
)
}
pub fn update_skill(
&mut self,
skill_roots: &[RuntimeSkillRoot],
request: &SkillInstallRequest,
) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
self.apply_skill_request(
SkillOperationPlane::Skills,
crate::skill::manager::SkillLifecycleAction::Update,
skill_roots,
request,
)
}
pub fn update_skill_in_root(
&mut self,
skill_roots: &[RuntimeSkillRoot],
target_root: &RuntimeSkillRoot,
request: &SkillInstallRequest,
) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
Self::validate_formal_skill_root_chain(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
Self::validate_ordinary_target_root(
skill_roots,
target_root,
crate::skill::manager::SkillLifecycleAction::Update,
)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
self.apply_skill_request_in_root(
SkillOperationPlane::Skills,
crate::skill::manager::SkillLifecycleAction::Update,
skill_roots,
Some(target_root),
request,
)
}
pub fn system_update_skill(
&mut self,
skill_roots: &[RuntimeSkillRoot],
authority: SkillManagementAuthority,
request: &SkillInstallRequest,
) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
self.apply_skill_request(
Self::operation_plane_for_authority(authority),
crate::skill::manager::SkillLifecycleAction::Update,
skill_roots,
request,
)
}
pub fn system_update_skill_in_root(
&mut self,
skill_roots: &[RuntimeSkillRoot],
target_root: &RuntimeSkillRoot,
authority: SkillManagementAuthority,
request: &SkillInstallRequest,
) -> Result<SkillApplyResult, Box<dyn std::error::Error>> {
Self::validate_formal_skill_root_chain(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
Self::runtime_skill_root_index(skill_roots, target_root)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
Self::validate_authority_for_target_root(
authority,
target_root,
crate::skill::manager::SkillLifecycleAction::Update,
)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
let plane = Self::operation_plane_for_authority(authority);
self.apply_skill_request_in_root(
plane,
crate::skill::manager::SkillLifecycleAction::Update,
skill_roots,
Some(target_root),
request,
)
}
fn emit_skill_lifecycle_event(
&self,
plane: SkillOperationPlane,
action: crate::skill::manager::SkillLifecycleAction,
skill_id: &str,
root_name: Option<String>,
skill_dir: Option<String>,
status: &str,
message: Option<String>,
) {
crate::host::callbacks::emit_skill_lifecycle_event(&RuntimeSkillLifecycleEvent {
plane,
action,
skill_id: skill_id.to_string(),
root_name,
skill_dir,
status: status.to_string(),
message,
});
}
fn emit_entry_registry_delta(&self, previous_entries: Vec<RuntimeEntryDescriptor>) {
let current_entries = self.list_entries();
let previous_map = previous_entries
.into_iter()
.map(|entry| (entry.canonical_name.clone(), entry))
.collect::<BTreeMap<String, RuntimeEntryDescriptor>>();
let current_map = current_entries
.into_iter()
.map(|entry| (entry.canonical_name.clone(), entry))
.collect::<BTreeMap<String, RuntimeEntryDescriptor>>();
let mut added_entries = Vec::new();
let mut updated_entries = Vec::new();
let mut removed_entry_names = Vec::new();
for (canonical_name, current_entry) in ¤t_map {
match previous_map.get(canonical_name) {
None => added_entries.push(current_entry.clone()),
Some(previous_entry) if previous_entry != current_entry => {
updated_entries.push(current_entry.clone());
}
Some(_) => {}
}
}
for canonical_name in previous_map.keys() {
if !current_map.contains_key(canonical_name) {
removed_entry_names.push(canonical_name.clone());
}
}
if added_entries.is_empty() && updated_entries.is_empty() && removed_entry_names.is_empty()
{
return;
}
crate::host::callbacks::emit_entry_registry_delta(&RuntimeEntryRegistryDelta {
added_entries,
removed_entry_names,
updated_entries,
});
}
fn load_single_skill(
&mut self,
dir: &Path,
root_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let skill_yaml = dir.join("skill.yaml");
if !skill_yaml.exists() {
return Err(format!("skill.yaml not found in {}", dir.display()).into());
}
let yaml_str = std::fs::read_to_string(&skill_yaml)?;
let yaml_value: serde_yaml::Value = serde_yaml::from_str(&yaml_str)?;
if yaml_value.as_mapping().is_some_and(|mapping| {
mapping.contains_key(serde_yaml::Value::String("skill_id".to_string()))
}) {
return Err(format!("skill {} must not declare skill_id in skill.yaml; directory name is the only skill_id", dir.display())
.into());
}
let mut meta: SkillMeta = serde_yaml::from_value(yaml_value)?;
let directory_skill_id = dir
.file_name()
.and_then(|value| value.to_str())
.ok_or_else(|| format!("invalid skill directory name: {}", dir.display()))?
.trim()
.to_string();
validate_luaskills_identifier(&directory_skill_id, "skill directory name")
.map_err(|error| format!("skill {}: {}", dir.display(), error))?;
meta.bind_directory_skill_id(directory_skill_id.clone());
if !meta.is_enabled() {
log_info(format!(
"[LuaSkill] Skip disabled skill '{}'",
meta.effective_skill_id()
));
return Ok(());
}
validate_luaskills_identifier(meta.effective_skill_id(), "skill_id")
.map_err(|error| format!("skill {}: {}", meta.name, error))?;
validate_luaskills_version(meta.version(), "version")
.map_err(|error| format!("skill {}: {}", meta.effective_skill_id(), error))?;
if meta.entries.is_empty() {
return Err(format!("skill {} must declare at least one entry", meta.name).into());
}
for tool in meta.entries() {
validate_luaskills_identifier(tool.name.trim(), "entry.name").map_err(|error| {
format!("skill {} entry {}: {}", meta.name, tool.name.trim(), error)
})?;
if tool.lua_entry.trim().is_empty() || tool.lua_module.trim().is_empty() {
return Err(format!(
"skill {} declares entry {} but lua_entry/lua_module is missing",
meta.name, tool.name
)
.into());
}
validate_skill_relative_path(&tool.lua_entry, "runtime", "entry.lua_entry")
.map_err(|error| format!("skill {} entry {}: {}", meta.name, tool.name, error))?;
let lua_path = tool_entry_path(dir, tool);
if !lua_path.exists() {
return Err(format!(
"Lua entry {} not found in {}",
tool.lua_entry,
dir.display()
)
.into());
}
}
if !meta.help.main.file.trim().is_empty() {
validate_skill_relative_path(&meta.help.main.file, "help", "help.main.file")
.map_err(|error| format!("skill {} help main: {}", meta.name, error))?;
}
for topic in &meta.help.topics {
validate_skill_relative_path(&topic.file, "help", "help.topic.file").map_err(
|error| {
format!(
"skill {} help topic {}: {}",
meta.name,
topic.name.trim(),
error
)
},
)?;
}
let effective_lancedb = meta.effective_lancedb();
let lancedb_binding = if effective_lancedb.enable {
if self.lancedb_host.is_none() {
self.lancedb_host = Some(Arc::new(
LanceDbSkillHost::new(
self.host_options.as_ref().clone(),
self.database_provider_callbacks.clone(),
)
.map_err(|error| {
format!("Failed to initialize LanceDB skill host: {}", error)
})?,
));
}
let host = self
.lancedb_host
.as_ref()
.ok_or("LanceDB skill host missing after initialization")?
.clone();
Some(
host.register_skill(root_name, meta.effective_skill_id(), dir, effective_lancedb)
.map_err(|error| {
format!(
"Failed to register LanceDB for skill {}: {}",
meta.effective_skill_id(),
error
)
})?,
)
} else {
None
};
let effective_sqlite = meta.effective_sqlite();
let sqlite_binding = if effective_sqlite.enable {
if self.sqlite_host.is_none() {
self.sqlite_host = Some(Arc::new(
SqliteSkillHost::new(
self.host_options.as_ref().clone(),
self.database_provider_callbacks.clone(),
)
.map_err(|error| {
format!("Failed to initialize SQLite skill host: {}", error)
})?,
));
}
let host = self
.sqlite_host
.as_ref()
.ok_or("SQLite skill host missing after initialization")?
.clone();
Some(
host.register_skill(root_name, meta.effective_skill_id(), dir, effective_sqlite)
.map_err(|error| {
format!(
"Failed to register SQLite for skill {}: {}",
meta.effective_skill_id(),
error
)
})?,
)
} else {
None
};
self.skills.insert(
meta.effective_skill_id().to_string(),
LoadedSkill {
meta,
dir: dir.to_path_buf(),
root_name: root_name.to_string(),
lancedb_binding,
sqlite_binding,
resolved_entry_names: HashMap::new(),
},
);
Ok(())
}
fn create_vm_with_runtime_state(
&self,
skills: HashMap<String, LoadedSkill>,
entry_registry: BTreeMap<String, ResolvedEntryTarget>,
) -> Result<LuaVm, String> {
let skills = Arc::new(skills);
let entry_registry = Arc::new(entry_registry);
let lua = unsafe { Lua::unsafe_new() };
Self::setup_package_paths(&lua, self.host_options.as_ref())
.map_err(|error| error.to_string())?;
Self::register_vulcan_module(
&lua,
self.host_options.as_ref(),
self.skill_config_store.clone(),
&self.runtime_skill_roots,
)
.map_err(|error| error.to_string())?;
Self::populate_vulcan_luaexec_bridge(
&lua,
self.host_options.clone(),
self.runlua_pool.clone(),
self.skill_config_store.clone(),
skills.clone(),
entry_registry.clone(),
self.runtime_skill_roots.clone(),
self.lancedb_host.clone(),
self.sqlite_host.clone(),
)?;
Self::register_skill_functions(&lua, skills.as_ref())?;
Self::populate_vulcan_call_for_lua(
&lua,
skills.as_ref(),
entry_registry.as_ref(),
self.host_options.clone(),
self.lancedb_host.clone(),
self.sqlite_host.clone(),
)?;
Ok(LuaVm {
lua,
last_used_at: Instant::now(),
})
}
fn create_vm(&self) -> Result<LuaVm, String> {
self.create_vm_with_runtime_state(self.skills.clone(), self.entry_registry.clone())
}
fn acquire_vm(&self) -> Result<LuaVmLease, String> {
self.pool.acquire(|| self.create_vm())
}
fn create_runlua_vm(
skills: &HashMap<String, LoadedSkill>,
entry_registry: &BTreeMap<String, ResolvedEntryTarget>,
host_options: Arc<LuaRuntimeHostOptions>,
skill_config_store: Arc<SkillConfigStore>,
runtime_skill_roots: Vec<RuntimeSkillRoot>,
lancedb_host: Option<Arc<LanceDbSkillHost>>,
sqlite_host: Option<Arc<SqliteSkillHost>>,
) -> Result<LuaVm, String> {
let lua = unsafe { Lua::unsafe_new() };
Self::setup_package_paths(&lua, host_options.as_ref())
.map_err(|error| error.to_string())?;
Self::register_vulcan_module(
&lua,
host_options.as_ref(),
skill_config_store,
&runtime_skill_roots,
)
.map_err(|error| error.to_string())?;
Self::register_skill_functions(&lua, skills)?;
Self::populate_vulcan_call_for_lua(
&lua,
skills,
entry_registry,
host_options,
lancedb_host,
sqlite_host,
)?;
Ok(LuaVm {
lua,
last_used_at: Instant::now(),
})
}
fn register_skill_functions(
lua: &Lua,
skills: &HashMap<String, LoadedSkill>,
) -> Result<(), String> {
for skill in skills.values() {
for tool in skill.meta.entries() {
Self::compile_skill_into_lua(lua, skill, tool, false)?;
}
}
Ok(())
}
fn compile_skill_into_lua(
lua: &Lua,
skill: &LoadedSkill,
tool: &crate::lua_skill::SkillToolMeta,
always_reload: bool,
) -> Result<(), String> {
let lua_path = tool_entry_path(&skill.dir, tool);
let source = std::fs::read_to_string(&lua_path)
.map_err(|error| format!("Failed to read {}: {}", lua_path.display(), error))?;
if always_reload {
log_info(format!(
"[LuaSkill] Hot reload {}: {}",
tool.lua_module,
render_log_friendly_path(&lua_path)
));
}
let chunk = lua.load(&source).set_name(&tool.lua_module);
let outer: Function = chunk.into_function().map_err(|error| {
format!(
"Failed to compile skill '{}::{}': {}",
skill.meta.name, tool.lua_module, error
)
})?;
let handler: Function = outer.call(()).map_err(|error| {
format!(
"Failed to initialize skill '{}::{}': {}",
skill.meta.name, tool.lua_module, error
)
})?;
lua.globals()
.set(format!("__skill_{}", tool.lua_module), handler)
.map_err(|error| {
format!(
"Failed to register skill '{}::{}': {}",
skill.meta.name, tool.lua_module, error
)
})?;
Ok(())
}
pub fn list_entries(&self) -> Vec<RuntimeEntryDescriptor> {
self.entry_registry
.values()
.filter_map(|target| {
let skill = self.skills.get(&target.skill_storage_key)?;
let tool = skill.meta.find_tool_by_local_name(&target.local_name)?;
Some(RuntimeEntryDescriptor {
canonical_name: target.canonical_name.clone(),
skill_id: target.skill_id.clone(),
local_name: target.local_name.clone(),
root_name: skill.root_name.clone(),
skill_dir: skill.dir.display().to_string(),
description: tool.description.clone(),
parameters: tool
.parameters
.iter()
.map(|parameter| RuntimeEntryParameterDescriptor {
name: parameter.name.clone(),
param_type: parameter.param_type.clone(),
description: parameter.description.clone(),
required: parameter.required,
})
.collect(),
})
})
.collect()
}
pub fn list_entries_for_authority(
&self,
authority: SkillManagementAuthority,
) -> Vec<RuntimeEntryDescriptor> {
self.list_entries()
.into_iter()
.filter(|entry| {
authority == SkillManagementAuthority::System
|| Self::normalized_skill_root_name(&entry.root_name) != "ROOT"
})
.collect()
}
pub fn list_skill_help(&self) -> Vec<RuntimeSkillHelpDescriptor> {
let mut descriptors = self
.skills
.values()
.map(|skill| RuntimeSkillHelpDescriptor {
skill_id: skill.meta.effective_skill_id().to_string(),
skill_name: skill.meta.name.clone(),
skill_version: skill.meta.version().to_string(),
root_name: skill.root_name.clone(),
skill_dir: skill.dir.display().to_string(),
main: self.build_help_node_descriptor(skill, skill.meta.main_help(), true),
flows: skill
.meta
.help_topics()
.map(|topic| self.build_help_node_descriptor(skill, topic, false))
.collect::<Vec<RuntimeHelpNodeDescriptor>>(),
})
.collect::<Vec<RuntimeSkillHelpDescriptor>>();
descriptors.sort_by(|left, right| left.skill_id.cmp(&right.skill_id));
descriptors
}
pub fn list_skill_help_for_authority(
&self,
authority: SkillManagementAuthority,
) -> Vec<RuntimeSkillHelpDescriptor> {
self.list_skill_help()
.into_iter()
.filter(|help| {
authority == SkillManagementAuthority::System
|| Self::normalized_skill_root_name(&help.root_name) != "ROOT"
})
.collect()
}
pub fn render_skill_help_detail(
&self,
skill_id: &str,
flow_name: &str,
request_context: Option<&RuntimeRequestContext>,
) -> Result<Option<RuntimeHelpDetail>, String> {
let Some(skill) = self
.skills
.values()
.find(|skill| skill.meta.effective_skill_id() == skill_id)
else {
return Ok(None);
};
let normalized_flow_name = flow_name.trim();
if normalized_flow_name.is_empty() {
return Err("Help flow name must not be empty".to_string());
}
let (selected_help, is_main) = if normalized_flow_name == "main" {
(skill.meta.main_help(), true)
} else {
(
skill
.meta
.find_help_topic(normalized_flow_name)
.ok_or_else(|| {
format!(
"Skill '{}' does not declare help flow '{}'",
skill.meta.effective_skill_id(),
normalized_flow_name
)
})?,
false,
)
};
let rendered_body =
self.render_help_payload(skill, &selected_help.file, request_context)?;
let descriptor = self.build_help_node_descriptor(skill, selected_help, is_main);
Ok(Some(RuntimeHelpDetail {
skill_id: skill.meta.effective_skill_id().to_string(),
skill_name: skill.meta.name.clone(),
skill_version: skill.meta.version().to_string(),
root_name: skill.root_name.clone(),
skill_dir: skill.dir.display().to_string(),
flow_name: descriptor.flow_name,
description: descriptor.description,
related_entries: descriptor.related_entries,
is_main: descriptor.is_main,
content_type: "markdown".to_string(),
content: rendered_body,
}))
}
pub fn render_skill_help_detail_for_authority(
&self,
authority: SkillManagementAuthority,
skill_id: &str,
flow_name: &str,
request_context: Option<&RuntimeRequestContext>,
) -> Result<Option<RuntimeHelpDetail>, String> {
if authority == SkillManagementAuthority::DelegatedTool
&& self
.skills
.values()
.find(|skill| skill.meta.effective_skill_id() == skill_id)
.map(|skill| Self::normalized_skill_root_name(&skill.root_name) == "ROOT")
.unwrap_or(false)
{
return Ok(None);
}
self.render_skill_help_detail(skill_id, flow_name, request_context)
}
fn build_help_node_descriptor(
&self,
skill: &LoadedSkill,
help_node: &crate::lua_skill::SkillHelpNodeMeta,
is_main: bool,
) -> RuntimeHelpNodeDescriptor {
let flow_name = if is_main {
"main".to_string()
} else {
help_node.name.trim().to_string()
};
let related_entries = if is_main {
skill
.meta
.entries()
.filter_map(|entry| {
skill
.resolved_tool_name(entry.name.trim())
.map(str::to_string)
})
.collect::<Vec<String>>()
} else {
skill
.meta
.entries_for_help_topic(help_node.name.trim())
.filter_map(|entry| {
skill
.resolved_tool_name(entry.name.trim())
.map(str::to_string)
})
.collect::<Vec<String>>()
};
RuntimeHelpNodeDescriptor {
flow_name,
description: help_node.description.trim().to_string(),
related_entries,
is_main,
}
}
pub fn prompt_argument_completions(
&self,
prompt_name: &str,
argument_name: &str,
) -> Option<Vec<String>> {
let _ = prompt_name;
let _ = argument_name;
None
}
pub fn prompt_argument_completions_for_authority(
&self,
authority: SkillManagementAuthority,
prompt_name: &str,
argument_name: &str,
) -> Option<Vec<String>> {
let _ = authority;
self.prompt_argument_completions(prompt_name, argument_name)
}
fn entry_target_visible_to_authority(
&self,
authority: SkillManagementAuthority,
target: &ResolvedEntryTarget,
) -> bool {
if authority == SkillManagementAuthority::System {
return true;
}
self.skills
.get(&target.skill_storage_key)
.map(|skill| Self::normalized_skill_root_name(&skill.root_name) != "ROOT")
.unwrap_or(false)
}
pub fn is_skill(&self, name: &str) -> bool {
self.entry_registry.contains_key(name)
}
pub fn is_skill_for_authority(&self, authority: SkillManagementAuthority, name: &str) -> bool {
self.entry_registry
.get(name)
.map(|target| self.entry_target_visible_to_authority(authority, target))
.unwrap_or(false)
}
pub fn skill_name_for_tool(&self, tool_name: &str) -> Option<String> {
self.entry_registry
.get(tool_name)
.map(|target| target.skill_id.clone())
}
pub fn skill_name_for_tool_for_authority(
&self,
authority: SkillManagementAuthority,
tool_name: &str,
) -> Option<String> {
self.entry_registry.get(tool_name).and_then(|target| {
self.entry_target_visible_to_authority(authority, target)
.then(|| target.skill_id.clone())
})
}
pub fn list_skill_config_entries(
&self,
skill_id: Option<&str>,
) -> Result<Vec<SkillConfigEntry>, String> {
self.skill_config_store.list_entries(skill_id)
}
pub fn get_skill_config_value(
&self,
skill_id: &str,
key: &str,
) -> Result<Option<String>, String> {
self.skill_config_store.get_value(skill_id, key)
}
pub fn set_skill_config_value(
&mut self,
skill_id: &str,
key: &str,
value: &str,
) -> Result<(), String> {
self.skill_config_store.set_value(skill_id, key, value)
}
pub fn delete_skill_config_value(&mut self, skill_id: &str, key: &str) -> Result<bool, String> {
self.skill_config_store.delete_value(skill_id, key)
}
fn populate_vulcan_request_context(
lua: &Lua,
invocation_context: Option<&LuaInvocationContext>,
) -> Result<(), String> {
let context_table = get_vulcan_context_table(lua)?;
let request_context =
invocation_context.and_then(|context| context.request_context.as_ref());
let context_value = match request_context {
Some(context) => serde_json::to_value(context)
.map_err(|error| format!("Failed to serialize request context: {}", error))?,
None => Value::Object(serde_json::Map::new()),
};
let context_lua = json_value_to_lua(lua, &context_value)
.map_err(|error| format!("Failed to convert request context to Lua: {}", error))?;
let client_info_value = match &context_value {
Value::Object(object) => object.get("client_info").cloned().unwrap_or(Value::Null),
_ => Value::Null,
};
let client_capabilities_value = match &context_value {
Value::Object(object) => object
.get("client_capabilities")
.cloned()
.unwrap_or_else(|| Value::Object(serde_json::Map::new())),
_ => Value::Object(serde_json::Map::new()),
};
let client_info_lua = json_value_to_lua(lua, &client_info_value)
.map_err(|error| format!("Failed to convert client_info to Lua: {}", error))?;
let client_capabilities_lua = json_value_to_lua(lua, &client_capabilities_value)
.map_err(|error| format!("Failed to convert client_capabilities to Lua: {}", error))?;
let client_budget_value = invocation_context
.map(|context| context.client_budget.clone())
.unwrap_or_else(|| Value::Object(serde_json::Map::new()));
let client_budget_lua = json_value_to_lua(lua, &client_budget_value)
.map_err(|error| format!("Failed to convert client_budget to Lua: {}", error))?;
let tool_config_value = invocation_context
.map(|context| context.tool_config.clone())
.unwrap_or_else(|| Value::Object(serde_json::Map::new()));
let tool_config_lua = json_value_to_lua(lua, &tool_config_value)
.map_err(|error| format!("Failed to convert tool_config to Lua: {}", error))?;
let host_result_capability = resolve_host_result_capability(invocation_context);
let host_result_value = host_result_capability_to_json_value(&host_result_capability);
let host_result_lua = json_value_to_lua(lua, &host_result_value)
.map_err(|error| format!("Failed to convert host_result helper to Lua: {}", error))?;
context_table
.set("request", context_lua)
.map_err(|error| format!("Failed to set vulcan.context.request: {}", error))?;
context_table
.set("client_info", client_info_lua)
.map_err(|error| format!("Failed to set vulcan.context.client_info: {}", error))?;
context_table
.set("client_capabilities", client_capabilities_lua)
.map_err(|error| {
format!(
"Failed to set vulcan.context.client_capabilities: {}",
error
)
})?;
context_table
.set("client_budget", client_budget_lua)
.map_err(|error| format!("Failed to set vulcan.context.client_budget: {}", error))?;
context_table
.set("tool_config", tool_config_lua)
.map_err(|error| format!("Failed to set vulcan.context.tool_config: {}", error))?;
context_table
.set("host_result", host_result_lua)
.map_err(|error| format!("Failed to set vulcan.context.host_result: {}", error))?;
Ok(())
}
fn populate_vulcan_lancedb_context(
lua: &Lua,
binding: Option<Arc<LanceDbSkillBinding>>,
current_skill_name: Option<&str>,
) -> Result<(), String> {
let vulcan: Table = lua
.globals()
.get("vulcan")
.map_err(|error| format!("Failed to get vulcan module: {}", error))?;
let lancedb_table = lua
.create_table()
.map_err(|error| format!("Failed to create vulcan.lancedb table: {}", error))?;
let current_skill = current_skill_name.unwrap_or("");
vulcan
.set("__lancedb_skill_name", current_skill)
.map_err(|error| format!("Failed to set vulcan.__lancedb_skill_name: {}", error))?;
if let Some(binding) = binding {
lancedb_table
.set("enabled", true)
.map_err(|error| format!("Failed to set vulcan.lancedb.enabled: {}", error))?;
let info_binding = binding.clone();
let info_fn = lua
.create_function(move |lua, ()| {
json_value_to_lua(lua, &info_binding.info_json()).map_err(mlua::Error::external)
})
.map_err(|error| format!("Failed to create vulcan.lancedb.info: {}", error))?;
lancedb_table
.set("info", info_fn)
.map_err(|error| format!("Failed to set vulcan.lancedb.info: {}", error))?;
let status_binding = binding.clone();
let status_fn = lua
.create_function(move |lua, ()| {
json_value_to_lua(lua, &status_binding.status_json())
.map_err(mlua::Error::external)
})
.map_err(|error| format!("Failed to create vulcan.lancedb.status: {}", error))?;
lancedb_table
.set("status", status_fn)
.map_err(|error| format!("Failed to set vulcan.lancedb.status: {}", error))?;
let create_binding = binding.clone();
let create_table_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table = require_table_arg(input, "lancedb.create_table", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = create_binding
.create_table_json(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!("Failed to create vulcan.lancedb.create_table: {}", error)
})?;
lancedb_table
.set("create_table", create_table_fn)
.map_err(|error| format!("Failed to set vulcan.lancedb.create_table: {}", error))?;
let upsert_binding = binding.clone();
let vector_upsert_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table = require_table_arg(input, "lancedb.vector_upsert", "input")?;
let mut input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let input_object = input_json.as_object_mut().ok_or_else(|| {
mlua::Error::runtime("lancedb.vector_upsert input must be an object")
})?;
let payload_value = if let Some(rows) = input_object.remove("rows") {
input_object
.entry("input_format".to_string())
.or_insert_with(|| Value::String("json".to_string()));
rows
} else if let Some(data) = input_object.remove("data") {
data
} else {
return Err(mlua::Error::runtime(
"lancedb.vector_upsert requires rows or data",
));
};
let payload_bytes = match payload_value {
Value::String(text) => {
if !input_object.contains_key("input_format") {
input_object.insert(
"input_format".to_string(),
Value::String("arrow_ipc".to_string()),
);
}
text.into_bytes()
}
Value::Array(_) | Value::Object(_) => {
if !input_object.contains_key("input_format") {
input_object.insert(
"input_format".to_string(),
Value::String("json".to_string()),
);
}
serde_json::to_vec(&payload_value).map_err(|error| {
mlua::Error::runtime(format!(
"failed to encode lancedb upsert payload: {}",
error
))
})?
}
_ => {
return Err(mlua::Error::runtime(
"lancedb.vector_upsert payload must be string",
));
}
};
let result = upsert_binding
.vector_upsert_json(&input_json, &payload_bytes)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!("Failed to create vulcan.lancedb.vector_upsert: {}", error)
})?;
lancedb_table
.set("vector_upsert", vector_upsert_fn)
.map_err(|error| {
format!("Failed to set vulcan.lancedb.vector_upsert: {}", error)
})?;
let search_binding = binding.clone();
let vector_search_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table = require_table_arg(input, "lancedb.vector_search", "input")?;
let mut input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let input_object = input_json.as_object_mut().ok_or_else(|| {
mlua::Error::runtime("lancedb.vector_search input must be an object")
})?;
input_object
.entry("output_format".to_string())
.or_insert_with(|| Value::String("json".to_string()));
let (meta, raw_bytes) = search_binding
.vector_search_json(&input_json)
.map_err(mlua::Error::runtime)?;
let result_table =
json_to_lua_table_inner(lua, &meta).map_err(mlua::Error::external)?;
if meta
.get("format")
.and_then(Value::as_str)
.map(|value| value == "json")
.unwrap_or(false)
{
let rows_json: Value =
serde_json::from_slice(&raw_bytes).map_err(|error| {
mlua::Error::runtime(format!(
"failed to parse LanceDB JSON rows: {}",
error
))
})?;
result_table
.set(
"data_json",
json_value_to_lua(lua, &rows_json)
.map_err(mlua::Error::external)?,
)
.map_err(mlua::Error::external)?;
} else {
result_table
.set(
"data",
LuaValue::String(
lua.create_string(&raw_bytes)
.map_err(mlua::Error::external)?,
),
)
.map_err(mlua::Error::external)?;
}
Ok(LuaValue::Table(result_table))
})
.map_err(|error| {
format!("Failed to create vulcan.lancedb.vector_search: {}", error)
})?;
lancedb_table
.set("vector_search", vector_search_fn)
.map_err(|error| {
format!("Failed to set vulcan.lancedb.vector_search: {}", error)
})?;
let delete_binding = binding.clone();
let delete_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table = require_table_arg(input, "lancedb.delete", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = delete_binding
.delete_json(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| format!("Failed to create vulcan.lancedb.delete: {}", error))?;
lancedb_table
.set("delete", delete_fn)
.map_err(|error| format!("Failed to set vulcan.lancedb.delete: {}", error))?;
let drop_binding = binding;
let drop_table_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table = require_table_arg(input, "lancedb.drop_table", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = drop_binding
.drop_table_json(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!("Failed to create vulcan.lancedb.drop_table: {}", error)
})?;
lancedb_table
.set("drop_table", drop_table_fn)
.map_err(|error| format!("Failed to set vulcan.lancedb.drop_table: {}", error))?;
} else {
let disabled_status = disabled_skill_status_json(current_skill_name);
lancedb_table
.set("enabled", false)
.map_err(|error| format!("Failed to set vulcan.lancedb.enabled: {}", error))?;
let status_value = disabled_status.clone();
let status_fn = lua
.create_function(move |lua, ()| {
json_value_to_lua(lua, &status_value).map_err(mlua::Error::external)
})
.map_err(|error| {
format!("Failed to create disabled vulcan.lancedb.status: {}", error)
})?;
lancedb_table
.set("status", status_fn)
.map_err(|error| format!("Failed to set vulcan.lancedb.status: {}", error))?;
let info_value = disabled_status.clone();
let info_fn = lua
.create_function(move |lua, ()| {
json_value_to_lua(lua, &info_value).map_err(mlua::Error::external)
})
.map_err(|error| {
format!("Failed to create disabled vulcan.lancedb.info: {}", error)
})?;
lancedb_table.set("info", info_fn).map_err(|error| {
format!("Failed to set disabled vulcan.lancedb.info: {}", error)
})?;
let disabled_error = "current skill has not enabled lancedb".to_string();
for method_name in [
"create_table",
"vector_upsert",
"vector_search",
"delete",
"drop_table",
] {
let error_text = disabled_error.clone();
let fn_value = lua
.create_function(move |_, _: MultiValue| {
Err::<LuaValue, _>(mlua::Error::runtime(error_text.clone()))
})
.map_err(|error| {
format!("Failed to create disabled vulcan.lancedb proxy: {}", error)
})?;
lancedb_table.set(method_name, fn_value).map_err(|error| {
format!("Failed to set disabled method {}: {}", method_name, error)
})?;
}
}
vulcan
.set("lancedb", lancedb_table)
.map_err(|error| format!("Failed to set vulcan.lancedb: {}", error))?;
Ok(())
}
fn populate_vulcan_sqlite_context(
lua: &Lua,
binding: Option<Arc<SqliteSkillBinding>>,
current_skill_name: Option<&str>,
) -> Result<(), String> {
let vulcan: Table = lua
.globals()
.get("vulcan")
.map_err(|error| format!("Failed to get vulcan module: {}", error))?;
let sqlite_table = lua
.create_table()
.map_err(|error| format!("Failed to create vulcan.sqlite table: {}", error))?;
let current_skill = current_skill_name.unwrap_or("");
vulcan
.set("__sqlite_skill_name", current_skill)
.map_err(|error| format!("Failed to set vulcan.__sqlite_skill_name: {}", error))?;
if let Some(binding) = binding {
sqlite_table
.set("enabled", true)
.map_err(|error| format!("Failed to set vulcan.sqlite.enabled: {}", error))?;
let info_binding = binding.clone();
let info_fn = lua
.create_function(move |lua, ()| {
json_value_to_lua(lua, &info_binding.info_json()).map_err(mlua::Error::external)
})
.map_err(|error| format!("Failed to create vulcan.sqlite.info: {}", error))?;
sqlite_table
.set("info", info_fn)
.map_err(|error| format!("Failed to set vulcan.sqlite.info: {}", error))?;
let status_binding = binding.clone();
let status_fn = lua
.create_function(move |lua, ()| {
json_value_to_lua(lua, &status_binding.status_json())
.map_err(mlua::Error::external)
})
.map_err(|error| format!("Failed to create vulcan.sqlite.status: {}", error))?;
sqlite_table
.set("status", status_fn)
.map_err(|error| format!("Failed to set vulcan.sqlite.status: {}", error))?;
let tokenize_binding = binding.clone();
let tokenize_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table = require_table_arg(input, "sqlite.tokenize_text", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = tokenize_binding
.tokenize_text_json(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!("Failed to create vulcan.sqlite.tokenize_text: {}", error)
})?;
sqlite_table
.set("tokenize_text", tokenize_fn)
.map_err(|error| format!("Failed to set vulcan.sqlite.tokenize_text: {}", error))?;
let execute_script_binding = binding.clone();
let execute_script_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table = require_table_arg(input, "sqlite.execute_script", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = execute_script_binding
.execute_script(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!("Failed to create vulcan.sqlite.execute_script: {}", error)
})?;
sqlite_table
.set("execute_script", execute_script_fn)
.map_err(|error| {
format!("Failed to set vulcan.sqlite.execute_script: {}", error)
})?;
let execute_batch_binding = binding.clone();
let execute_batch_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table = require_table_arg(input, "sqlite.execute_batch", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = execute_batch_binding
.execute_batch(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!("Failed to create vulcan.sqlite.execute_batch: {}", error)
})?;
sqlite_table
.set("execute_batch", execute_batch_fn)
.map_err(|error| format!("Failed to set vulcan.sqlite.execute_batch: {}", error))?;
let query_json_binding = binding.clone();
let query_json_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table = require_table_arg(input, "sqlite.query_json", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = query_json_binding
.query_json(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| format!("Failed to create vulcan.sqlite.query_json: {}", error))?;
sqlite_table
.set("query_json", query_json_fn)
.map_err(|error| format!("Failed to set vulcan.sqlite.query_json: {}", error))?;
let query_stream_binding = binding.clone();
let query_stream_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table = require_table_arg(input, "sqlite.query_stream", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = query_stream_binding
.query_stream(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!("Failed to create vulcan.sqlite.query_stream: {}", error)
})?;
sqlite_table
.set("query_stream", query_stream_fn)
.map_err(|error| format!("Failed to set vulcan.sqlite.query_stream: {}", error))?;
let query_stream_wait_metrics_binding = binding.clone();
let query_stream_wait_metrics_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table =
require_table_arg(input, "sqlite.query_stream_wait_metrics", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = query_stream_wait_metrics_binding
.query_stream_wait_metrics(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!(
"Failed to create vulcan.sqlite.query_stream_wait_metrics: {}",
error
)
})?;
sqlite_table
.set("query_stream_wait_metrics", query_stream_wait_metrics_fn)
.map_err(|error| {
format!(
"Failed to set vulcan.sqlite.query_stream_wait_metrics: {}",
error
)
})?;
let query_stream_chunk_binding = binding.clone();
let query_stream_chunk_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table =
require_table_arg(input, "sqlite.query_stream_chunk", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = query_stream_chunk_binding
.query_stream_chunk(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!(
"Failed to create vulcan.sqlite.query_stream_chunk: {}",
error
)
})?;
sqlite_table
.set("query_stream_chunk", query_stream_chunk_fn)
.map_err(|error| {
format!("Failed to set vulcan.sqlite.query_stream_chunk: {}", error)
})?;
let query_stream_close_binding = binding.clone();
let query_stream_close_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table =
require_table_arg(input, "sqlite.query_stream_close", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = query_stream_close_binding
.query_stream_close(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!(
"Failed to create vulcan.sqlite.query_stream_close: {}",
error
)
})?;
sqlite_table
.set("query_stream_close", query_stream_close_fn)
.map_err(|error| {
format!("Failed to set vulcan.sqlite.query_stream_close: {}", error)
})?;
let upsert_word_binding = binding.clone();
let upsert_word_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table =
require_table_arg(input, "sqlite.upsert_custom_word", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = upsert_word_binding
.upsert_custom_word_json(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!(
"Failed to create vulcan.sqlite.upsert_custom_word: {}",
error
)
})?;
sqlite_table
.set("upsert_custom_word", upsert_word_fn)
.map_err(|error| {
format!("Failed to set vulcan.sqlite.upsert_custom_word: {}", error)
})?;
let remove_word_binding = binding.clone();
let remove_word_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table =
require_table_arg(input, "sqlite.remove_custom_word", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = remove_word_binding
.remove_custom_word_json(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!(
"Failed to create vulcan.sqlite.remove_custom_word: {}",
error
)
})?;
sqlite_table
.set("remove_custom_word", remove_word_fn)
.map_err(|error| {
format!("Failed to set vulcan.sqlite.remove_custom_word: {}", error)
})?;
let list_words_binding = binding.clone();
let list_words_fn = lua
.create_function(move |lua, ()| {
let result = list_words_binding
.list_custom_words_json()
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!(
"Failed to create vulcan.sqlite.list_custom_words: {}",
error
)
})?;
sqlite_table
.set("list_custom_words", list_words_fn)
.map_err(|error| {
format!("Failed to set vulcan.sqlite.list_custom_words: {}", error)
})?;
let ensure_index_binding = binding.clone();
let ensure_index_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table = require_table_arg(input, "sqlite.ensure_fts_index", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = ensure_index_binding
.ensure_fts_index_json(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!("Failed to create vulcan.sqlite.ensure_fts_index: {}", error)
})?;
sqlite_table
.set("ensure_fts_index", ensure_index_fn)
.map_err(|error| {
format!("Failed to set vulcan.sqlite.ensure_fts_index: {}", error)
})?;
let rebuild_index_binding = binding.clone();
let rebuild_index_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table =
require_table_arg(input, "sqlite.rebuild_fts_index", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = rebuild_index_binding
.rebuild_fts_index_json(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!(
"Failed to create vulcan.sqlite.rebuild_fts_index: {}",
error
)
})?;
sqlite_table
.set("rebuild_fts_index", rebuild_index_fn)
.map_err(|error| {
format!("Failed to set vulcan.sqlite.rebuild_fts_index: {}", error)
})?;
let upsert_doc_binding = binding.clone();
let upsert_doc_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table =
require_table_arg(input, "sqlite.upsert_fts_document", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = upsert_doc_binding
.upsert_fts_document_json(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!(
"Failed to create vulcan.sqlite.upsert_fts_document: {}",
error
)
})?;
sqlite_table
.set("upsert_fts_document", upsert_doc_fn)
.map_err(|error| {
format!("Failed to set vulcan.sqlite.upsert_fts_document: {}", error)
})?;
let delete_doc_binding = binding.clone();
let delete_doc_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table =
require_table_arg(input, "sqlite.delete_fts_document", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = delete_doc_binding
.delete_fts_document_json(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| {
format!(
"Failed to create vulcan.sqlite.delete_fts_document: {}",
error
)
})?;
sqlite_table
.set("delete_fts_document", delete_doc_fn)
.map_err(|error| {
format!("Failed to set vulcan.sqlite.delete_fts_document: {}", error)
})?;
let search_binding = binding;
let search_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table = require_table_arg(input, "sqlite.search_fts", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let result = search_binding
.search_fts_json(&input_json)
.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
})
.map_err(|error| format!("Failed to create vulcan.sqlite.search_fts: {}", error))?;
sqlite_table
.set("search_fts", search_fn)
.map_err(|error| format!("Failed to set vulcan.sqlite.search_fts: {}", error))?;
} else {
let disabled_status = disabled_sqlite_skill_status_json(current_skill_name);
sqlite_table
.set("enabled", false)
.map_err(|error| format!("Failed to set vulcan.sqlite.enabled: {}", error))?;
let status_value = disabled_status.clone();
let status_fn = lua
.create_function(move |lua, ()| {
json_value_to_lua(lua, &status_value).map_err(mlua::Error::external)
})
.map_err(|error| {
format!("Failed to create disabled vulcan.sqlite.status: {}", error)
})?;
sqlite_table
.set("status", status_fn)
.map_err(|error| format!("Failed to set vulcan.sqlite.status: {}", error))?;
let info_value = disabled_status.clone();
let info_fn = lua
.create_function(move |lua, ()| {
json_value_to_lua(lua, &info_value).map_err(mlua::Error::external)
})
.map_err(|error| {
format!("Failed to create disabled vulcan.sqlite.info: {}", error)
})?;
sqlite_table
.set("info", info_fn)
.map_err(|error| format!("Failed to set disabled vulcan.sqlite.info: {}", error))?;
let disabled_error = "current skill has not enabled sqlite".to_string();
for method_name in [
"execute_script",
"execute_batch",
"query_json",
"query_stream",
"query_stream_wait_metrics",
"query_stream_chunk",
"query_stream_close",
"tokenize_text",
"upsert_custom_word",
"remove_custom_word",
"list_custom_words",
"ensure_fts_index",
"rebuild_fts_index",
"upsert_fts_document",
"delete_fts_document",
"search_fts",
] {
let error_text = disabled_error.clone();
let fn_value = lua
.create_function(move |_, _: MultiValue| {
Err::<LuaValue, _>(mlua::Error::runtime(error_text.clone()))
})
.map_err(|error| {
format!("Failed to create disabled vulcan.sqlite proxy: {}", error)
})?;
sqlite_table.set(method_name, fn_value).map_err(|error| {
format!("Failed to set disabled method {}: {}", method_name, error)
})?;
}
}
vulcan
.set("sqlite", sqlite_table)
.map_err(|error| format!("Failed to set vulcan.sqlite: {}", error))?;
Ok(())
}
pub fn call_skill(
&self,
tool_name: &str,
args: &Value,
invocation_context: Option<&LuaInvocationContext>,
) -> Result<RuntimeInvocationResult, String> {
let resolved_target = self
.entry_registry
.get(tool_name)
.ok_or_else(|| format!("Lua skill '{}' not found", tool_name))?;
let skill = self
.skills
.get(&resolved_target.skill_storage_key)
.ok_or_else(|| format!("Lua skill '{}' not found", tool_name))?;
let tool = skill
.meta
.find_tool_by_local_name(&resolved_target.local_name)
.ok_or_else(|| format!("Lua skill '{}' not found", tool_name))?;
let display_tool_name = resolved_target.canonical_name.clone();
let module_name = tool.lua_module.clone();
let func_name = format!("__skill_{}", module_name);
let mut lease = self.acquire_vm()?;
let scope_guard = LuaVmRequestScopeGuard::new(&mut lease, self.host_options.as_ref())?;
let lua = scope_guard.lua();
if skill.meta.debug {
Self::compile_skill_into_lua(lua, skill, tool, true)?;
}
Self::populate_vulcan_request_context(lua, invocation_context)?;
populate_vulcan_internal_execution_context(
lua,
&VulcanInternalExecutionContext {
tool_name: Some(display_tool_name.clone()),
skill_name: Some(skill.meta.effective_skill_id().to_string()),
entry_name: Some(resolved_target.local_name.clone()),
root_name: Some(skill.root_name.clone()),
luaexec_active: false,
luaexec_caller_tool_name: None,
},
)?;
let entry_path = tool_entry_path(&skill.dir, tool);
populate_vulcan_file_context(lua, Some(&skill.dir), Some(&entry_path))?;
populate_vulcan_dependency_context(
lua,
self.host_options.as_ref(),
Some(&skill.dir),
Some(skill.meta.effective_skill_id()),
)?;
Self::populate_vulcan_lancedb_context(
lua,
skill.lancedb_binding.clone(),
Some(skill.meta.effective_skill_id()),
)?;
Self::populate_vulcan_sqlite_context(
lua,
skill.sqlite_binding.clone(),
Some(skill.meta.effective_skill_id()),
)?;
let handler: Function = lua
.globals()
.get(func_name.as_str())
.map_err(|e| format!("Skill function '{}' not found: {}", module_name, e))?;
let args_table = json_to_lua_table(lua, args)?;
let call_result = (|| {
let result: MultiValue = handler.call(args_table).map_err(|e| {
let msg = format!("Lua skill '{}' error: {}", display_tool_name, e);
log_error(format!("[LuaSkill:error] {}", msg));
msg
})?;
parse_tool_call_output(result, &display_tool_name, invocation_context).map_err(|e| {
log_error(format!("[LuaSkill:error] {}", e));
e
})
})();
let cleanup_result = scope_guard.finish();
match (call_result, cleanup_result) {
(Ok(result), Ok(())) => Ok(result),
(Ok(_), Err(cleanup_error)) => Err(cleanup_error),
(Err(call_error), Ok(())) => Err(call_error),
(Err(call_error), Err(cleanup_error)) => Err(format!(
"{}; pooled Lua VM cleanup failed: {}",
call_error, cleanup_error
)),
}
}
fn render_help_payload(
&self,
skill: &LoadedSkill,
relative_path: &str,
request_context: Option<&RuntimeRequestContext>,
) -> Result<String, String> {
if !is_lua_help_file(relative_path) {
return read_skill_text_file(&skill.dir, relative_path, "help");
}
let helper_path = skill.dir.join(relative_path);
let helper_source = std::fs::read_to_string(&helper_path).map_err(|error| {
format!(
"Failed to read help file {}: {}",
helper_path.display(),
error
)
})?;
let mut lease = self.acquire_vm()?;
let scope_guard = LuaVmRequestScopeGuard::new(&mut lease, self.host_options.as_ref())?;
let lua = scope_guard.lua();
let help_invocation_context = LuaInvocationContext::new(
request_context.cloned(),
Value::Object(serde_json::Map::new()),
Value::Object(serde_json::Map::new()),
);
Self::populate_vulcan_request_context(lua, Some(&help_invocation_context))?;
populate_vulcan_internal_execution_context(
lua,
&VulcanInternalExecutionContext {
tool_name: Some("vulcan-help".to_string()),
skill_name: Some(skill.meta.effective_skill_id().to_string()),
entry_name: Some(relative_path.to_string()),
root_name: Some(skill.root_name.clone()),
luaexec_active: false,
luaexec_caller_tool_name: None,
},
)?;
populate_vulcan_file_context(lua, Some(&skill.dir), Some(&helper_path))?;
populate_vulcan_dependency_context(
lua,
self.host_options.as_ref(),
Some(&skill.dir),
Some(skill.meta.effective_skill_id()),
)?;
Self::populate_vulcan_lancedb_context(
lua,
skill.lancedb_binding.clone(),
Some(skill.meta.effective_skill_id()),
)?;
Self::populate_vulcan_sqlite_context(
lua,
skill.sqlite_binding.clone(),
Some(skill.meta.effective_skill_id()),
)?;
let chunk_name = format!("{}-{}", skill.meta.effective_skill_id(), relative_path);
let chunk = lua.load(&helper_source).set_name(&chunk_name);
let rendered_result = (|| {
let exported: LuaValue = chunk
.into_function()
.map_err(|error| {
format!(
"Help compile error for {}: {}",
helper_path.display(),
error
)
})?
.call(())
.map_err(|error| {
format!("Help init error for {}: {}", helper_path.display(), error)
})?;
let rendered_value = match exported {
LuaValue::Function(function) => function.call(()).map_err(|error| {
format!(
"Help runtime error for {}: {}",
helper_path.display(),
error
)
})?,
other => other,
};
match rendered_value {
LuaValue::String(text) => {
text.to_str()
.map(|value| value.to_string())
.map_err(|error| {
format!(
"Help {} returned invalid UTF-8 text: {}",
helper_path.display(),
error
)
})
}
other => Err(format!(
"Help {} must return a plain string, actual_type='{}'",
helper_path.display(),
lua_value_type_name(&other)
)),
}
})();
let cleanup_result = scope_guard.finish();
match (rendered_result, cleanup_result) {
(Ok(rendered), Ok(())) => Ok(rendered),
(Ok(_), Err(cleanup_error)) => Err(cleanup_error),
(Err(render_error), Ok(())) => Err(render_error),
(Err(render_error), Err(cleanup_error)) => Err(format!(
"{}; pooled Lua VM cleanup failed: {}",
render_error, cleanup_error
)),
}
}
fn populate_vulcan_call_for_lua(
lua: &Lua,
skills_map: &HashMap<String, LoadedSkill>,
entry_registry: &BTreeMap<String, ResolvedEntryTarget>,
host_options: Arc<LuaRuntimeHostOptions>,
lancedb_host: Option<Arc<LanceDbSkillHost>>,
sqlite_host: Option<Arc<SqliteSkillHost>>,
) -> Result<(), String> {
let vulcan: Table = lua
.globals()
.get("vulcan")
.map_err(|e| format!("vulcan module not found: {}", e))?;
#[derive(Clone)]
struct DispatchEntry {
display_name: String,
module_name: String,
owner_skill_id: String,
local_name: String,
root_name: String,
owner_skill_dir: String,
entry_path: String,
}
let dispatch_entries: Vec<DispatchEntry> = entry_registry
.values()
.filter_map(|target| {
let skill = skills_map.get(&target.skill_storage_key)?;
let tool = skill.meta.find_tool_by_local_name(&target.local_name)?;
let entry_path = tool_entry_path(&skill.dir, tool);
Some(DispatchEntry {
display_name: target.canonical_name.clone(),
module_name: tool.lua_module.clone(),
owner_skill_id: target.skill_id.clone(),
local_name: target.local_name.clone(),
root_name: skill.root_name.clone(),
owner_skill_dir: skill.dir.to_string_lossy().to_string(),
entry_path: entry_path.to_string_lossy().to_string(),
})
})
.collect();
let dispatcher = lua
.create_function(move |lua, (name, args): (LuaValue, LuaValue)| {
let name = require_string_arg(name, "call", "name", false)?;
let args = require_table_arg(args, "call", "args")?;
let dispatch_entry = dispatch_entries
.iter()
.find(|entry| entry.display_name == name)
.ok_or_else(|| mlua::Error::runtime(format!("Skill '{}' not found", name)))?;
let module = &dispatch_entry.module_name;
let owner_skill_name = &dispatch_entry.owner_skill_id;
let func_name = format!("__skill_{}", module);
let func: Function = lua.globals().get(func_name.as_str()).map_err(|_| {
mlua::Error::runtime(format!("Skill function '{}' not found", module))
})?;
let nested_scope_guard = LuaNestedCallScopeGuard::new(
lua,
host_options.clone(),
lancedb_host.clone(),
sqlite_host.clone(),
)
.map_err(mlua::Error::runtime)?;
let current_request_context_json =
lua_value_to_json(&nested_scope_guard.previous_context)
.map_err(mlua::Error::runtime)?;
let current_request_context = match ¤t_request_context_json {
Value::Object(object) if object.is_empty() => None,
_ => serde_json::from_value::<RuntimeRequestContext>(
current_request_context_json,
)
.ok(),
};
let current_client_budget =
lua_value_to_json(&nested_scope_guard.previous_client_budget)
.map_err(mlua::Error::runtime)?;
let current_tool_config =
lua_value_to_json(&nested_scope_guard.previous_tool_config)
.map_err(mlua::Error::runtime)?;
if nested_scope_guard.previous_internal_context.luaexec_active {
if nested_scope_guard
.previous_internal_context
.luaexec_caller_tool_name
.as_deref()
== Some(dispatch_entry.display_name.as_str())
{
return Err(mlua::Error::runtime(format!(
"vulcan.call cannot call the current luaexec caller tool '{}'",
dispatch_entry.display_name
)));
}
if dispatch_entry.owner_skill_id == "vulcan-runtime"
&& (dispatch_entry.local_name == "lua-exec"
|| dispatch_entry.local_name == "lua-file")
{
return Err(mlua::Error::runtime(format!(
"vulcan.call cannot invoke '{}' inside luaexec",
dispatch_entry.display_name
)));
}
}
let target_binding = match lancedb_host.as_ref() {
Some(host) => host
.binding_for_skill(owner_skill_name)
.map_err(mlua::Error::runtime)?,
None => None,
};
let target_sqlite_binding = match sqlite_host.as_ref() {
Some(host) => host
.binding_for_skill(owner_skill_name)
.map_err(mlua::Error::runtime)?,
None => None,
};
let nested_invocation_context = LuaInvocationContext::new(
current_request_context,
current_client_budget,
current_tool_config,
);
nested_scope_guard
.enter_nested_call(
&dispatch_entry.display_name,
owner_skill_name,
&dispatch_entry.local_name,
&dispatch_entry.root_name,
&dispatch_entry.owner_skill_dir,
&dispatch_entry.entry_path,
&nested_invocation_context,
target_binding,
target_sqlite_binding,
)
.map_err(mlua::Error::runtime)?;
let call_result = func.call::<MultiValue>(args);
let restore_result = nested_scope_guard.finish().map_err(mlua::Error::runtime);
match (call_result, restore_result) {
(Ok(result), Ok(())) => Ok(result),
(Ok(_), Err(restore_error)) => Err(restore_error),
(Err(call_error), Ok(())) => Err(call_error),
(Err(call_error), Err(restore_error)) => Err(mlua::Error::runtime(format!(
"{}; nested vulcan.call restore failed: {}",
call_error, restore_error
))),
}
})
.map_err(|e| format!("Failed to create vulcan.call dispatcher: {}", e))?;
vulcan
.set("call", dispatcher)
.map_err(|e| format!("Failed to set vulcan.call: {}", e))?;
Ok(())
}
fn setup_package_paths(
lua: &Lua,
host_options: &LuaRuntimeHostOptions,
) -> Result<(), Box<dyn std::error::Error>> {
let Some(lua_packages) = host_options.lua_packages_dir.as_ref() else {
return Ok(());
};
if !lua_packages.exists() {
return Ok(());
}
#[cfg(windows)]
let cpath_pattern = format!(
"{}\\lib\\lua\\?.dll;{}\\lib\\lua\\?\\init.dll;{}\\lib\\lua\\loadall.dll;{}\\?\\?.dll;",
lua_packages.display(),
lua_packages.display(),
lua_packages.display(),
lua_packages.display()
);
#[cfg(target_os = "linux")]
let cpath_pattern = format!(
"{}/lib/lua/?.so;{}/lib/lua/?/init.so;{}/lib/lua/loadall.so;{}/?.so;",
lua_packages.display(),
lua_packages.display(),
lua_packages.display(),
lua_packages.display()
);
#[cfg(target_os = "macos")]
let cpath_pattern = format!(
"{}/lib/lua/?.dylib;{}/lib/lua/?/init.dylib;{}/lib/lua/loadall.dylib;{}/?.dylib;",
lua_packages.display(),
lua_packages.display(),
lua_packages.display(),
lua_packages.display()
);
#[cfg(windows)]
let path_pattern = format!(
"{}\\share\\lua\\?.lua;{}\\share\\lua\\?\\init.lua;{}\\?.lua;",
lua_packages.display(),
lua_packages.display(),
lua_packages.display()
);
#[cfg(unix)]
let path_pattern = format!(
"{}/share/lua/?.lua;{}/share/lua/?/init.lua;{}/?.lua;",
lua_packages.display(),
lua_packages.display(),
lua_packages.display()
);
let package: Table = lua.globals().get("package")?;
let old_cpath: mlua::String = package.get("cpath")?;
let new_cpath = format!("{}{}", cpath_pattern, old_cpath.to_str()?.to_string());
package.set("cpath", lua.create_string(&new_cpath)?)?;
let old_path: mlua::String = package.get("path")?;
let new_path = format!("{}{}", path_pattern, old_path.to_str()?.to_string());
package.set("path", lua.create_string(&new_path)?)?;
Ok(())
}
fn register_vulcan_module(
lua: &Lua,
host_options: &LuaRuntimeHostOptions,
skill_config_store: Arc<SkillConfigStore>,
runtime_skill_roots: &[RuntimeSkillRoot],
) -> Result<(), Box<dyn std::error::Error>> {
let vulcan = lua.create_table()?;
let runtime = lua.create_table()?;
let runtime_skills = lua.create_table()?;
let runtime_internal = lua.create_table()?;
let runtime_lua = lua.create_table()?;
let fs = lua.create_table()?;
let path = lua.create_table()?;
let process = lua.create_table()?;
let os = lua.create_table()?;
let json = lua.create_table()?;
let cache = lua.create_table()?;
let config = lua.create_table()?;
let host = lua.create_table()?;
let models = lua.create_table()?;
let context = lua.create_table()?;
let deps = lua.create_table()?;
let default_text_encoding = resolve_host_default_text_encoding(host_options)?;
let vulcan_io = create_vulcan_io_table(lua, default_text_encoding)?;
let runtime_log_fn = lua.create_function(|_, (level, msg): (LuaValue, LuaValue)| {
let level = require_string_arg(level, "runtime.log", "level", false)?;
let msg = require_string_arg(msg, "runtime.log", "message", true)?;
let normalized_level = level.trim().to_ascii_lowercase();
let rendered = format!("[LuaSkill:{}] {}", level, msg);
if normalized_level.contains("error") || normalized_level.contains("fatal") {
log_error(rendered);
} else if normalized_level.contains("warn") {
log_warn(rendered);
} else {
log_info(rendered);
}
Ok(())
})?;
runtime.set("log", runtime_log_fn)?;
let print_fn = lua.create_function(|_, args: MultiValue| {
let mut parts = Vec::new();
for val in args.into_iter() {
let s = match val {
LuaValue::String(s) => s.to_str().map(|b| b.to_string()).unwrap_or_default(),
LuaValue::Integer(i) => i.to_string(),
LuaValue::Number(f) => f.to_string(),
LuaValue::Boolean(b) => b.to_string(),
LuaValue::Nil => "nil".to_string(),
_ => format!("{:?}", val),
};
parts.push(s);
}
log_info(format!("[LuaSkill:info] {}", parts.join("\t")));
Ok(())
})?;
lua.globals().set("print", print_fn)?;
let fs_list_fn = lua.create_function(|_, dir: LuaValue| {
let dir = require_path_arg(dir, "fs.list", "dir")?;
let mut entries = Vec::new();
for entry in std::fs::read_dir(&dir)
.map_err(|e| mlua::Error::runtime(format!("fs.list: {}", e)))?
{
let entry = entry.map_err(|e| mlua::Error::runtime(format!("fs.list: {}", e)))?;
let file_name = entry.file_name().into_string().map_err(|name| {
mlua::Error::runtime(format!(
"fs.list: non-UTF-8 file name under {}: {:?}",
Path::new(&dir).display(),
name
))
})?;
entries.push(file_name);
}
Ok(entries)
})?;
fs.set("list", fs_list_fn)?;
let fs_read_fn = lua.create_function(|_, path: LuaValue| {
let path = require_path_arg(path, "fs.read", "path")?;
std::fs::read_to_string(&path)
.map_err(|e| mlua::Error::runtime(format!("fs.read: {}", e)))
})?;
fs.set("read", fs_read_fn)?;
let fs_write_fn = lua.create_function(|_, (path, content): (LuaValue, LuaValue)| {
let path = require_path_arg(path, "fs.write", "path")?;
let content = require_string_arg(content, "fs.write", "content", true)?;
std::fs::write(&path, content)
.map_err(|e| mlua::Error::runtime(format!("fs.write: {}", e)))
})?;
fs.set("write", fs_write_fn)?;
let fs_exists_fn = lua.create_function(|_, path: LuaValue| {
let path = require_path_arg(path, "fs.exists", "path")?;
Ok(Path::new(&path).exists())
})?;
fs.set("exists", fs_exists_fn)?;
let fs_is_dir_fn = lua.create_function(|_, path: LuaValue| {
let path = require_path_arg(path, "fs.is_dir", "path")?;
Ok(Path::new(&path).is_dir())
})?;
fs.set("is_dir", fs_is_dir_fn)?;
let path_join_fn = lua.create_function(|lua, parts: MultiValue| {
if parts.is_empty() {
return Err(mlua::Error::runtime(
"path.join: expected at least one path segment",
));
}
let mut joined = PathBuf::new();
for (index, val) in parts.into_iter().enumerate() {
let param_name = format!("part[{}]", index + 1);
let part = require_path_arg(val, "path.join", ¶m_name)?;
joined.push(part);
}
let result = render_host_visible_path(&joined);
lua.create_string(&result)
})?;
path.set("join", path_join_fn)?;
let cwd_fn = lua.create_function(|lua, ()| {
let current_dir = std::env::current_dir()
.map_err(|error| mlua::Error::runtime(format!("runtime.cwd: {}", error)))?;
let current_dir_text = render_host_visible_path(¤t_dir);
lua.create_string(¤t_dir_text)
})?;
runtime.set("cwd", cwd_fn)?;
match host_options.temp_dir.as_ref() {
Some(path_buf) => runtime.set("temp_dir", render_host_visible_path(path_buf))?,
None => runtime.set("temp_dir", LuaValue::Nil)?,
}
match host_options.resources_dir.as_ref() {
Some(path_buf) => runtime.set("resources_dir", render_host_visible_path(path_buf))?,
None => runtime.set("resources_dir", LuaValue::Nil)?,
}
let exec_default_encoding = default_text_encoding;
let exec_fn = lua.create_function(move |lua, spec: LuaValue| {
let request = parse_exec_request(spec, "process.exec", exec_default_encoding)?;
let result = execute_exec_request(request);
exec_result_to_lua_table(lua, result)
})?;
process.set("exec", exec_fn)?;
process.set(
"session",
create_process_session_table(lua, default_text_encoding)?,
)?;
let os_info_fn = lua.create_function(|lua, ()| {
let current_os = match std::env::consts::OS {
"windows" => "windows",
"linux" => "linux",
"macos" => "macos",
_ => std::env::consts::OS,
};
let arch = match std::env::consts::ARCH {
"x86_64" => "x86_64",
"x86" => "i686",
"aarch64" => "aarch64",
"arm" => "armv7l",
_ => std::env::consts::ARCH,
};
let info = lua.create_table()?;
info.set("os", current_os)?;
info.set("arch", arch)?;
Ok(info)
})?;
os.set("info", os_info_fn)?;
let json_encode_fn =
lua.create_function(|lua, val: LuaValue| match lua_value_to_json(&val) {
Ok(value) => lua.create_string(serde_json::to_string(&value).unwrap_or_default()),
Err(error) => Err(mlua::Error::runtime(format!("json.encode: {}", error))),
})?;
json.set("encode", json_encode_fn)?;
let json_decode_fn = lua.create_function(|lua, s: LuaValue| {
let s = require_string_arg(s, "json.decode", "text", false)?;
match serde_json::from_str::<Value>(&s) {
Ok(value) => json_value_to_lua(lua, &value),
Err(error) => Err(mlua::Error::runtime(format!("json.decode: {}", error))),
}
})?;
json.set("decode", json_decode_fn)?;
let cache_put_fn = lua.create_function(|lua, (value, ttl_sec): (LuaValue, LuaValue)| {
let internal = get_vulcan_runtime_internal_table(lua).map_err(mlua::Error::runtime)?;
let tool_name: Option<String> =
internal.get("tool_name").map_err(mlua::Error::runtime)?;
let skill_name: Option<String> =
internal.get("skill_name").map_err(mlua::Error::runtime)?;
let scope = tool_name
.or(skill_name)
.unwrap_or_else(|| "__runtime".to_string());
let ttl_secs = optional_u64_arg(ttl_sec, "cache.put", "ttl_sec")?;
let payload = lua_value_to_json(&value)
.map_err(|error| mlua::Error::runtime(format!("cache.put: {}", error)))?;
Ok(global_tool_cache().create(&scope, payload, ttl_secs))
})?;
cache.set("put", cache_put_fn)?;
let cache_get_fn = lua.create_function(|lua, cache_id: LuaValue| {
let internal = get_vulcan_runtime_internal_table(lua).map_err(mlua::Error::runtime)?;
let tool_name: Option<String> =
internal.get("tool_name").map_err(mlua::Error::runtime)?;
let skill_name: Option<String> =
internal.get("skill_name").map_err(mlua::Error::runtime)?;
let scope = tool_name
.or(skill_name)
.unwrap_or_else(|| "__runtime".to_string());
let cache_id = require_string_arg(cache_id, "cache.get", "cache_id", false)?;
match global_tool_cache().get(&scope, &cache_id) {
Some(value) => json_value_to_lua(lua, &value),
None => Ok(LuaValue::Nil),
}
})?;
cache.set("get", cache_get_fn)?;
let cache_delete_fn = lua.create_function(|lua, cache_id: LuaValue| {
let internal = get_vulcan_runtime_internal_table(lua).map_err(mlua::Error::runtime)?;
let tool_name: Option<String> =
internal.get("tool_name").map_err(mlua::Error::runtime)?;
let skill_name: Option<String> =
internal.get("skill_name").map_err(mlua::Error::runtime)?;
let scope = tool_name
.or(skill_name)
.unwrap_or_else(|| "__runtime".to_string());
let cache_id = require_string_arg(cache_id, "cache.delete", "cache_id", false)?;
Ok(global_tool_cache().delete(&scope, &cache_id))
})?;
cache.set("delete", cache_delete_fn)?;
let config_get_store = skill_config_store.clone();
let config_get_fn = lua.create_function(move |lua, key: LuaValue| {
let key = require_string_arg(key, "config.get", "key", false)?;
let skill_id = current_vulcan_config_skill_id(lua, "vulcan.config.get")?;
match config_get_store
.get_value(&skill_id, &key)
.map_err(mlua::Error::runtime)?
{
Some(value) => Ok(LuaValue::String(
lua.create_string(&value).map_err(mlua::Error::runtime)?,
)),
None => Ok(LuaValue::Nil),
}
})?;
config.set("get", config_get_fn)?;
let config_has_store = skill_config_store.clone();
let config_has_fn = lua.create_function(move |lua, key: LuaValue| {
let key = require_string_arg(key, "config.has", "key", false)?;
let skill_id = current_vulcan_config_skill_id(lua, "vulcan.config.has")?;
config_has_store
.has_value(&skill_id, &key)
.map_err(mlua::Error::runtime)
})?;
config.set("has", config_has_fn)?;
let config_set_store = skill_config_store.clone();
let config_set_fn =
lua.create_function(move |lua, (key, value): (LuaValue, LuaValue)| {
let key = require_string_arg(key, "config.set", "key", false)?;
let value = require_string_arg(value, "config.set", "value", true)?;
let skill_id = current_vulcan_config_skill_id(lua, "vulcan.config.set")?;
config_set_store
.set_value(&skill_id, &key, &value)
.map_err(mlua::Error::runtime)?;
Ok(true)
})?;
config.set("set", config_set_fn)?;
let config_delete_store = skill_config_store.clone();
let config_delete_fn = lua.create_function(move |lua, key: LuaValue| {
let key = require_string_arg(key, "config.delete", "key", false)?;
let skill_id = current_vulcan_config_skill_id(lua, "vulcan.config.delete")?;
config_delete_store
.delete_value(&skill_id, &key)
.map_err(mlua::Error::runtime)
})?;
config.set("delete", config_delete_fn)?;
let config_list_store = skill_config_store.clone();
let config_list_fn = lua.create_function(move |lua, ()| {
let skill_id = current_vulcan_config_skill_id(lua, "vulcan.config.list")?;
let items = config_list_store
.list_skill_values(&skill_id)
.map_err(mlua::Error::runtime)?;
let table = lua.create_table().map_err(mlua::Error::runtime)?;
for (key, value) in items {
table
.set(
key,
LuaValue::String(lua.create_string(&value).map_err(mlua::Error::runtime)?),
)
.map_err(mlua::Error::runtime)?;
}
Ok(LuaValue::Table(table))
})?;
config.set("list", config_list_fn)?;
host.set("list", create_host_tool_list_fn(lua)?)?;
let host_has_fn = create_host_tool_has_fn(lua)?;
host.set("has", host_has_fn.clone())?;
host.set("has_tool", host_has_fn)?;
host.set("call", create_host_tool_call_fn(lua)?)?;
models.set("status", create_model_status_fn(lua)?)?;
models.set("has", create_model_has_fn(lua)?)?;
models.set("embed", create_model_embed_fn(lua)?)?;
models.set("llm", create_model_llm_fn(lua)?)?;
context.set("request", lua.create_table()?)?;
context.set("client_info", LuaValue::Nil)?;
context.set("client_capabilities", lua.create_table()?)?;
context.set("client_budget", lua.create_table()?)?;
context.set("tool_config", lua.create_table()?)?;
context.set("host_result", lua.create_table()?)?;
context.set("skill_dir", LuaValue::Nil)?;
context.set("entry_dir", LuaValue::Nil)?;
context.set("entry_file", LuaValue::Nil)?;
deps.set("tools_path", LuaValue::Nil)?;
deps.set("lua_path", LuaValue::Nil)?;
deps.set("ffi_path", LuaValue::Nil)?;
let skill_management_enabled = host_options.capabilities.enable_skill_management_bridge;
runtime_skills.set("enabled", skill_management_enabled)?;
let runtime_skills_status_fn = lua.create_function(move |lua, ()| {
let status = lua.create_table()?;
let callback_registered =
try_has_skill_management_callback().map_err(mlua::Error::runtime)?;
status.set("enabled", skill_management_enabled)?;
status.set("callback_registered", callback_registered)?;
status.set("mode", "host_callback")?;
let message = if !skill_management_enabled {
"Skill management bridge is disabled by host policy"
} else if callback_registered {
"Skill management bridge is enabled and ready"
} else {
"Skill management bridge is enabled but no host callback is registered"
};
status.set("message", message)?;
Ok(status)
})?;
runtime_skills.set("status", runtime_skills_status_fn)?;
runtime_skills.set(
"layers",
create_runtime_skill_layers_fn(lua, runtime_skill_roots, skill_management_enabled)?,
)?;
runtime_skills.set(
"install",
create_runtime_skill_management_bridge_fn(
lua,
skill_management_enabled,
RuntimeSkillManagementAction::Install,
"install",
)?,
)?;
runtime_skills.set(
"update",
create_runtime_skill_management_bridge_fn(
lua,
skill_management_enabled,
RuntimeSkillManagementAction::Update,
"update",
)?,
)?;
runtime_skills.set(
"uninstall",
create_runtime_skill_management_bridge_fn(
lua,
skill_management_enabled,
RuntimeSkillManagementAction::Uninstall,
"uninstall",
)?,
)?;
runtime_skills.set(
"enable",
create_runtime_skill_management_bridge_fn(
lua,
skill_management_enabled,
RuntimeSkillManagementAction::Enable,
"enable",
)?,
)?;
runtime_skills.set(
"disable",
create_runtime_skill_management_bridge_fn(
lua,
skill_management_enabled,
RuntimeSkillManagementAction::Disable,
"disable",
)?,
)?;
let overflow_type = lua.create_table()?;
overflow_type.set("truncate", "truncate")?;
overflow_type.set("page", "page")?;
runtime.set("overflow_type", overflow_type)?;
runtime_internal.set("tool_name", LuaValue::Nil)?;
runtime_internal.set("skill_name", LuaValue::Nil)?;
runtime_internal.set("entry_name", LuaValue::Nil)?;
runtime_internal.set("root_name", LuaValue::Nil)?;
runtime_internal.set("luaexec_active", false)?;
runtime_internal.set("luaexec_caller_tool_name", LuaValue::Nil)?;
runtime.set("internal", runtime_internal)?;
runtime.set("skills", runtime_skills)?;
runtime.set("lua", runtime_lua)?;
let call_stub = lua.create_function(|_, _: (LuaValue, LuaValue)| {
Err::<(), _>(mlua::Error::runtime("vulcan.call not initialized"))
})?;
vulcan.set("call", call_stub)?;
vulcan.set("runtime", runtime)?;
vulcan.set("fs", fs)?;
vulcan.set("io", vulcan_io)?;
vulcan.set("path", path)?;
vulcan.set("process", process)?;
vulcan.set("os", os)?;
vulcan.set("json", json)?;
vulcan.set("cache", cache)?;
vulcan.set("config", config)?;
vulcan.set("host", host)?;
vulcan.set("models", models)?;
vulcan.set("context", context)?;
vulcan.set("deps", deps)?;
lua.globals().set("vulcan", vulcan)?;
Ok(())
}
}
fn json_to_lua_table(lua: &Lua, json: &Value) -> Result<Table, String> {
json_to_lua_table_inner(lua, json).map_err(|e| e.to_string())
}
fn json_to_lua_table_inner(lua: &Lua, json: &Value) -> mlua::Result<Table> {
let table = lua.create_table()?;
if let Value::Object(obj) = json {
for (k, v) in obj {
table.set(k.as_str(), json_value_to_lua(lua, v)?)?;
}
} else if let Value::Array(arr) = json {
for (i, v) in arr.iter().enumerate() {
table.set(i + 1, json_value_to_lua(lua, v)?)?;
}
}
Ok(table)
}
fn json_value_to_lua(lua: &Lua, json: &Value) -> mlua::Result<LuaValue> {
match json {
Value::Null => Ok(LuaValue::Nil),
Value::Bool(b) => Ok(LuaValue::Boolean(*b)),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(LuaValue::Integer(i))
} else {
Ok(LuaValue::Number(n.as_f64().unwrap_or(0.0)))
}
}
Value::String(s) => Ok(LuaValue::String(lua.create_string(s)?)),
Value::Array(_) | Value::Object(_) => {
Ok(LuaValue::Table(json_to_lua_table_inner(lua, json)?))
}
}
}
fn lua_value_to_json(val: &LuaValue) -> Result<Value, String> {
match val {
LuaValue::Nil => Ok(Value::Null),
LuaValue::Boolean(b) => Ok(Value::Bool(*b)),
LuaValue::Integer(i) => Ok(Value::Number((*i).into())),
LuaValue::Number(f) => {
if let Some(n) = serde_json::Number::from_f64(*f) {
Ok(Value::Number(n))
} else {
Ok(Value::Null)
}
}
LuaValue::String(s) => Ok(Value::String(
s.to_str().map(|b| b.to_string()).unwrap_or_default(),
)),
LuaValue::Table(t) => {
if t.raw_len() > 0 {
let arr = lua_table_to_array(t)?;
Ok(Value::Array(arr))
} else {
lua_table_to_object(t)
}
}
LuaValue::Function(_) => Err("Cannot convert Lua function to JSON".to_string()),
LuaValue::Thread(_) => Err("Cannot convert Lua thread to JSON".to_string()),
LuaValue::UserData(_) => Err("Cannot convert Lua userdata to JSON".to_string()),
LuaValue::LightUserData(_) => Err("Cannot convert light userdata to JSON".to_string()),
_ => Err("Unknown Lua value type".to_string()),
}
}
fn lua_table_to_array(t: &Table) -> Result<Vec<Value>, String> {
let len = t.raw_len();
if len == 0 {
return Ok(Vec::new());
}
let mut arr = Vec::with_capacity(len);
for i in 1..=len {
let val: LuaValue = t.get(i).map_err(|e| format!("Array index {}: {}", i, e))?;
arr.push(lua_value_to_json(&val)?);
}
Ok(arr)
}
fn lua_table_to_object(t: &Table) -> Result<Value, String> {
let mut obj = serde_json::Map::new();
for pair in t.pairs::<String, LuaValue>() {
let (k, v) = pair.map_err(|e| format!("Table key: {}", e))?;
obj.insert(k, lua_value_to_json(&v)?);
}
if obj.is_empty() && t.raw_len() == 0 {
return Ok(Value::Array(Vec::new()));
}
Ok(Value::Object(obj))
}
#[cfg(test)]
mod tests;