use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use mlua::{
AnyUserData, 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::ffi::OsStr;
use std::fmt::Display;
use std::fs;
use std::hash::{Hash, Hasher};
use std::io::{BufRead, BufReader, ErrorKind, Read, Write};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Component, Path, PathBuf};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, TryLockError, Weak, mpsc};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
#[cfg(windows)]
use windows_sys::Win32::System::LibraryLoader::{
AddDllDirectory, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, LOAD_LIBRARY_SEARCH_USER_DIRS,
RemoveDllDirectory, SetDefaultDllDirectories,
};
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,
RuntimeSkillOperationProgressEmitter, 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::managed_package::{
ManagedRuntimeOwnerState, ManagedRuntimePackageContext, ManagedRuntimePackageIdentity,
current_lua_managed_package_context, optional_lua_managed_package_context,
replace_lua_managed_package_context, retire_managed_runtime_owner_state,
};
use crate::runtime::managed_runtime::{
ManagedRuntimeEnvPlan, ManagedRuntimeRoots,
current_managed_runtime_persistent_session_capability, ensure_managed_env,
managed_env_is_ready, resolve_node_env_plan, resolve_python_env_plan,
};
use crate::runtime::managed_runtime_services::{
ManagedRuntimeServices, ManagedRuntimeSessionEventIdentity, ManagedRuntimeTransactionContext,
};
#[cfg(test)]
use crate::runtime::managed_runtime_session::prepare_unleased_managed_package_snapshot;
use crate::runtime::managed_runtime_session::{
ManagedPackageSnapshot, ManagedRuntimeSessionOpenRequest,
configure_managed_node_command_environment, configure_managed_python_command_environment,
ensure_persistent_managed_runtime_session_platform_supported, launch_managed_node_session,
launch_managed_python_session, prepare_managed_package_snapshot,
};
use crate::runtime::managed_session_events::{
ManagedSessionEventCenter, RuntimeManagedSessionEventBatch, RuntimeManagedSessionWakeCallback,
};
use crate::runtime::path::render_host_visible_path;
use crate::runtime::process_session::{
DetachedChildReaperPermit, ManagedChildProcessTree, create_managed_process_session_userdata,
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, LuaRuntimeManagedRuntimeConfig, RuntimeSkillRoot,
};
use crate::runtime_result::RuntimeInvocationResult;
use crate::skill::dependencies::PackageDependencyManifest;
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::{
RunLuaRuntimeContext, default_exec_shell_name, 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, supported_exec_shell_names,
};
#[derive(Clone)]
struct LoadedSkill {
meta: SkillMeta,
dir: std::path::PathBuf,
root_name: String,
managed_package: Arc<ManagedRuntimePackageContext>,
lancedb_binding: Option<Arc<LanceDbSkillBinding>>,
sqlite_binding: Option<Arc<SqliteSkillBinding>>,
resolved_entry_names: HashMap<String, String>,
}
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,
}
#[derive(Debug, Clone, Copy)]
enum PackagedRuntimeTargetKind {
File,
Directory,
}
impl PackagedRuntimeTargetKind {
fn matches_metadata(self, metadata: &fs::Metadata) -> bool {
match self {
Self::File => metadata.is_file(),
Self::Directory => metadata.is_dir(),
}
}
fn diagnostic_noun(self) -> &'static str {
match self {
Self::File => "file",
Self::Directory => "directory",
}
}
}
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,
expected_kind: PackagedRuntimeTargetKind,
) -> Result<(), String> {
validate_runtime_relative_manifest_path(label, relative_path)?;
let candidate = runtime_root.join(relative_path);
if !packaged_runtime_target_exists(&candidate, label, expected_kind)? {
return Err(format!(
"packaged runtime is invalid: missing {}",
render_log_friendly_path(&candidate)
));
}
Ok(())
}
fn packaged_runtime_target_exists(
path: &Path,
label: &str,
expected_kind: PackagedRuntimeTargetKind,
) -> Result<bool, String> {
match fs::metadata(path) {
Ok(metadata) if expected_kind.matches_metadata(&metadata) => Ok(true),
Ok(_) => Err(format!(
"packaged runtime is invalid: {} is not a {}: {}",
label,
expected_kind.diagnostic_noun(),
render_log_friendly_path(path)
)),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!(
"packaged runtime is invalid: failed to inspect {} '{}': {}",
label,
render_log_friendly_path(path),
error
)),
}
}
fn validate_packaged_runtime_packages_layout(resources_dir: &Path) -> Result<(), String> {
let runtime_manifest_path = resources_dir.join("lua-runtime-manifest.json");
if !packaged_runtime_target_exists(
&runtime_manifest_path,
"lua-runtime-manifest",
PackagedRuntimeTargetKind::File,
)? {
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 !packaged_runtime_target_exists(
&packages_manifest_path,
"luaskills-packages-manifest",
PackagedRuntimeTargetKind::File,
)? {
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,
PackagedRuntimeTargetKind::File,
)?;
validate_packaged_runtime_target(
runtime_root,
"compat_lua_packages_txt",
&manifest.paths.compat_lua_packages_txt,
PackagedRuntimeTargetKind::File,
)?;
validate_packaged_runtime_target(
runtime_root,
"platform_support",
&manifest.paths.platform_support,
PackagedRuntimeTargetKind::File,
)?;
validate_packaged_runtime_target(
runtime_root,
"third_party_licenses",
&manifest.paths.third_party_licenses,
PackagedRuntimeTargetKind::File,
)?;
validate_packaged_runtime_target(
runtime_root,
"third_party_notices",
&manifest.paths.third_party_notices,
PackagedRuntimeTargetKind::File,
)?;
validate_packaged_runtime_target(
runtime_root,
"help_index",
&manifest.paths.help_index,
PackagedRuntimeTargetKind::File,
)?;
validate_packaged_runtime_target(
runtime_root,
"package_help_root",
&manifest.paths.package_help_root,
PackagedRuntimeTargetKind::Directory,
)?;
validate_packaged_runtime_target(
runtime_root,
"module_help_root",
&manifest.paths.module_help_root,
PackagedRuntimeTargetKind::Directory,
)?;
validate_packaged_runtime_target(
runtime_root,
"license_index",
&manifest.paths.license_index,
PackagedRuntimeTargetKind::File,
)?;
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 RunLuaVmBuildContext<'a> {
skills: &'a HashMap<String, LoadedSkill>,
entry_registry: &'a 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>>,
managed_runtime_services: Arc<ManagedRuntimeServices>,
managed_runtime_workers: Arc<ManagedRuntimeWorkerService>,
}
impl<'a> RunLuaVmBuildContext<'a> {
fn from_engine(
engine: &LuaEngine,
skills: &'a HashMap<String, LoadedSkill>,
entry_registry: &'a BTreeMap<String, ResolvedEntryTarget>,
) -> Self {
Self {
skills,
entry_registry,
host_options: Arc::clone(&engine.host_options),
skill_config_store: Arc::clone(&engine.skill_config_store),
runtime_skill_roots: engine.runtime_skill_roots.clone(),
lancedb_host: engine.lancedb_host.clone(),
sqlite_host: engine.sqlite_host.clone(),
managed_runtime_services: Arc::clone(&engine.managed_runtime_services),
managed_runtime_workers: Arc::clone(&engine.managed_runtime_workers),
}
}
}
struct LuaVmPoolState {
available: Vec<LuaVm>,
total_count: usize,
}
struct LuaVmPool {
config: LuaVmPoolConfig,
state: Mutex<LuaVmPoolState>,
condvar: Condvar,
}
#[derive(Debug, Default)]
struct NativeLibrarySearchGuard {
#[cfg(windows)]
directories: Vec<NativeLibraryDirectoryCookie>,
}
#[cfg(windows)]
#[derive(Debug)]
struct NativeLibraryDirectoryCookie(*mut core::ffi::c_void);
#[cfg(windows)]
impl NativeLibraryDirectoryCookie {
fn is_valid(&self) -> bool {
!self.0.is_null()
}
}
#[cfg(windows)]
unsafe impl Send for NativeLibraryDirectoryCookie {}
#[cfg(windows)]
unsafe impl Sync for NativeLibraryDirectoryCookie {}
#[cfg(windows)]
impl Drop for NativeLibrarySearchGuard {
fn drop(&mut self) {
self.directories.clear();
}
}
#[cfg(windows)]
impl Drop for NativeLibraryDirectoryCookie {
fn drop(&mut self) {
if self.is_valid() {
unsafe {
RemoveDllDirectory(self.0);
}
}
}
}
impl NativeLibrarySearchGuard {
fn new(host_options: &LuaRuntimeHostOptions) -> Result<Self, String> {
#[cfg(windows)]
{
Self::new_windows(host_options)
}
#[cfg(not(windows))]
{
let _ = host_options;
Ok(Self::default())
}
}
#[cfg(windows)]
fn new_windows(host_options: &LuaRuntimeHostOptions) -> Result<Self, String> {
let mut directories = Vec::new();
let Some(host_provided_ffi_root) = host_options.host_provided_ffi_root.as_ref() else {
return Ok(Self { directories });
};
let ffi_root_is_directory = host_provided_ffi_root_is_directory(host_provided_ffi_root)?;
if !ffi_root_is_directory {
return Ok(Self { directories });
}
let default_directory_result = unsafe {
SetDefaultDllDirectories(
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_USER_DIRS,
)
};
if default_directory_result == 0 {
return Err(format!(
"failed to enable Windows DLL directory search for host_provided_ffi_root: {}",
std::io::Error::last_os_error()
));
}
let wide_path = windows_wide_null_path(host_provided_ffi_root)?;
let cookie = NativeLibraryDirectoryCookie(unsafe { AddDllDirectory(wide_path.as_ptr()) });
if !cookie.is_valid() {
return Err(format!(
"failed to add Windows DLL directory {}: {}",
render_log_friendly_path(host_provided_ffi_root),
std::io::Error::last_os_error()
));
}
directories.push(cookie);
Ok(Self { directories })
}
}
#[cfg(windows)]
fn host_provided_ffi_root_is_directory(path: &Path) -> Result<bool, String> {
match fs::metadata(path) {
Ok(metadata) => Ok(metadata.is_dir()),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!(
"failed to inspect host_provided_ffi_root {}: {}",
render_log_friendly_path(path),
error
)),
}
}
fn configured_package_search_directory_exists(
path: &Path,
option_name: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
match fs::metadata(path) {
Ok(metadata) if metadata.is_dir() => Ok(true),
Ok(_) => Err(format!(
"configured {option_name} is not a directory: {}",
render_log_friendly_path(path)
)
.into()),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!(
"failed to inspect configured {option_name} {}: {}",
render_log_friendly_path(path),
error
)
.into()),
}
}
#[cfg(windows)]
fn windows_wide_null_path(path: &Path) -> Result<Vec<u16>, String> {
use std::os::windows::ffi::OsStrExt;
let mut wide_path = path.as_os_str().encode_wide().collect::<Vec<u16>>();
if wide_path.contains(&0) {
return Err(format!(
"Windows DLL directory contains an embedded NUL: {}",
render_log_friendly_path(path)
));
}
wide_path.push(0);
Ok(wide_path)
}
pub struct LuaEngine {
skills: HashMap<String, LoadedSkill>,
entry_registry: BTreeMap<String, ResolvedEntryTarget>,
runtime_skill_roots: Vec<RuntimeSkillRoot>,
pool: Arc<LuaVmPool>,
runlua_pool: Arc<LuaVmPool>,
public_runtime_sessions: Arc<RuntimeSessionManager>,
system_runtime_sessions: Arc<RuntimeSessionManager>,
managed_runtime_services: Arc<ManagedRuntimeServices>,
managed_runtime_workers: Arc<ManagedRuntimeWorkerService>,
managed_runtime_roots: Option<Arc<ManagedRuntimeRoots>>,
skill_config_store: Arc<SkillConfigStore>,
lancedb_host: Option<Arc<LanceDbSkillHost>>,
sqlite_host: Option<Arc<SqliteSkillHost>>,
database_provider_callbacks: Arc<RuntimeDatabaseProviderCallbacks>,
native_library_search_guard: NativeLibrarySearchGuard,
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,
}
}
}
fn resolve_engine_managed_runtime_roots(
host_options: &LuaRuntimeHostOptions,
) -> Result<Option<Arc<ManagedRuntimeRoots>>, String> {
match host_options.runtime_root.as_deref() {
Some(runtime_root) => Ok(Some(Arc::new(ManagedRuntimeRoots::new(
runtime_root,
host_options.managed_runtime_distribution_root.as_deref(),
host_options.managed_runtime_environment_root.as_deref(),
)?))),
None if host_options.managed_runtime_distribution_root.is_some()
|| host_options.managed_runtime_environment_root.is_some() =>
{
Err("runtime_root is required when a managed runtime root is configured".to_string())
}
None => Ok(None),
}
}
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 render_lua_print_argument(value: LuaValue) -> String {
match value {
LuaValue::String(value) => match value.to_str() {
Ok(text) => text.to_string(),
Err(error) => format!("<invalid UTF-8 Lua string: {error}>"),
},
LuaValue::Integer(value) => value.to_string(),
LuaValue::Number(value) => value.to_string(),
LuaValue::Boolean(value) => value.to_string(),
LuaValue::Nil => "nil".to_string(),
other => format!("{:?}", other),
}
}
fn parse_vulcan_fs_recursive_option(value: LuaValue, fn_name: &str) -> mlua::Result<bool> {
match value {
LuaValue::Nil => Ok(false),
other => {
let options = require_table_arg(other, fn_name, "options")?;
let recursive_value: LuaValue = options.get("recursive")?;
match recursive_value {
LuaValue::Nil => Ok(false),
LuaValue::Boolean(flag) => Ok(flag),
other => Err(mlua::Error::runtime(format!(
"{fn_name}: options.recursive must be a boolean when provided: {}",
lua_value_type_name(&other)
))),
}
}
}
}
fn parse_vulcan_fs_overwrite_option(value: LuaValue, fn_name: &str) -> mlua::Result<bool> {
match value {
LuaValue::Nil => Ok(false),
other => {
let options = require_table_arg(other, fn_name, "options")?;
let overwrite_value: LuaValue = options.get("overwrite")?;
match overwrite_value {
LuaValue::Nil => Ok(false),
LuaValue::Boolean(flag) => Ok(flag),
other => Err(mlua::Error::runtime(format!(
"{fn_name}: options.overwrite must be a boolean when provided: {}",
lua_value_type_name(&other)
))),
}
}
}
}
fn resolve_vulcan_fs_copy_absolute_path(path: &Path) -> Result<PathBuf, String> {
let cwd = std::env::current_dir().map_err(|error| format!("fs.copy: {}", error))?;
Ok(if path.is_absolute() {
normalize_runtime_root_path(path)
} else {
normalize_runtime_root_path(&cwd.join(path))
})
}
fn resolve_vulcan_fs_copy_effective_destination_path(
target: &Path,
recreate_leaf: bool,
) -> Result<PathBuf, String> {
let absolute_target = resolve_vulcan_fs_copy_absolute_path(target)?;
let mut suffix = Vec::<PathBuf>::new();
let mut cursor = if recreate_leaf {
let leaf_name = absolute_target.file_name().ok_or_else(|| {
format!(
"fs.copy: destination path must not be one filesystem root: {}",
render_log_friendly_path(&absolute_target)
)
})?;
suffix.push(PathBuf::from(leaf_name));
absolute_target.parent().ok_or_else(|| {
format!(
"fs.copy: destination path must have one parent directory: {}",
render_log_friendly_path(&absolute_target)
)
})?
} else {
absolute_target.as_path()
};
loop {
let ancestor_exists = path_entry_exists(
cursor,
&format!(
"fs.copy: failed to inspect destination ancestor {}",
render_log_friendly_path(cursor)
),
)?;
if ancestor_exists {
break;
}
let missing_name = cursor.file_name().ok_or_else(|| {
format!(
"fs.copy: destination path could not resolve one existing ancestor: {}",
render_log_friendly_path(&absolute_target)
)
})?;
suffix.push(PathBuf::from(missing_name));
cursor = cursor.parent().ok_or_else(|| {
format!(
"fs.copy: destination path must stay under one existing filesystem root: {}",
render_log_friendly_path(&absolute_target)
)
})?;
}
let mut resolved = fs::canonicalize(cursor).map_err(|error| format!("fs.copy: {}", error))?;
for component in suffix.into_iter().rev() {
resolved.push(component);
}
Ok(normalize_runtime_root_path(&resolved))
}
fn validate_vulcan_fs_copy_directory_target(
source: &Path,
target: &Path,
recreate_target_leaf: bool,
) -> Result<(), String> {
let resolved_source =
fs::canonicalize(source).map_err(|error| format!("fs.copy: {}", error))?;
let resolved_target =
resolve_vulcan_fs_copy_effective_destination_path(target, recreate_target_leaf)?;
if resolved_source == resolved_target {
return Err(format!(
"fs.copy: source and destination must differ: {}",
render_log_friendly_path(&resolved_source)
));
}
if resolved_target.starts_with(&resolved_source) {
return Err(format!(
"fs.copy: destination directory must not be inside source directory: {}",
render_log_friendly_path(&resolved_target)
));
}
Ok(())
}
fn remove_vulcan_fs_copy_target(target: &Path) -> Result<(), String> {
let metadata = fs::symlink_metadata(target).map_err(|error| format!("fs.copy: {}", error))?;
let file_type = metadata.file_type();
if file_type.is_dir() {
fs::remove_dir_all(target).map_err(|error| format!("fs.copy: {}", error))?;
} else {
fs::remove_file(target).map_err(|error| format!("fs.copy: {}", error))?;
}
Ok(())
}
fn path_entry_exists(path: &Path, error_prefix: &str) -> Result<bool, String> {
match fs::symlink_metadata(path) {
Ok(_) => Ok(true),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!("{error_prefix}: {}", error)),
}
}
fn skill_dependency_manifest_path_exists(dependencies_path: &Path) -> Result<bool, String> {
match fs::metadata(dependencies_path) {
Ok(metadata) if metadata.is_file() => Ok(true),
Ok(_) => Err(format!(
"dependency manifest is not a file: {}",
render_log_friendly_path(dependencies_path)
)),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!(
"failed to inspect dependency manifest {}: {}",
render_log_friendly_path(dependencies_path),
error
)),
}
}
fn required_skill_file_path_exists(
path: &Path,
file_label: &str,
skill_dir: &Path,
) -> Result<bool, String> {
match fs::metadata(path) {
Ok(metadata) if metadata.is_file() => Ok(true),
Ok(_) => Err(format!(
"{} is not a file for skill {}: {}",
file_label,
render_log_friendly_path(skill_dir),
render_log_friendly_path(path)
)),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!(
"failed to inspect {} {} for skill {}: {}",
file_label,
render_log_friendly_path(path),
render_log_friendly_path(skill_dir),
error
)),
}
}
fn vulcan_fs_target_exists(path: &Path, api_name: &str) -> Result<bool, String> {
match fs::metadata(path) {
Ok(_) => Ok(true),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!(
"{api_name}: failed to inspect {}: {}",
render_log_friendly_path(path),
error
)),
}
}
fn vulcan_fs_target_is_dir(path: &Path, api_name: &str) -> Result<bool, String> {
match fs::metadata(path) {
Ok(metadata) => Ok(metadata.is_dir()),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!(
"{api_name}: failed to inspect {}: {}",
render_log_friendly_path(path),
error
)),
}
}
#[derive(Debug)]
enum VulcanFsMkdirTargetStatus {
Missing,
ExistingDirectory,
ExistingNonDirectory,
}
fn vulcan_fs_mkdir_target_status(path: &Path) -> Result<VulcanFsMkdirTargetStatus, String> {
let metadata = match fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == ErrorKind::NotFound => {
return Ok(VulcanFsMkdirTargetStatus::Missing);
}
Err(error) => {
return Err(format!(
"fs.mkdir: failed to inspect {}: {}",
render_log_friendly_path(path),
error
));
}
};
if metadata.file_type().is_symlink() {
return match fs::metadata(path) {
Ok(target_metadata) if target_metadata.is_dir() => {
Ok(VulcanFsMkdirTargetStatus::ExistingDirectory)
}
Ok(_) => Ok(VulcanFsMkdirTargetStatus::ExistingNonDirectory),
Err(error) if error.kind() == ErrorKind::NotFound => {
Ok(VulcanFsMkdirTargetStatus::ExistingNonDirectory)
}
Err(error) => Err(format!(
"fs.mkdir: failed to inspect {}: {}",
render_log_friendly_path(path),
error
)),
};
}
if metadata.is_dir() {
Ok(VulcanFsMkdirTargetStatus::ExistingDirectory)
} else {
Ok(VulcanFsMkdirTargetStatus::ExistingNonDirectory)
}
}
fn copy_vulcan_fs_directory_recursive(source: &Path, target: &Path) -> Result<(), String> {
fs::create_dir_all(target).map_err(|error| format!("fs.copy: {}", error))?;
for entry in fs::read_dir(source).map_err(|error| format!("fs.copy: {}", error))? {
let entry = entry.map_err(|error| format!("fs.copy: {}", error))?;
let entry_path = entry.path();
let file_type = entry
.file_type()
.map_err(|error| format!("fs.copy: {}", error))?;
let destination = target.join(entry.file_name());
if file_type.is_symlink() {
return Err(format!(
"fs.copy: symbolic-link entries are not supported inside directory trees: {}",
render_log_friendly_path(&entry_path)
));
}
if file_type.is_dir() {
copy_vulcan_fs_directory_recursive(&entry_path, &destination)?;
} else if file_type.is_file() {
fs::copy(&entry_path, &destination).map_err(|error| format!("fs.copy: {}", error))?;
} else {
return Err(format!(
"fs.copy: unsupported entry type inside directory tree: {}",
render_log_friendly_path(&entry_path)
));
}
}
Ok(())
}
fn classify_vulcan_fs_kind(file_type: &fs::FileType) -> &'static str {
if file_type.is_file() {
"file"
} else if file_type.is_dir() {
"dir"
} else if file_type.is_symlink() {
"symlink"
} else {
"other"
}
}
fn system_time_to_unix_millis_i64(time: SystemTime, context: &str) -> Result<i64, String> {
let duration = time.duration_since(UNIX_EPOCH).map_err(|error| {
format!(
"{} is before Unix epoch and cannot be represented as modified_unix_ms: {}",
context, error
)
})?;
let millis = duration.as_millis();
Ok(if millis > i64::MAX as u128 {
i64::MAX
} else {
millis as i64
})
}
fn metadata_modified_unix_ms(metadata: &fs::Metadata, path: &Path) -> Result<i64, String> {
let path_label = render_log_friendly_path(path);
let modified = metadata.modified().map_err(|error| {
format!(
"fs.stat: failed to read modified time for {}: {}",
path_label, error
)
})?;
system_time_to_unix_millis_i64(
modified,
&format!("fs.stat modified time for {}", path_label),
)
}
fn create_vulcan_fs_stat_table(
lua: &Lua,
metadata: &fs::Metadata,
path: &Path,
) -> mlua::Result<Table> {
let file_type = metadata.file_type();
let stat = lua.create_table()?;
stat.set("kind", classify_vulcan_fs_kind(&file_type))?;
stat.set("is_file", file_type.is_file())?;
stat.set("is_dir", file_type.is_dir())?;
stat.set("is_symlink", file_type.is_symlink())?;
stat.set("readonly", metadata.permissions().readonly())?;
if file_type.is_file() {
stat.set("size", metadata.len())?;
}
let modified_unix_ms =
metadata_modified_unix_ms(metadata, path).map_err(mlua::Error::runtime)?;
stat.set("modified_unix_ms", modified_unix_ms)?;
Ok(stat)
}
fn format_vulcan_fs_list_non_utf8_file_name_error(dir: &Path, name: &OsStr) -> String {
format!(
"fs.list: non-UTF-8 file name under {}: {:?}",
render_log_friendly_path(dir),
name
)
}
fn render_vulcan_path_dirname(path: &Path) -> String {
match path.parent() {
Some(parent) if parent.as_os_str().is_empty() => ".".to_string(),
Some(parent) => render_host_visible_path(parent),
None if path.is_absolute() => render_host_visible_path(path),
None => ".".to_string(),
}
}
fn render_vulcan_normalized_path(path: &Path) -> String {
let normalized = normalize_runtime_root_path(path);
if normalized.as_os_str().is_empty() {
".".to_string()
} else {
render_host_visible_path(&normalized)
}
}
fn render_vulcan_path_component(component: Option<&OsStr>, api_name: &str) -> mlua::Result<String> {
let Some(component) = component else {
return Ok(String::new());
};
component.to_str().map(str::to_string).ok_or_else(|| {
mlua::Error::runtime(format!(
"{api_name}: path component cannot be represented as UTF-8"
))
})
}
fn is_vulcan_process_explicit_path(program: &str) -> bool {
Path::new(program).is_absolute() || program.contains('/') || program.contains('\\')
}
fn resolve_vulcan_process_search_path(path: &Path, cwd: &Path) -> PathBuf {
if path.is_absolute() {
normalize_runtime_root_path(path)
} else {
normalize_runtime_root_path(&cwd.join(path))
}
}
#[cfg(windows)]
fn default_vulcan_process_windows_pathexts() -> Vec<String> {
vec![
".com".to_string(),
".exe".to_string(),
".bat".to_string(),
".cmd".to_string(),
]
}
#[cfg(windows)]
fn parse_vulcan_process_windows_pathexts(value: &str) -> Vec<String> {
value
.split(';')
.filter_map(|entry| {
let trimmed = entry.trim();
if trimmed.is_empty() {
None
} else if trimmed.starts_with('.') {
Some(trimmed.to_ascii_lowercase())
} else {
Some(format!(".{}", trimmed).to_ascii_lowercase())
}
})
.collect::<Vec<_>>()
}
#[cfg(windows)]
fn vulcan_process_windows_pathexts() -> Result<Vec<String>, String> {
match std::env::var("PATHEXT") {
Ok(value) => Ok(parse_vulcan_process_windows_pathexts(&value)),
Err(std::env::VarError::NotPresent) => Ok(default_vulcan_process_windows_pathexts()),
Err(std::env::VarError::NotUnicode(_)) => {
Err("process.which: PATHEXT cannot be represented as UTF-8".to_string())
}
}
}
#[cfg(windows)]
fn vulcan_process_candidate_paths(base: &Path) -> Result<Vec<PathBuf>, String> {
let mut candidates = vec![base.to_path_buf()];
if base.extension().is_some() {
return Ok(candidates);
}
for ext in vulcan_process_windows_pathexts()? {
let mut candidate = base.as_os_str().to_os_string();
candidate.push(ext);
candidates.push(PathBuf::from(candidate));
}
Ok(candidates)
}
#[cfg(not(windows))]
fn vulcan_process_candidate_paths(base: &Path) -> Result<Vec<PathBuf>, String> {
Ok(vec![base.to_path_buf()])
}
#[cfg(unix)]
fn is_vulcan_process_executable(path: &Path) -> Result<bool, String> {
match fs::metadata(path) {
Ok(metadata) => Ok(metadata.is_file() && (metadata.permissions().mode() & 0o111) != 0),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!(
"process.which: failed to inspect executable candidate {}: {}",
render_log_friendly_path(path),
error
)),
}
}
#[cfg(windows)]
fn is_vulcan_process_executable(path: &Path) -> Result<bool, String> {
match fs::metadata(path) {
Ok(metadata) => Ok(metadata.is_file()),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!(
"process.which: failed to inspect executable candidate {}: {}",
render_log_friendly_path(path),
error
)),
}
}
#[cfg(not(any(unix, windows)))]
fn is_vulcan_process_executable(path: &Path) -> Result<bool, String> {
match fs::metadata(path) {
Ok(metadata) => Ok(metadata.is_file()),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!(
"process.which: failed to inspect executable candidate {}: {}",
render_log_friendly_path(path),
error
)),
}
}
fn find_vulcan_process_candidate(base: &Path) -> Result<Option<PathBuf>, String> {
for candidate in vulcan_process_candidate_paths(base)? {
if is_vulcan_process_executable(&candidate)? {
return Ok(Some(candidate));
}
}
Ok(None)
}
fn resolve_vulcan_process_which(program: &str) -> Result<Option<PathBuf>, String> {
let cwd = std::env::current_dir().map_err(|error| format!("process.which: {}", error))?;
if is_vulcan_process_explicit_path(program) {
let explicit_path = resolve_vulcan_process_search_path(Path::new(program), &cwd);
return find_vulcan_process_candidate(&explicit_path);
}
let Some(path_env) = std::env::var_os("PATH") else {
return Ok(None);
};
for search_dir in std::env::split_paths(&path_env) {
let resolved_dir = resolve_vulcan_process_search_path(&search_dir, &cwd);
let base = resolved_dir.join(program);
if let Some(found) = find_vulcan_process_candidate(&base)? {
return Ok(Some(found));
}
}
Ok(None)
}
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)
}
fn lua_skill_handler_global_name(module_name: &str) -> String {
format!("__skill_{}", module_name)
}
fn resolve_lua_skill_handler(lua: &Lua, module_name: &str) -> mlua::Result<Function> {
lua.globals()
.get(lua_skill_handler_global_name(module_name))
}
struct CallSkillLuaInvocationInput {
handler: Function,
args_table: Table,
}
fn prepare_call_skill_lua_invocation_input(
lua: &Lua,
module_name: &str,
args: &Value,
) -> Result<CallSkillLuaInvocationInput, String> {
let handler = resolve_lua_skill_handler(lua, module_name)
.map_err(|error| format!("Skill function '{}' not found: {}", module_name, error))?;
let args_table = json_to_lua_table(lua, args)?;
Ok(CallSkillLuaInvocationInput {
handler,
args_table,
})
}
fn invoke_loaded_lua_skill_handler(
handler: Function,
args_table: Table,
display_tool_name: &str,
invocation_context: Option<&LuaInvocationContext>,
) -> Result<RuntimeInvocationResult, String> {
let result: MultiValue = handler.call(args_table).map_err(|error| {
let message = format!("Lua skill '{}' error: {}", display_tool_name, error);
log_error(format!("[LuaSkill:error] {}", message));
message
})?;
parse_tool_call_output(result, display_tool_name, invocation_context).map_err(|error| {
log_error(format!("[LuaSkill:error] {}", error));
error
})
}
#[derive(Clone)]
struct LuaCallDispatchEntry {
display_name: String,
module_name: String,
owner_skill_id: String,
local_name: String,
root_name: String,
owner_skill_dir: PathBuf,
owner_managed_package: Arc<ManagedRuntimePackageContext>,
entry_path: PathBuf,
}
struct LuaCallProviderBindings {
lancedb: Option<Arc<LanceDbSkillBinding>>,
sqlite: Option<Arc<SqliteSkillBinding>>,
}
impl LuaCallDispatchEntry {
fn reject_forbidden_luaexec_call(
&self,
outer_internal_context: &VulcanInternalExecutionContext,
) -> mlua::Result<()> {
if !outer_internal_context.luaexec_active {
return Ok(());
}
if outer_internal_context.luaexec_caller_tool_name.as_deref()
== Some(self.display_name.as_str())
{
return Err(mlua::Error::runtime(format!(
"vulcan.call cannot call the current luaexec caller tool '{}'",
self.display_name
)));
}
if self.owner_skill_id == "vulcan-runtime"
&& (self.local_name == "lua-exec" || self.local_name == "lua-file")
{
return Err(mlua::Error::runtime(format!(
"vulcan.call cannot invoke '{}' inside luaexec",
self.display_name
)));
}
Ok(())
}
}
fn resolve_lua_call_provider_bindings(
owner_skill_name: &str,
lancedb_host: Option<&Arc<LanceDbSkillHost>>,
sqlite_host: Option<&Arc<SqliteSkillHost>>,
) -> Result<LuaCallProviderBindings, String> {
let lancedb = match lancedb_host {
Some(host) => host.binding_for_skill(owner_skill_name)?,
None => None,
};
let sqlite = match sqlite_host {
Some(host) => host.binding_for_skill(owner_skill_name)?,
None => None,
};
Ok(LuaCallProviderBindings { lancedb, sqlite })
}
fn build_lua_call_dispatch_entries(
skills_map: &HashMap<String, LoadedSkill>,
entry_registry: &BTreeMap<String, ResolvedEntryTarget>,
) -> Result<Vec<LuaCallDispatchEntry>, String> {
let mut dispatch_entries = Vec::with_capacity(entry_registry.len());
for target in entry_registry.values() {
let skill = skills_map.get(&target.skill_storage_key).ok_or_else(|| {
format!(
"vulcan.call registry target '{}' references missing loaded skill storage key '{}'",
target.canonical_name, target.skill_storage_key
)
})?;
let tool = skill
.meta
.find_tool_by_local_name(&target.local_name)
.ok_or_else(|| {
format!(
"vulcan.call registry target '{}' references missing local entry '{}' in skill '{}'",
target.canonical_name, target.local_name, target.skill_id
)
})?;
let entry_path = tool_entry_path(&skill.dir, tool);
dispatch_entries.push(LuaCallDispatchEntry {
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.clone(),
owner_managed_package: skill.managed_package.clone(),
entry_path,
});
}
Ok(dispatch_entries)
}
fn resolve_lua_call_dispatch_entry<'a>(
dispatch_entries: &'a [LuaCallDispatchEntry],
name: &str,
) -> mlua::Result<&'a LuaCallDispatchEntry> {
dispatch_entries
.iter()
.find(|entry| entry.display_name == name)
.ok_or_else(|| mlua::Error::runtime(format!("Skill '{}' not found", name)))
}
fn resolve_lua_call_dispatch_handler(
lua: &Lua,
dispatch_entry: &LuaCallDispatchEntry,
) -> mlua::Result<Function> {
let module = &dispatch_entry.module_name;
resolve_lua_skill_handler(lua, module)
.map_err(|_| mlua::Error::runtime(format!("Skill function '{}' not found", module)))
}
#[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 parse_runtime_request_context_json(
value: Value,
source_name: &str,
) -> Result<Option<RuntimeRequestContext>, String> {
match &value {
Value::Object(object) if object.is_empty() => Ok(None),
Value::Array(array) if array.is_empty() => Ok(None),
_ => serde_json::from_value::<RuntimeRequestContext>(value)
.map(Some)
.map_err(|error| {
format!("{source_name} is not a valid runtime request context: {error}")
}),
}
}
struct VulcanContextSnapshot {
request: LuaValue,
client_info: LuaValue,
client_capabilities: LuaValue,
client_budget: LuaValue,
tool_config: LuaValue,
host_result: LuaValue,
}
struct VulcanFileContextPath {
lua_text: String,
path: PathBuf,
}
impl VulcanFileContextPath {
fn from_lua_text(lua_text: String) -> Self {
let path = PathBuf::from(&lua_text);
Self { lua_text, path }
}
fn lua_text(&self) -> &str {
&self.lua_text
}
fn path(&self) -> &Path {
&self.path
}
}
struct VulcanFileContextSnapshot {
skill_dir: Option<VulcanFileContextPath>,
entry_dir: Option<VulcanFileContextPath>,
entry_file: Option<VulcanFileContextPath>,
}
struct LoadedSkillLuaContext<'a> {
display_tool_name: &'a str,
entry_name: &'a str,
entry_path: &'a Path,
invocation_context: Option<&'a LuaInvocationContext>,
}
struct CallSkillInvocationTarget<'a> {
skill: &'a LoadedSkill,
tool: &'a crate::lua_skill::SkillToolMeta,
display_tool_name: &'a str,
local_entry_name: &'a str,
}
enum AnonymousLuaDependencyContext<'a> {
ClearWithHostOptions(&'a LuaRuntimeHostOptions),
PreserveCurrent,
}
enum AnonymousLuaManagedPackageContext<'a> {
Clear,
Set(&'a Arc<ManagedRuntimePackageContext>),
PreserveCurrent,
}
struct AnonymousLuaExecutionContext<'a> {
invocation_context: Option<&'a LuaInvocationContext>,
internal_context: VulcanInternalExecutionContext,
entry_file: Option<&'a Path>,
dependency_context: AnonymousLuaDependencyContext<'a>,
managed_package_context: AnonymousLuaManagedPackageContext<'a>,
}
struct LuaNestedCallTarget<'a> {
display_name: &'a str,
owner_skill_name: &'a str,
owner_local_name: &'a str,
owner_root_name: &'a str,
owner_skill_dir: &'a Path,
owner_managed_package: &'a Arc<ManagedRuntimePackageContext>,
entry_path: &'a Path,
invocation_context: &'a LuaInvocationContext,
lancedb_binding: Option<Arc<LanceDbSkillBinding>>,
sqlite_binding: Option<Arc<SqliteSkillBinding>>,
}
fn build_lua_nested_call_target<'a>(
dispatch_entry: &'a LuaCallDispatchEntry,
invocation_context: &'a LuaInvocationContext,
provider_bindings: LuaCallProviderBindings,
) -> LuaNestedCallTarget<'a> {
LuaNestedCallTarget {
display_name: &dispatch_entry.display_name,
owner_skill_name: &dispatch_entry.owner_skill_id,
owner_local_name: &dispatch_entry.local_name,
owner_root_name: &dispatch_entry.root_name,
owner_skill_dir: &dispatch_entry.owner_skill_dir,
owner_managed_package: &dispatch_entry.owner_managed_package,
entry_path: &dispatch_entry.entry_path,
invocation_context,
lancedb_binding: provider_bindings.lancedb,
sqlite_binding: provider_bindings.sqlite,
}
}
fn build_lua_nested_internal_execution_context(
target: &LuaNestedCallTarget<'_>,
outer_internal_context: &VulcanInternalExecutionContext,
) -> VulcanInternalExecutionContext {
VulcanInternalExecutionContext {
tool_name: Some(target.display_name.to_string()),
skill_name: Some(target.owner_skill_name.to_string()),
entry_name: Some(target.owner_local_name.to_string()),
root_name: Some(target.owner_root_name.to_string()),
luaexec_active: outer_internal_context.luaexec_active,
luaexec_caller_tool_name: outer_internal_context.luaexec_caller_tool_name.clone(),
}
}
fn populate_lua_nested_resource_contexts(
lua: &Lua,
host_options: &LuaRuntimeHostOptions,
target: LuaNestedCallTarget<'_>,
) -> Result<(), String> {
replace_lua_managed_package_context(lua, Some(target.owner_managed_package.clone()));
populate_vulcan_file_context(lua, Some(target.owner_skill_dir), Some(target.entry_path))?;
populate_vulcan_dependency_context(
lua,
host_options,
Some(target.owner_skill_dir),
Some(target.owner_skill_name),
)?;
LuaEngine::populate_vulcan_lancedb_context(
lua,
target.lancedb_binding,
Some(target.owner_skill_name),
)?;
LuaEngine::populate_vulcan_sqlite_context(
lua,
target.sqlite_binding,
Some(target.owner_skill_name),
)?;
Ok(())
}
struct SkillLifecycleEventDraft<'a> {
plane: SkillOperationPlane,
action: crate::skill::manager::SkillLifecycleAction,
skill_id: &'a str,
root_name: Option<String>,
skill_dir: Option<String>,
status: &'a str,
message: Option<String>,
}
fn format_lifecycle_recovery_error<R, S>(
base_message: String,
rollback_result: Result<(), R>,
restore_result: Result<(), S>,
) -> String
where
R: Display,
S: Display,
{
let mut message = base_message;
if let Err(error) = rollback_result {
message.push_str(&format!(". rollback failed: {}", error));
}
if let Err(error) = restore_result {
message.push_str(&format!(". runtime restore failed: {}", error));
}
message
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SkillApplyLifecycleAction {
Install,
Update,
}
impl SkillApplyLifecycleAction {
fn from_lifecycle_action(
action: crate::skill::manager::SkillLifecycleAction,
) -> Result<Self, String> {
match action {
crate::skill::manager::SkillLifecycleAction::Install => Ok(Self::Install),
crate::skill::manager::SkillLifecycleAction::Update => Ok(Self::Update),
unsupported => Err(format!("unsupported apply action {:?}", unsupported)),
}
}
}
fn capture_vulcan_context_snapshot_field(
context: &Table,
field_name: &str,
) -> Result<LuaValue, String> {
context
.get(field_name)
.map_err(|error| format!("Failed to read vulcan.context.{}: {}", field_name, error))
}
fn capture_vulcan_context_snapshot(lua: &Lua) -> Result<VulcanContextSnapshot, String> {
let context = get_vulcan_context_table(lua)?;
Ok(VulcanContextSnapshot {
request: capture_vulcan_context_snapshot_field(&context, "request")?,
client_info: capture_vulcan_context_snapshot_field(&context, "client_info")?,
client_capabilities: capture_vulcan_context_snapshot_field(
&context,
"client_capabilities",
)?,
client_budget: capture_vulcan_context_snapshot_field(&context, "client_budget")?,
tool_config: capture_vulcan_context_snapshot_field(&context, "tool_config")?,
host_result: capture_vulcan_context_snapshot_field(&context, "host_result")?,
})
}
fn restore_vulcan_context_snapshot_field(
context: &Table,
field_name: &str,
value: &LuaValue,
) -> Result<(), String> {
context
.set(field_name, value.clone())
.map_err(|error| format!("Failed to restore vulcan.context.{}: {}", field_name, error))
}
fn restore_vulcan_context_snapshot(
lua: &Lua,
snapshot: &VulcanContextSnapshot,
) -> Result<(), String> {
let context = get_vulcan_context_table(lua)?;
restore_vulcan_context_snapshot_field(&context, "request", &snapshot.request)?;
restore_vulcan_context_snapshot_field(&context, "client_info", &snapshot.client_info)?;
restore_vulcan_context_snapshot_field(
&context,
"client_capabilities",
&snapshot.client_capabilities,
)?;
restore_vulcan_context_snapshot_field(&context, "client_budget", &snapshot.client_budget)?;
restore_vulcan_context_snapshot_field(&context, "tool_config", &snapshot.tool_config)?;
restore_vulcan_context_snapshot_field(&context, "host_result", &snapshot.host_result)?;
Ok(())
}
fn capture_vulcan_file_context(lua: &Lua) -> Result<VulcanFileContextSnapshot, String> {
let context = get_vulcan_context_table(lua)?;
Ok(VulcanFileContextSnapshot {
skill_dir: capture_vulcan_file_context_path(&context, "skill_dir")?,
entry_dir: capture_vulcan_file_context_path(&context, "entry_dir")?,
entry_file: capture_vulcan_file_context_path(&context, "entry_file")?,
})
}
fn capture_vulcan_file_context_path(
context: &Table,
field_name: &str,
) -> Result<Option<VulcanFileContextPath>, String> {
let lua_text: Option<String> = context
.get(field_name)
.map_err(|error| format!("Failed to read vulcan.context.{}: {}", field_name, error))?;
Ok(lua_text.map(VulcanFileContextPath::from_lua_text))
}
fn restore_vulcan_file_context_field(
context: &Table,
field_name: &str,
value: Option<&VulcanFileContextPath>,
) -> Result<(), String> {
match value {
Some(value) => context
.set(field_name, value.lua_text())
.map_err(|error| format!("Failed to restore vulcan.context.{}: {}", field_name, error)),
None => context
.set(field_name, LuaValue::Nil)
.map_err(|error| format!("Failed to restore vulcan.context.{}: {}", field_name, error)),
}
}
fn restore_vulcan_file_context_snapshot(
lua: &Lua,
snapshot: &VulcanFileContextSnapshot,
) -> Result<(), String> {
let context = get_vulcan_context_table(lua)?;
restore_vulcan_file_context_field(&context, "skill_dir", snapshot.skill_dir.as_ref())?;
restore_vulcan_file_context_field(&context, "entry_dir", snapshot.entry_dir.as_ref())?;
restore_vulcan_file_context_field(&context, "entry_file", snapshot.entry_file.as_ref())?;
Ok(())
}
fn restore_lua_nested_dependency_context(
lua: &Lua,
host_options: &LuaRuntimeHostOptions,
file_context: &VulcanFileContextSnapshot,
internal_context: &VulcanInternalExecutionContext,
) -> Result<(), String> {
populate_vulcan_dependency_context(
lua,
host_options,
file_context
.skill_dir
.as_ref()
.map(VulcanFileContextPath::path),
internal_context.skill_name.as_deref(),
)
}
struct LuaNestedOuterStateSnapshot {
core_state: VulcanCoreModuleState,
context: VulcanContextSnapshot,
lancedb_skill_name: String,
sqlite_skill_name: String,
internal_context: VulcanInternalExecutionContext,
file_context: VulcanFileContextSnapshot,
managed_package: Option<Arc<ManagedRuntimePackageContext>>,
}
fn capture_lua_nested_outer_state(lua: &Lua) -> Result<LuaNestedOuterStateSnapshot, String> {
let vulcan = get_vulcan_table(lua)?;
Ok(LuaNestedOuterStateSnapshot {
core_state: VulcanCoreModuleState::capture(lua)?,
context: capture_vulcan_context_snapshot(lua)?,
lancedb_skill_name: capture_provider_skill_marker(&vulcan, "__lancedb_skill_name")?,
sqlite_skill_name: capture_provider_skill_marker(&vulcan, "__sqlite_skill_name")?,
internal_context: capture_vulcan_internal_execution_context(lua)?,
file_context: capture_vulcan_file_context(lua)?,
managed_package: optional_lua_managed_package_context(lua),
})
}
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 {}",
render_log_friendly_path(skill_dir)
)
})?;
let runtime_root = skills_root.parent().ok_or_else(|| {
format!(
"Failed to derive runtime root from skill directory {}",
render_log_friendly_path(skill_dir)
)
})?;
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 managed_runtime_optional_string_field(
table: &Table,
api_name: &str,
field_name: &str,
) -> Result<Option<String>, mlua::Error> {
let value: LuaValue = table
.get(field_name)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
match value {
LuaValue::Nil => Ok(None),
LuaValue::String(value) => Ok(Some(
value
.to_str()
.map_err(|error| {
mlua::Error::runtime(format!("{api_name}: {field_name} is not UTF-8: {error}"))
})?
.to_string(),
)),
other => Err(mlua::Error::runtime(format!(
"{api_name}: {field_name} must be a string or nil, got {}",
lua_value_type_name(&other)
))),
}
}
fn managed_runtime_optional_timeout_ms(
table: &Table,
api_name: &str,
) -> Result<Option<u64>, mlua::Error> {
let value: LuaValue = table
.get("timeout_ms")
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
optional_u64_arg(value, api_name, "timeout_ms")
}
#[derive(Clone, Copy, Default)]
struct ManagedRuntimeTransactionContextSlot {
context: Option<ManagedRuntimeTransactionContext>,
}
struct LuaManagedRuntimeTransactionScopeGuard {
lua: Lua,
previous: Option<ManagedRuntimeTransactionContext>,
}
fn current_lua_managed_runtime_services(
lua: &Lua,
api_name: &str,
) -> Result<Arc<ManagedRuntimeServices>, mlua::Error> {
let services = lua
.app_data_ref::<Arc<ManagedRuntimeServices>>()
.map(|services| Arc::clone(&services));
services.ok_or_else(|| {
mlua::Error::runtime(format!(
"{api_name}: managed runtime services are not installed"
))
})
}
fn current_lua_managed_runtime_worker_service(
lua: &Lua,
api_name: &str,
) -> Result<Arc<ManagedRuntimeWorkerService>, mlua::Error> {
let service = lua
.app_data_ref::<Arc<ManagedRuntimeWorkerService>>()
.map(|service| Arc::clone(&service));
service.ok_or_else(|| {
mlua::Error::runtime(format!(
"{api_name}: managed runtime worker service is not installed"
))
})
}
fn optional_lua_managed_runtime_transaction(lua: &Lua) -> Option<ManagedRuntimeTransactionContext> {
lua.app_data_ref::<ManagedRuntimeTransactionContextSlot>()
.and_then(|slot| slot.context)
}
fn replace_lua_managed_runtime_transaction(
lua: &Lua,
context: Option<ManagedRuntimeTransactionContext>,
) -> Option<ManagedRuntimeTransactionContext> {
lua.set_app_data(ManagedRuntimeTransactionContextSlot { context })
.and_then(|slot| slot.context)
}
impl LuaManagedRuntimeTransactionScopeGuard {
fn install(lua: &Lua, context: Option<ManagedRuntimeTransactionContext>) -> Self {
let previous = replace_lua_managed_runtime_transaction(lua, context);
Self {
lua: lua.clone(),
previous,
}
}
}
impl Drop for LuaManagedRuntimeTransactionScopeGuard {
fn drop(&mut self) {
replace_lua_managed_runtime_transaction(&self.lua, self.previous);
}
}
fn parse_managed_runtime_session_open_request(
spec: LuaValue,
api_name: &str,
default_encoding: RuntimeTextEncoding,
default_buffer_limit_bytes_per_stream: usize,
) -> Result<ManagedRuntimeSessionOpenRequest, mlua::Error> {
let table = match spec {
LuaValue::Table(table) => table,
other => {
return Err(mlua::Error::runtime(format!(
"{api_name}: spec must be a table, got {}",
lua_value_type_name(&other)
)));
}
};
validate_managed_runtime_session_open_fields(&table, api_name)?;
let file = managed_runtime_optional_string_field(&table, api_name, "file")?
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.ok_or_else(|| mlua::Error::runtime(format!("{api_name}: file is required")))?;
let args = parse_managed_runtime_session_args(&table, api_name)?;
let cwd = managed_runtime_optional_string_field(&table, api_name, "cwd")?;
let stdout_encoding = managed_runtime_session_encoding_field(
&table,
"stdout_encoding",
api_name,
default_encoding,
)?;
let stderr_encoding = managed_runtime_session_encoding_field(
&table,
"stderr_encoding",
api_name,
default_encoding,
)?;
let stdin_encoding = managed_runtime_session_encoding_field(
&table,
"stdin_encoding",
api_name,
default_encoding,
)?;
let buffer_limit_bytes = match optional_u64_arg(
table
.get("buffer_limit_bytes")
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?,
api_name,
"buffer_limit_bytes",
)? {
Some(0) => {
return Err(mlua::Error::runtime(format!(
"{api_name}: buffer_limit_bytes must be greater than zero"
)));
}
Some(value) => usize::try_from(value).map_err(|_| {
mlua::Error::runtime(format!(
"{api_name}: buffer_limit_bytes exceeds the platform usize range"
))
})?,
None => default_buffer_limit_bytes_per_stream,
};
Ok(ManagedRuntimeSessionOpenRequest {
file,
args,
cwd,
stdout_encoding,
stderr_encoding,
stdin_encoding,
buffer_limit_bytes,
})
}
fn validate_managed_runtime_session_open_fields(
table: &Table,
api_name: &str,
) -> Result<(), mlua::Error> {
for pair in table.clone().pairs::<LuaValue, LuaValue>() {
let (key, _value) =
pair.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let LuaValue::String(key) = key else {
return Err(mlua::Error::runtime(format!(
"{api_name}: request keys must be strings"
)));
};
let key = key.to_str().map_err(|error| {
mlua::Error::runtime(format!("{api_name}: key is not UTF-8: {error}"))
})?;
if !matches!(
key.as_ref(),
"file"
| "args"
| "cwd"
| "stdout_encoding"
| "stderr_encoding"
| "stdin_encoding"
| "buffer_limit_bytes"
) {
return Err(mlua::Error::runtime(format!(
"{api_name}: unknown field `{key}`"
)));
}
}
Ok(())
}
fn parse_managed_runtime_session_args(
table: &Table,
api_name: &str,
) -> Result<Vec<String>, mlua::Error> {
let value: LuaValue = table
.get("args")
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let args_table = match value {
LuaValue::Nil => return Ok(Vec::new()),
LuaValue::Table(table) => table,
other => {
return Err(mlua::Error::runtime(format!(
"{api_name}: args must be an array table or nil, got {}",
lua_value_type_name(&other)
)));
}
};
let length = args_table.raw_len();
let mut args = Vec::with_capacity(length);
for index in 1..=length {
let argument: mlua::String = args_table.get(index).map_err(|error| {
mlua::Error::runtime(format!(
"{api_name}: args[{index}] must be a string: {error}"
))
})?;
args.push(
argument
.to_str()
.map_err(|error| {
mlua::Error::runtime(format!("{api_name}: args[{index}] is not UTF-8: {error}"))
})?
.to_string(),
);
}
for pair in args_table.pairs::<LuaValue, LuaValue>() {
let (key, _value) =
pair.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let LuaValue::Integer(index) = key else {
return Err(mlua::Error::runtime(format!(
"{api_name}: args must contain only integer array keys"
)));
};
if index < 1
|| usize::try_from(index)
.ok()
.is_none_or(|index| index > length)
{
return Err(mlua::Error::runtime(format!(
"{api_name}: args must be a dense one-based array"
)));
}
}
Ok(args)
}
fn managed_runtime_session_encoding_field(
table: &Table,
field_name: &str,
api_name: &str,
default_encoding: RuntimeTextEncoding,
) -> Result<RuntimeTextEncoding, mlua::Error> {
let label = managed_runtime_optional_string_field(table, api_name, field_name)?;
match label {
Some(label) => RuntimeTextEncoding::parse(&label)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {field_name}: {error}"))),
None => Ok(default_encoding),
}
}
fn system_managed_runtime_session_event_identity(
package: &ManagedRuntimePackageContext,
api_name: &str,
) -> Result<ManagedRuntimeSessionEventIdentity, mlua::Error> {
let binding = package.lease_binding().ok_or_else(|| {
mlua::Error::runtime(format!(
"{api_name}: persistent managed sessions are available only inside a System Plugin lease"
))
})?;
let identity = binding.identity().ok_or_else(|| {
mlua::Error::runtime(format!("{api_name}: system lease identity is not bound"))
})?;
Ok(ManagedRuntimeSessionEventIdentity {
lease_id: identity.lease_id().to_string(),
sid: binding.sid().to_string(),
generation: identity.generation(),
})
}
fn open_managed_python_session(
lua: &Lua,
spec: LuaValue,
default_encoding: RuntimeTextEncoding,
) -> Result<AnyUserData, mlua::Error> {
let api_name = "vulcan.runtime.python.session.open";
ensure_persistent_managed_runtime_session_platform_supported()
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let package = current_lua_managed_package_context(lua, api_name)?;
let services = current_lua_managed_runtime_services(lua, api_name)?;
let event_identity = system_managed_runtime_session_event_identity(package.as_ref(), api_name)?;
let transaction = optional_lua_managed_runtime_transaction(lua).ok_or_else(|| {
mlua::Error::runtime(format!(
"{api_name}: persistent managed sessions require an active System lease evaluation"
))
})?;
let reservation = services
.reserve_session(
package.owner_token(),
Some(transaction),
Some(event_identity),
)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let observer = reservation.observer();
let request = parse_managed_runtime_session_open_request(
spec,
api_name,
default_encoding,
services.persistent_session_default_buffer_limit_bytes_per_stream(),
)?;
let launch = launch_managed_python_session(package.as_ref(), request, observer)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let (session_id, cleanup) = reservation
.activate(&launch.core, launch.cleanup)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
create_managed_process_session_userdata(lua, launch.core, Some(cleanup), Some(session_id))
}
fn open_managed_node_session(
lua: &Lua,
spec: LuaValue,
default_encoding: RuntimeTextEncoding,
) -> Result<AnyUserData, mlua::Error> {
let api_name = "vulcan.runtime.node.session.open";
ensure_persistent_managed_runtime_session_platform_supported()
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let package = current_lua_managed_package_context(lua, api_name)?;
let services = current_lua_managed_runtime_services(lua, api_name)?;
let event_identity = system_managed_runtime_session_event_identity(package.as_ref(), api_name)?;
let transaction = optional_lua_managed_runtime_transaction(lua).ok_or_else(|| {
mlua::Error::runtime(format!(
"{api_name}: persistent managed sessions require an active System lease evaluation"
))
})?;
let reservation = services
.reserve_session(
package.owner_token(),
Some(transaction),
Some(event_identity),
)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let observer = reservation.observer();
let request = parse_managed_runtime_session_open_request(
spec,
api_name,
default_encoding,
services.persistent_session_default_buffer_limit_bytes_per_stream(),
)?;
let launch = launch_managed_node_session(package.as_ref(), request, observer)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let (session_id, cleanup) = reservation
.activate(&launch.core, launch.cleanup)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
create_managed_process_session_userdata(lua, launch.core, Some(cleanup), Some(session_id))
}
const MANAGED_RUNTIME_WORKER_TEARDOWN_TIMEOUT_SECS: u64 = 2;
const MANAGED_RUNTIME_WORKER_PROTOCOL_LINE_LIMIT_BYTES: usize = 2 * 1024 * 1024;
const MANAGED_RUNTIME_WORKER_CHANNEL_CAPACITY: usize = 4;
struct ManagedRuntimeInvokeRequest {
file: String,
handler: String,
args: Value,
timeout_ms: Option<u64>,
}
#[derive(Debug, Clone)]
struct ManagedRuntimeWorkerKey {
runtime: String,
env_hash: String,
env_dir: PathBuf,
package_identity: ManagedRuntimePackageIdentity,
owner_token: u64,
owner_state: Weak<ManagedRuntimeOwnerState>,
}
impl ManagedRuntimeWorkerKey {
fn ensure_owner_active(&self) -> Result<(), String> {
let owner_state = self.owner_state.upgrade().ok_or_else(|| {
format!(
"managed runtime worker owner `{}` is no longer active",
self.owner_token
)
})?;
if owner_state.is_retired() {
return Err(format!(
"managed runtime worker owner `{}` is retired",
self.owner_token
));
}
Ok(())
}
}
impl PartialEq for ManagedRuntimeWorkerKey {
fn eq(&self, other: &Self) -> bool {
self.runtime == other.runtime
&& self.env_hash == other.env_hash
&& self.env_dir == other.env_dir
&& self.package_identity == other.package_identity
&& self.owner_token == other.owner_token
}
}
impl Eq for ManagedRuntimeWorkerKey {}
impl Hash for ManagedRuntimeWorkerKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.runtime.hash(state);
self.env_hash.hash(state);
self.env_dir.hash(state);
self.package_identity.hash(state);
self.owner_token.hash(state);
}
}
struct ManagedRuntimeWorkerInvokeResult {
envelope: Value,
timed_out: bool,
worker_reused: bool,
discard_worker: bool,
}
struct ManagedRuntimeWorkerChildOwnership {
child: Child,
reaper_permit: DetachedChildReaperPermit,
}
struct ManagedRuntimeWorker {
child_ownership: Option<ManagedRuntimeWorkerChildOwnership>,
process_tree: ManagedChildProcessTree,
package_snapshot: Option<Arc<ManagedPackageSnapshot>>,
stdin: ChildStdin,
stdout_rx: mpsc::Receiver<Result<String, String>>,
stderr_rx: mpsc::Receiver<Result<String, String>>,
stdout_reader: Option<thread::JoinHandle<()>>,
stderr_reader: Option<thread::JoinHandle<()>>,
last_used_at: Instant,
fresh: bool,
}
struct ManagedRuntimeWorkerBucket {
available: Vec<ManagedRuntimeWorker>,
total_count: usize,
}
enum ManagedRuntimeWorkerCheckout {
Reused(Box<ManagedRuntimeWorker>),
SpawnReserved,
}
struct ManagedRuntimeWorkerPool {
buckets: HashMap<ManagedRuntimeWorkerKey, ManagedRuntimeWorkerBucket>,
max_size_per_environment: usize,
idle_ttl_secs: u64,
}
type ManagedPackageSnapshotCell = Arc<OnceLock<Result<Arc<ManagedPackageSnapshot>, String>>>;
type ManagedPackageSnapshotMap = HashMap<ManagedRuntimeWorkerKey, ManagedPackageSnapshotCell>;
impl ManagedRuntimeWorkerPool {
fn new(config: LuaRuntimeManagedRuntimeConfig) -> Self {
Self {
buckets: HashMap::new(),
max_size_per_environment: config.worker_pool_max_size_per_environment,
idle_ttl_secs: config.worker_idle_ttl_secs,
}
}
fn reap_idle_locked(
bucket: &mut ManagedRuntimeWorkerBucket,
idle_ttl_secs: u64,
) -> Vec<ManagedRuntimeWorker> {
let idle_limit = Duration::from_secs(idle_ttl_secs);
let now = Instant::now();
let mut index = 0usize;
let mut retired_workers = Vec::new();
while index < bucket.available.len() {
let should_remove = now
.checked_duration_since(bucket.available[index].last_used_at)
.map(|idle| idle >= idle_limit)
.unwrap_or(false);
if should_remove {
retired_workers.push(bucket.available.swap_remove(index));
bucket.total_count = bucket.total_count.saturating_sub(1);
} else {
index += 1;
}
}
retired_workers
}
fn reserve(
&mut self,
key: &ManagedRuntimeWorkerKey,
) -> Result<(ManagedRuntimeWorkerCheckout, Vec<ManagedRuntimeWorker>), String> {
key.ensure_owner_active()?;
let max_size_per_environment = self.max_size_per_environment;
let idle_ttl_secs = self.idle_ttl_secs;
let bucket =
self.buckets
.entry(key.clone())
.or_insert_with(|| ManagedRuntimeWorkerBucket {
available: Vec::new(),
total_count: 0,
});
let retired_workers = Self::reap_idle_locked(bucket, idle_ttl_secs);
if let Some(mut worker) = bucket.available.pop() {
worker.last_used_at = Instant::now();
return Ok((
ManagedRuntimeWorkerCheckout::Reused(Box::new(worker)),
retired_workers,
));
}
if bucket.total_count >= max_size_per_environment {
return Err(format!(
"managed runtime worker pool is exhausted for this environment; max_size={max_size_per_environment}"
));
}
bucket.total_count = bucket
.total_count
.checked_add(1)
.ok_or_else(|| "managed runtime worker pool count exhausted".to_string())?;
Ok((ManagedRuntimeWorkerCheckout::SpawnReserved, retired_workers))
}
fn release(
&mut self,
key: ManagedRuntimeWorkerKey,
mut worker: ManagedRuntimeWorker,
) -> Vec<ManagedRuntimeWorker> {
let idle_ttl_secs = self.idle_ttl_secs;
if key.ensure_owner_active().is_err() {
return vec![worker];
}
worker.last_used_at = Instant::now();
worker.fresh = false;
let bucket = self
.buckets
.entry(key)
.or_insert_with(|| ManagedRuntimeWorkerBucket {
available: Vec::new(),
total_count: 1,
});
if bucket.total_count == 0 {
bucket.total_count = 1;
}
bucket.available.push(worker);
Self::reap_idle_locked(bucket, idle_ttl_secs)
}
fn discard(&mut self, key: &ManagedRuntimeWorkerKey) {
if let Some(bucket) = self.buckets.get_mut(key) {
bucket.total_count = bucket.total_count.saturating_sub(1);
if bucket.total_count == 0 && bucket.available.is_empty() {
self.buckets.remove(key);
}
}
}
fn retire_owner(&mut self, owner_token: u64) -> Vec<ManagedRuntimeWorker> {
let retired_keys: Vec<_> = self
.buckets
.keys()
.filter(|key| key.owner_token == owner_token)
.cloned()
.collect();
let mut retired_workers = Vec::new();
for key in retired_keys {
if let Some(bucket) = self.buckets.remove(&key) {
retired_workers.extend(bucket.available);
}
}
retired_workers
}
}
struct ManagedRuntimeWorkerService {
config: LuaRuntimeManagedRuntimeConfig,
pool: Mutex<ManagedRuntimeWorkerPool>,
package_snapshots: Mutex<ManagedPackageSnapshotMap>,
}
impl ManagedRuntimeWorkerService {
#[cfg(test)]
fn new() -> Arc<Self> {
Self::new_with_config(LuaRuntimeManagedRuntimeConfig::default())
.expect("default managed runtime config must be valid")
}
fn new_with_config(config: LuaRuntimeManagedRuntimeConfig) -> Result<Arc<Self>, String> {
config.validate()?;
Ok(Arc::new(Self {
config,
pool: Mutex::new(ManagedRuntimeWorkerPool::new(config)),
package_snapshots: Mutex::new(HashMap::new()),
}))
}
fn invoke_default_timeout_ms(&self) -> Option<u64> {
self.config.invoke_default_timeout_ms
}
fn package_snapshot(
&self,
key: &ManagedRuntimeWorkerKey,
plan: &ManagedRuntimeEnvPlan,
package: &ManagedRuntimePackageContext,
) -> Result<Arc<ManagedPackageSnapshot>, String> {
key.ensure_owner_active()?;
let snapshot_cell = {
let mut snapshots = self.lock_package_snapshots();
Arc::clone(
snapshots
.entry(key.clone())
.or_insert_with(|| Arc::new(OnceLock::new())),
)
};
let result = snapshot_cell
.get_or_init(|| prepare_managed_package_snapshot(plan, package, ".ls-w").map(Arc::new))
.clone();
if result.is_err() {
let mut snapshots = self.lock_package_snapshots();
if snapshots
.get(key)
.is_some_and(|current| Arc::ptr_eq(current, &snapshot_cell))
{
snapshots.remove(key);
}
return result;
}
if let Err(error) = key.ensure_owner_active() {
let mut snapshots = self.lock_package_snapshots();
if snapshots
.get(key)
.is_some_and(|current| Arc::ptr_eq(current, &snapshot_cell))
{
snapshots.remove(key);
}
return Err(error);
}
result
}
fn acquire<F>(
&self,
key: ManagedRuntimeWorkerKey,
mut factory: F,
) -> Result<(ManagedRuntimeWorker, bool), String>
where
F: FnMut() -> Result<ManagedRuntimeWorker, String>,
{
let (checkout, retired_workers) = self.lock_pool().reserve(&key)?;
drop(retired_workers);
match checkout {
ManagedRuntimeWorkerCheckout::Reused(worker) => Ok((*worker, true)),
ManagedRuntimeWorkerCheckout::SpawnReserved => match factory() {
Ok(mut worker) => {
worker.last_used_at = Instant::now();
let owner_retired = {
let mut pool = self.lock_pool();
if key.ensure_owner_active().is_err() {
pool.discard(&key);
true
} else {
false
}
};
if owner_retired {
drop(worker);
Err(format!(
"managed runtime worker owner `{}` retired during worker startup",
key.owner_token
))
} else {
Ok((worker, false))
}
}
Err(error) => {
self.lock_pool().discard(&key);
Err(error)
}
},
}
}
fn release(&self, key: ManagedRuntimeWorkerKey, worker: ManagedRuntimeWorker) {
let retired_workers = self.lock_pool().release(key, worker);
drop(retired_workers);
}
fn discard(&self, key: &ManagedRuntimeWorkerKey, worker: ManagedRuntimeWorker) {
{
let mut pool = self.lock_pool();
pool.discard(key);
}
drop(worker);
}
fn retire_owner(&self, owner_token: u64) {
let _ = retire_managed_runtime_owner_state(owner_token);
let retired_workers = self.lock_pool().retire_owner(owner_token);
drop(retired_workers);
let retired_snapshots = {
let mut snapshots = self.lock_package_snapshots();
let retired_keys = snapshots
.keys()
.filter(|key| key.owner_token == owner_token)
.cloned()
.collect::<Vec<_>>();
retired_keys
.into_iter()
.filter_map(|key| snapshots.remove(&key))
.collect::<Vec<_>>()
};
drop(retired_snapshots);
}
fn lock_pool(&self) -> MutexGuard<'_, ManagedRuntimeWorkerPool> {
self.pool
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn lock_package_snapshots(&self) -> MutexGuard<'_, ManagedPackageSnapshotMap> {
self.package_snapshots
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
impl Drop for ManagedRuntimeWorkerService {
fn drop(&mut self) {
let pool = self
.pool
.get_mut()
.unwrap_or_else(std::sync::PoisonError::into_inner);
pool.buckets.clear();
self.package_snapshots
.get_mut()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
}
}
impl Drop for ManagedRuntimeWorker {
fn drop(&mut self) {
if let Some(ManagedRuntimeWorkerChildOwnership {
mut child,
reaper_permit,
}) = self.child_ownership.take()
{
let (errors, reaped) =
terminate_managed_runtime_worker_child_bounded(&mut child, &self.process_tree);
for error in errors {
log_warn(format!(
"[LuaSkill:warn] managed runtime worker teardown failed: {error}"
));
}
if reaped {
self.process_tree.clear_detached_guard();
drop(reaper_permit);
} else {
let keepalive = self
.package_snapshot
.take()
.map(|snapshot| Box::new(move || drop(snapshot)) as Box<dyn FnOnce() + Send>);
self.process_tree
.handoff_to_reaper(reaper_permit, child, keepalive);
}
}
let reader_deadline = Instant::now()
.checked_add(Duration::from_secs(
MANAGED_RUNTIME_WORKER_TEARDOWN_TIMEOUT_SECS,
))
.unwrap_or_else(Instant::now);
if let Some(handle) = self.stdout_reader.take() {
join_managed_runtime_reader_bounded(handle, "stdout", reader_deadline);
}
if let Some(handle) = self.stderr_reader.take() {
join_managed_runtime_reader_bounded(handle, "stderr", reader_deadline);
}
}
}
fn terminate_managed_runtime_worker_child_bounded(
child: &mut Child,
process_tree: &ManagedChildProcessTree,
) -> (Vec<String>, bool) {
let mut errors = Vec::new();
if let Err(error) = process_tree.terminate(child) {
errors.push(format!("process-tree termination: {error}"));
}
let direct_child_already_reaped = match child.try_wait() {
Ok(Some(_)) => true,
Ok(None) => {
if let Err(error) = child.kill() {
errors.push(format!("direct-child kill: {error}"));
}
false
}
Err(error) => {
errors.push(format!("initial direct-child status probe: {error}"));
false
}
};
let direct_child_reaped = if direct_child_already_reaped {
true
} else {
let deadline = Instant::now()
.checked_add(Duration::from_secs(
MANAGED_RUNTIME_WORKER_TEARDOWN_TIMEOUT_SECS,
))
.unwrap_or_else(Instant::now);
loop {
match child.try_wait() {
Ok(Some(_)) => break true,
Ok(None) if Instant::now() < deadline => thread::sleep(Duration::from_millis(10)),
Ok(None) => {
errors.push("direct child did not exit before teardown deadline".to_string());
break false;
}
Err(error) => {
errors.push(format!("direct-child status probe: {error}"));
break false;
}
}
}
};
let descendant_tree_empty = match process_tree.detached_tree_is_empty() {
Ok(empty) => empty,
Err(error) => {
errors.push(format!("process-tree convergence probe: {error}"));
false
}
};
(errors, direct_child_reaped && descendant_tree_empty)
}
fn teardown_unpublished_managed_runtime_worker(
mut child: Child,
process_tree: &ManagedChildProcessTree,
permit: DetachedChildReaperPermit,
keepalive: Option<Box<dyn FnOnce() + Send>>,
) -> Vec<String> {
let (errors, reaped) = terminate_managed_runtime_worker_child_bounded(&mut child, process_tree);
if reaped {
process_tree.clear_detached_guard();
drop(permit);
} else {
process_tree.handoff_to_reaper(permit, child, keepalive);
}
errors
}
fn join_managed_runtime_reader_bounded(
handle: thread::JoinHandle<()>,
stream_name: &str,
deadline: Instant,
) {
while !handle.is_finished() && Instant::now() < deadline {
thread::sleep(Duration::from_millis(10));
}
if handle.is_finished() {
if handle.join().is_err() {
log_warn(format!(
"[LuaSkill:warn] managed runtime worker {stream_name} reader panicked"
));
}
} else {
log_warn(format!(
"[LuaSkill:warn] managed runtime worker {stream_name} reader exceeded teardown deadline and was detached"
));
drop(handle);
}
}
fn spawn_managed_runtime_line_reader<R>(
reader: R,
sender: mpsc::SyncSender<Result<String, String>>,
stream_name: &'static str,
) -> Result<thread::JoinHandle<()>, String>
where
R: std::io::Read + Send + 'static,
{
thread::Builder::new()
.name(format!("managed-runtime-worker-{stream_name}"))
.spawn(move || {
let mut reader = BufReader::new(reader);
loop {
let record = match read_managed_runtime_worker_line(&mut reader) {
Ok(Some(record)) => record,
Ok(None) => break,
Err(error) => Err(format!(
"managed runtime worker {stream_name} read failed: {error}"
)),
};
let terminal_error = record.is_err();
if sender.send(record).is_err() || terminal_error {
break;
}
}
})
.map_err(|error| format!("spawn managed runtime worker {stream_name} reader: {error}"))
}
fn read_managed_runtime_worker_line<R>(
reader: &mut R,
) -> Result<Option<Result<String, String>>, std::io::Error>
where
R: BufRead,
{
let mut retained = Vec::new();
let mut saw_input = false;
let mut overflowed = false;
loop {
let available = reader.fill_buf()?;
if available.is_empty() {
if !saw_input {
return Ok(None);
}
break;
}
saw_input = true;
let newline = available.iter().position(|byte| *byte == b'\n');
let consumed = newline.map_or(available.len(), |index| index + 1);
let content_len = newline.unwrap_or(consumed);
if !overflowed {
let remaining =
MANAGED_RUNTIME_WORKER_PROTOCOL_LINE_LIMIT_BYTES.saturating_sub(retained.len());
let copied = content_len.min(remaining);
retained.extend_from_slice(&available[..copied]);
overflowed = copied < content_len;
}
reader.consume(consumed);
if newline.is_some() {
break;
}
}
if retained.last() == Some(&b'\r') {
retained.pop();
}
if overflowed {
return Ok(Some(Err(format!(
"managed runtime worker protocol line exceeded {MANAGED_RUNTIME_WORKER_PROTOCOL_LINE_LIMIT_BYTES} bytes"
))));
}
match String::from_utf8(retained) {
Ok(line) => Ok(Some(Ok(line))),
Err(error) => Ok(Some(Err(format!(
"managed runtime worker protocol line is not valid UTF-8: {error}"
)))),
}
}
fn managed_runtime_worker_key(
plan: &ManagedRuntimeEnvPlan,
package: &ManagedRuntimePackageContext,
) -> ManagedRuntimeWorkerKey {
ManagedRuntimeWorkerKey {
runtime: plan.runtime.as_str().to_string(),
env_hash: plan.env_hash.clone(),
env_dir: normalize_runtime_root_path(&plan.env_dir),
package_identity: package.identity().clone(),
owner_token: package.owner_token(),
owner_state: package.owner_state(),
}
}
#[cfg(test)]
fn spawn_managed_runtime_worker(command: &mut Command) -> Result<ManagedRuntimeWorker, String> {
spawn_managed_runtime_worker_with_snapshot(command, None)
}
fn spawn_managed_runtime_worker_with_snapshot(
command: &mut Command,
package_snapshot: Option<Arc<ManagedPackageSnapshot>>,
) -> Result<ManagedRuntimeWorker, String> {
command.stdin(Stdio::piped());
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
let attach_keepalive = package_snapshot.as_ref().map(|snapshot| {
let snapshot = Arc::clone(snapshot);
Box::new(move || drop(snapshot)) as Box<dyn FnOnce() + Send>
});
let (mut child, process_tree, reaper_permit) = ManagedChildProcessTree::spawn_with_keepalive(
command,
"managed runtime worker",
attach_keepalive,
)?;
let (stdin, stdout, stderr) =
match (child.stdin.take(), child.stdout.take(), child.stderr.take()) {
(Some(stdin), Some(stdout), Some(stderr)) => (stdin, stdout, stderr),
_ => {
let cleanup_errors = teardown_unpublished_managed_runtime_worker(
child,
&process_tree,
reaper_permit,
package_snapshot.as_ref().map(|snapshot| {
let snapshot = Arc::clone(snapshot);
Box::new(move || drop(snapshot)) as Box<dyn FnOnce() + Send>
}),
);
return Err(if cleanup_errors.is_empty() {
"managed runtime worker pipe is unavailable".to_string()
} else {
format!(
"managed runtime worker pipe is unavailable; cleanup also failed: {}",
cleanup_errors.join("; ")
)
});
}
};
let (stdout_tx, stdout_rx) = mpsc::sync_channel(MANAGED_RUNTIME_WORKER_CHANNEL_CAPACITY);
let (stderr_tx, stderr_rx) = mpsc::sync_channel(MANAGED_RUNTIME_WORKER_CHANNEL_CAPACITY);
let stdout_reader = match spawn_managed_runtime_line_reader(stdout, stdout_tx, "stdout") {
Ok(handle) => handle,
Err(error) => {
let cleanup_errors = teardown_unpublished_managed_runtime_worker(
child,
&process_tree,
reaper_permit,
package_snapshot.as_ref().map(|snapshot| {
let snapshot = Arc::clone(snapshot);
Box::new(move || drop(snapshot)) as Box<dyn FnOnce() + Send>
}),
);
return Err(if cleanup_errors.is_empty() {
error
} else {
format!(
"{error}; cleanup also failed: {}",
cleanup_errors.join("; ")
)
});
}
};
let stderr_reader = match spawn_managed_runtime_line_reader(stderr, stderr_tx, "stderr") {
Ok(handle) => handle,
Err(error) => {
let cleanup_errors = teardown_unpublished_managed_runtime_worker(
child,
&process_tree,
reaper_permit,
package_snapshot.as_ref().map(|snapshot| {
let snapshot = Arc::clone(snapshot);
Box::new(move || drop(snapshot)) as Box<dyn FnOnce() + Send>
}),
);
let reader_deadline = Instant::now()
.checked_add(Duration::from_secs(
MANAGED_RUNTIME_WORKER_TEARDOWN_TIMEOUT_SECS,
))
.unwrap_or_else(Instant::now);
join_managed_runtime_reader_bounded(stdout_reader, "stdout", reader_deadline);
return Err(if cleanup_errors.is_empty() {
error
} else {
format!(
"{error}; cleanup also failed: {}",
cleanup_errors.join("; ")
)
});
}
};
Ok(ManagedRuntimeWorker {
child_ownership: Some(ManagedRuntimeWorkerChildOwnership {
child,
reaper_permit,
}),
process_tree,
package_snapshot,
stdin,
stdout_rx,
stderr_rx,
stdout_reader: Some(stdout_reader),
stderr_reader: Some(stderr_reader),
last_used_at: Instant::now(),
fresh: true,
})
}
fn drain_managed_runtime_worker_stderr(worker: &ManagedRuntimeWorker) -> String {
let mut output = String::new();
let mut dropped_bytes = 0usize;
while let Ok(record) = worker.stderr_rx.try_recv() {
let line = match record {
Ok(line) => line,
Err(error) => format!("[worker stderr reader error: {error}]"),
};
if !output.is_empty() {
append_bounded_worker_diagnostic(&mut output, "\n", &mut dropped_bytes);
}
append_bounded_worker_diagnostic(&mut output, &line, &mut dropped_bytes);
}
if dropped_bytes > 0 {
let marker = format!("\n[worker stderr truncated; dropped_bytes={dropped_bytes}]");
while output.len().saturating_add(marker.len())
> MANAGED_RUNTIME_WORKER_PROTOCOL_LINE_LIMIT_BYTES
{
output.pop();
}
output.push_str(&marker);
}
output
}
fn append_bounded_worker_diagnostic(output: &mut String, text: &str, dropped_bytes: &mut usize) {
let remaining = MANAGED_RUNTIME_WORKER_PROTOCOL_LINE_LIMIT_BYTES.saturating_sub(output.len());
if text.len() <= remaining {
output.push_str(text);
return;
}
let mut retained_bytes = remaining.min(text.len());
while retained_bytes > 0 && !text.is_char_boundary(retained_bytes) {
retained_bytes -= 1;
}
output.push_str(&text[..retained_bytes]);
*dropped_bytes = dropped_bytes.saturating_add(text.len().saturating_sub(retained_bytes));
}
fn managed_runtime_worker_stdout_closed_error(worker: &mut ManagedRuntimeWorker) -> String {
let status = match worker.child_ownership.as_mut() {
Some(ownership) => match ownership.child.try_wait() {
Ok(Some(status)) => format!("exited with {status}"),
Ok(None) => "is still running".to_string(),
Err(error) => format!("status probe failed: {error}"),
},
None => "has no direct-child ownership".to_string(),
};
format!("managed runtime worker stdout closed; child {status}")
}
fn invoke_managed_runtime_worker(
mut worker: ManagedRuntimeWorker,
payload: &Value,
timeout_ms: Option<u64>,
worker_reused: bool,
) -> (ManagedRuntimeWorker, ManagedRuntimeWorkerInvokeResult) {
let mut discard_worker = false;
let mut timed_out = false;
let stale_stderr = drain_managed_runtime_worker_stderr(&worker);
if !stale_stderr.is_empty() {
let envelope = json!({
"ok": false,
"value": null,
"stdout": "",
"stderr": stale_stderr,
"error": "managed runtime worker produced raw stderr outside its response envelope",
});
return (
worker,
ManagedRuntimeWorkerInvokeResult {
envelope,
timed_out,
worker_reused,
discard_worker: true,
},
);
}
let payload_text = match serde_json::to_string(payload) {
Ok(payload_text) => payload_text,
Err(error) => {
discard_worker = true;
let envelope = json!({
"ok": false,
"value": null,
"stdout": "",
"stderr": "",
"error": format!("failed to serialize managed runtime request: {error}"),
});
return (
worker,
ManagedRuntimeWorkerInvokeResult {
envelope,
timed_out,
worker_reused,
discard_worker,
},
);
}
};
if let Err(error) = writeln!(worker.stdin, "{payload_text}") {
discard_worker = true;
let envelope = json!({
"ok": false,
"value": null,
"stdout": "",
"stderr": drain_managed_runtime_worker_stderr(&worker),
"error": format!("failed to write managed runtime worker stdin: {error}"),
});
return (
worker,
ManagedRuntimeWorkerInvokeResult {
envelope,
timed_out,
worker_reused,
discard_worker,
},
);
}
if let Err(error) = worker.stdin.flush() {
discard_worker = true;
let envelope = json!({
"ok": false,
"value": null,
"stdout": "",
"stderr": drain_managed_runtime_worker_stderr(&worker),
"error": format!("failed to flush managed runtime worker stdin: {error}"),
});
return (
worker,
ManagedRuntimeWorkerInvokeResult {
envelope,
timed_out,
worker_reused,
discard_worker,
},
);
}
let line_result = match timeout_ms {
Some(timeout_ms) => match worker
.stdout_rx
.recv_timeout(Duration::from_millis(timeout_ms))
{
Ok(line) => line,
Err(mpsc::RecvTimeoutError::Timeout) => {
timed_out = true;
discard_worker = true;
if let Some(child_ownership) = worker.child_ownership.as_mut() {
let _ = child_ownership.child.kill();
}
Err("managed runtime worker timed out".to_string())
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
discard_worker = true;
Err(managed_runtime_worker_stdout_closed_error(&mut worker))
}
},
None => match worker.stdout_rx.recv() {
Ok(line) => line,
Err(_) => {
discard_worker = true;
Err(managed_runtime_worker_stdout_closed_error(&mut worker))
}
},
};
let mut envelope = match line_result {
Ok(line) => match serde_json::from_str::<Value>(&line) {
Ok(envelope) => match validate_managed_runtime_worker_envelope(&envelope) {
Ok(()) => envelope,
Err(error) => {
discard_worker = true;
managed_runtime_worker_protocol_error_envelope(format!(
"managed runtime worker returned malformed JSON envelope: {error}"
))
}
},
Err(error) => {
discard_worker = true;
json!({
"ok": false,
"value": null,
"stdout": line,
"stderr": drain_managed_runtime_worker_stderr(&worker),
"error": format!("managed runtime worker returned invalid JSON envelope: {error}"),
})
}
},
Err(error) => json!({
"ok": false,
"value": null,
"stdout": "",
"stderr": drain_managed_runtime_worker_stderr(&worker),
"error": error,
}),
};
let raw_stderr = drain_managed_runtime_worker_stderr(&worker);
if !raw_stderr.is_empty() {
discard_worker = true;
merge_managed_runtime_worker_raw_stderr(&mut envelope, &raw_stderr);
}
(
worker,
ManagedRuntimeWorkerInvokeResult {
envelope,
timed_out,
worker_reused,
discard_worker,
},
)
}
fn merge_managed_runtime_worker_raw_stderr(envelope: &mut Value, raw_stderr: &str) {
let Some(object) = envelope.as_object_mut() else {
return;
};
let existing = object
.get("stderr")
.and_then(Value::as_str)
.unwrap_or_default();
let mut merged = String::new();
let mut dropped_bytes = 0usize;
append_bounded_worker_diagnostic(&mut merged, existing, &mut dropped_bytes);
if !merged.is_empty() {
append_bounded_worker_diagnostic(&mut merged, "\n", &mut dropped_bytes);
}
append_bounded_worker_diagnostic(&mut merged, raw_stderr, &mut dropped_bytes);
if dropped_bytes > 0 {
let marker = format!("\n[combined stderr truncated; dropped_bytes={dropped_bytes}]");
while merged.len().saturating_add(marker.len())
> MANAGED_RUNTIME_WORKER_PROTOCOL_LINE_LIMIT_BYTES
{
merged.pop();
}
merged.push_str(&marker);
}
object.insert("stderr".to_string(), Value::String(merged));
}
fn managed_runtime_worker_protocol_error_envelope(message: impl Into<String>) -> Value {
let error = message.into();
json!({
"ok": false,
"value": null,
"stdout": "",
"stderr": "",
"error": error,
})
}
fn managed_runtime_worker_envelope_object(
envelope: &Value,
) -> Result<&serde_json::Map<String, Value>, String> {
envelope
.as_object()
.ok_or_else(|| "managed runtime worker envelope must be a JSON object".to_string())
}
fn managed_runtime_worker_required_envelope_field<'a>(
object: &'a serde_json::Map<String, Value>,
field_name: &str,
) -> Result<&'a Value, String> {
object
.get(field_name)
.ok_or_else(|| format!("managed runtime worker envelope field `{field_name}` is required"))
}
fn managed_runtime_worker_required_bool_envelope_field(
object: &serde_json::Map<String, Value>,
field_name: &str,
) -> Result<bool, String> {
managed_runtime_worker_required_envelope_field(object, field_name)?
.as_bool()
.ok_or_else(|| {
format!("managed runtime worker envelope field `{field_name}` must be a boolean")
})
}
fn managed_runtime_worker_required_string_envelope_field<'a>(
object: &'a serde_json::Map<String, Value>,
field_name: &str,
) -> Result<&'a str, String> {
managed_runtime_worker_required_envelope_field(object, field_name)?
.as_str()
.ok_or_else(|| {
format!("managed runtime worker envelope field `{field_name}` must be a string")
})
}
fn validate_managed_runtime_worker_optional_nullable_string_field(
object: &serde_json::Map<String, Value>,
field_name: &str,
) -> Result<(), String> {
match object.get(field_name) {
None | Some(Value::Null) => Ok(()),
Some(Value::String(_)) => Ok(()),
Some(_) => Err(format!(
"managed runtime worker envelope field `{field_name}` must be a string or null"
)),
}
}
fn managed_runtime_worker_optional_u64_envelope_field(
object: &serde_json::Map<String, Value>,
field_name: &str,
) -> Result<u64, String> {
match object.get(field_name) {
None => Ok(0),
Some(value) => value.as_u64().ok_or_else(|| {
format!("managed runtime worker envelope field `{field_name}` must be a non-negative integer")
}),
}
}
fn validate_managed_runtime_worker_envelope(envelope: &Value) -> Result<(), String> {
let object = managed_runtime_worker_envelope_object(envelope)?;
let ok = managed_runtime_worker_required_bool_envelope_field(object, "ok")?;
managed_runtime_worker_required_envelope_field(object, "value")?;
managed_runtime_worker_required_string_envelope_field(object, "stdout")?;
managed_runtime_worker_required_string_envelope_field(object, "stderr")?;
managed_runtime_worker_optional_u64_envelope_field(object, "stdout_dropped_bytes")?;
managed_runtime_worker_optional_u64_envelope_field(object, "stderr_dropped_bytes")?;
validate_managed_runtime_worker_optional_nullable_string_field(object, "trace")?;
if !ok {
let error = managed_runtime_worker_required_string_envelope_field(object, "error")?;
if error.trim().is_empty() {
return Err(
"managed runtime worker envelope field `error` must be a non-empty string when `ok` is false"
.to_string(),
);
}
}
Ok(())
}
fn managed_runtime_worker_validated_result_to_json(
result: &ManagedRuntimeWorkerInvokeResult,
plan: &ManagedRuntimeEnvPlan,
) -> Result<Value, String> {
validate_managed_runtime_worker_envelope(&result.envelope)?;
let object = managed_runtime_worker_envelope_object(&result.envelope)?;
let ok =
managed_runtime_worker_required_bool_envelope_field(object, "ok")? && !result.timed_out;
let value = managed_runtime_worker_required_envelope_field(object, "value")?.clone();
let stdout = Value::String(
managed_runtime_worker_required_string_envelope_field(object, "stdout")?.to_string(),
);
let stderr = Value::String(
managed_runtime_worker_required_string_envelope_field(object, "stderr")?.to_string(),
);
let stdout_dropped_bytes =
managed_runtime_worker_optional_u64_envelope_field(object, "stdout_dropped_bytes")?;
let stderr_dropped_bytes =
managed_runtime_worker_optional_u64_envelope_field(object, "stderr_dropped_bytes")?;
let error = match object.get("error") {
Some(value) => value.clone(),
None => Value::Null,
};
let trace = match object.get("trace") {
Some(value) => value.clone(),
None => Value::Null,
};
Ok(json!({
"ok": ok,
"value": value,
"stdout": stdout,
"stderr": stderr,
"stdout_dropped_bytes": stdout_dropped_bytes,
"stderr_dropped_bytes": stderr_dropped_bytes,
"error": error,
"trace": trace,
"status": if result.timed_out { Value::Null } else { json!(0) },
"timed_out": result.timed_out,
"worker_reused": result.worker_reused,
"env_hash": plan.env_hash,
"env_dir": render_host_visible_path(&plan.env_dir),
}))
}
fn managed_runtime_worker_protocol_result_json(
error: String,
result: &ManagedRuntimeWorkerInvokeResult,
plan: &ManagedRuntimeEnvPlan,
) -> Value {
json!({
"ok": false,
"value": null,
"stdout": "",
"stderr": "",
"stdout_dropped_bytes": 0,
"stderr_dropped_bytes": 0,
"error": error,
"trace": Value::Null,
"status": if result.timed_out { Value::Null } else { json!(0) },
"timed_out": result.timed_out,
"worker_reused": result.worker_reused,
"env_hash": plan.env_hash,
"env_dir": render_host_visible_path(&plan.env_dir),
})
}
fn managed_runtime_worker_result_to_json(
result: ManagedRuntimeWorkerInvokeResult,
plan: &ManagedRuntimeEnvPlan,
) -> Value {
match managed_runtime_worker_validated_result_to_json(&result, plan) {
Ok(value) => value,
Err(error) => managed_runtime_worker_protocol_result_json(error, &result, plan),
}
}
fn managed_python_venv_executable(plan: &ManagedRuntimeEnvPlan) -> PathBuf {
if cfg!(windows) {
plan.env_dir
.join(".venv")
.join("Scripts")
.join("python.exe")
} else {
plan.env_dir.join(".venv").join("bin").join("python")
}
}
fn parse_managed_runtime_invoke_request(
value: LuaValue,
api_name: &str,
default_handler: &str,
default_timeout_ms: Option<u64>,
) -> Result<ManagedRuntimeInvokeRequest, mlua::Error> {
let table = require_table_arg(value, api_name, "spec")?;
let file = managed_runtime_optional_string_field(&table, api_name, "file")?
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| mlua::Error::runtime(format!("{api_name}: file is required")))?;
let handler = managed_runtime_optional_string_field(&table, api_name, "handler")?
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| default_handler.to_string());
let args_value: LuaValue = table
.get("args")
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let args = match args_value {
LuaValue::Nil => Value::Object(serde_json::Map::new()),
other => lua_value_to_json(&other)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: args: {error}")))?,
};
let timeout_ms = match managed_runtime_optional_timeout_ms(&table, api_name)? {
Some(0) => {
return Err(mlua::Error::runtime(format!(
"{api_name}: timeout_ms must be greater than zero"
)));
}
Some(value) => Some(value),
None => default_timeout_ms,
};
Ok(ManagedRuntimeInvokeRequest {
file,
handler,
args,
timeout_ms,
})
}
fn managed_runtime_status_from_plan(plan: &ManagedRuntimeEnvPlan) -> Value {
let persistent_session = current_managed_runtime_persistent_session_capability();
let readiness_result = managed_env_is_ready(plan);
match readiness_result {
Ok(ready) => json!({
"available": true,
"configured": true,
"ready": ready,
"runtime": plan.runtime.as_str(),
"runtime_version": plan.runtime_version,
"runtime_executable": render_host_visible_path(&plan.runtime_executable),
"package_manager": plan.package_manager,
"package_manager_version": plan.package_manager_version,
"package_manager_executable": render_host_visible_path(&plan.package_manager_executable),
"distribution_root": render_host_visible_path(&plan.distribution_root),
"distribution_source": plan.distribution_source.as_str(),
"environment_root": render_host_visible_path(&plan.environment_root),
"environment_source": plan.environment_source.as_str(),
"env_hash": plan.env_hash,
"env_dir": render_host_visible_path(&plan.env_dir),
"persistent_session": persistent_session,
"message": if ready {
"managed runtime environment is ready"
} else {
"managed runtime environment is configured but not yet created"
},
}),
Err(error) => json!({
"available": true,
"configured": true,
"ready": false,
"runtime": plan.runtime.as_str(),
"runtime_version": plan.runtime_version,
"runtime_executable": render_host_visible_path(&plan.runtime_executable),
"package_manager": plan.package_manager,
"package_manager_version": plan.package_manager_version,
"package_manager_executable": render_host_visible_path(&plan.package_manager_executable),
"distribution_root": render_host_visible_path(&plan.distribution_root),
"distribution_source": plan.distribution_source.as_str(),
"environment_root": render_host_visible_path(&plan.environment_root),
"environment_source": plan.environment_source.as_str(),
"env_hash": plan.env_hash,
"env_dir": render_host_visible_path(&plan.env_dir),
"persistent_session": persistent_session,
"message": "managed runtime environment status check failed",
"error": error,
}),
}
}
fn managed_python_worker_source() -> &'static str {
r#"
import contextlib
import importlib.util
import io
import json
import sys
import traceback
MODULE_CACHE = {}
ACTIVE_SNAPSHOT_ROOT = None
MAX_CAPTURE_BYTES = 256 * 1024
MAX_ENVELOPE_BYTES = 2 * 1024 * 1024
class BoundedTextCapture(io.TextIOBase):
"""Retain a bounded UTF-8 prefix while accounting for dropped bytes.
保留有界 UTF-8 前缀并统计丢弃字节。"""
def __init__(self):
"""Initialize one empty bounded capture.
初始化一个空的有界捕获器。"""
self._bytes = bytearray()
self.dropped_bytes = 0
def write(self, value):
"""Append text without allowing retained memory to exceed the hard limit.
追加文本且不允许保留内存超过硬上限。"""
text = str(value)
encoded = text.encode("utf-8", errors="replace")
remaining = max(0, MAX_CAPTURE_BYTES - len(self._bytes))
retained = min(remaining, len(encoded))
self._bytes.extend(encoded[:retained])
self.dropped_bytes += len(encoded) - retained
return len(text)
def flush(self):
"""Provide the TextIO flush contract without external buffering.
提供 TextIO flush 契约且不使用外部缓冲。"""
return None
def getvalue(self):
"""Decode the retained UTF-8 prefix for the response envelope.
为响应信封解码保留的 UTF-8 前缀。"""
return bytes(self._bytes).decode("utf-8", errors="replace")
def encode_envelope(envelope, stdout_buffer, stderr_buffer):
"""Serialize one bounded response and replace oversized or invalid values with an error.
序列化一个有界响应,并用错误替换过大或无效的值。"""
try:
encoded = json.dumps(envelope, ensure_ascii=False, separators=(",", ":"))
if len(encoded.encode("utf-8")) <= MAX_ENVELOPE_BYTES:
return encoded
error = f"managed runtime worker envelope exceeded {MAX_ENVELOPE_BYTES} bytes"
except Exception as exc:
error = f"handler result is not JSON serializable: {exc}"
fallback = {
"ok": False,
"value": None,
"stdout": stdout_buffer.getvalue(),
"stderr": stderr_buffer.getvalue(),
"stdout_dropped_bytes": stdout_buffer.dropped_bytes,
"stderr_dropped_bytes": stderr_buffer.dropped_bytes,
"error": error,
"trace": traceback.format_exc(),
}
return json.dumps(fallback, ensure_ascii=False, separators=(",", ":"))
def configure_snapshot_root(request):
"""Install exactly one immutable package import root for this pooled worker.
为当前池化 Worker 安装唯一不可变包导入根。"""
global ACTIVE_SNAPSHOT_ROOT
snapshot_root = request.get("snapshot_root")
if not isinstance(snapshot_root, str) or not snapshot_root:
raise RuntimeError("managed Python worker snapshot_root must be a non-empty string")
if ACTIVE_SNAPSHOT_ROOT is None:
sys.path[:] = [entry for entry in sys.path if entry not in ("", snapshot_root)]
sys.path.insert(0, snapshot_root)
ACTIVE_SNAPSHOT_ROOT = snapshot_root
elif ACTIVE_SNAPSHOT_ROOT != snapshot_root:
raise RuntimeError("managed Python worker snapshot_root changed within one worker")
def handle(request):
stdout_buffer = BoundedTextCapture()
stderr_buffer = BoundedTextCapture()
try:
configure_snapshot_root(request)
file_path = request["file"]
module = MODULE_CACHE.get(file_path)
if module is None:
spec = importlib.util.spec_from_file_location("_luaskills_python_entry_" + str(len(MODULE_CACHE)), file_path)
if spec is None or spec.loader is None:
raise RuntimeError(f"failed to load Python module: {file_path}")
module = importlib.util.module_from_spec(spec)
with contextlib.redirect_stdout(stdout_buffer), contextlib.redirect_stderr(stderr_buffer):
spec.loader.exec_module(module)
MODULE_CACHE[file_path] = module
handler = getattr(module, request.get("handler") or "main")
with contextlib.redirect_stdout(stdout_buffer), contextlib.redirect_stderr(stderr_buffer):
value = handler(request.get("args") or {}, request.get("ctx") or {})
envelope = {
"ok": True,
"value": value,
"stdout": stdout_buffer.getvalue(),
"stderr": stderr_buffer.getvalue(),
"stdout_dropped_bytes": stdout_buffer.dropped_bytes,
"stderr_dropped_bytes": stderr_buffer.dropped_bytes,
}
except Exception as exc:
envelope = {
"ok": False,
"value": None,
"stdout": stdout_buffer.getvalue(),
"stderr": stderr_buffer.getvalue(),
"stdout_dropped_bytes": stdout_buffer.dropped_bytes,
"stderr_dropped_bytes": stderr_buffer.dropped_bytes,
"error": str(exc),
"trace": traceback.format_exc(),
}
return encode_envelope(envelope, stdout_buffer, stderr_buffer)
for line in sys.stdin:
try:
request = json.loads(line)
encoded = handle(request)
except Exception as exc:
encoded = json.dumps({
"ok": False,
"value": None,
"stdout": "",
"stderr": "",
"stdout_dropped_bytes": 0,
"stderr_dropped_bytes": 0,
"error": str(exc),
"trace": traceback.format_exc(),
}, ensure_ascii=False, separators=(",", ":"))
sys.stdout.write(encoded + "\n")
sys.stdout.flush()
"#
}
fn managed_node_worker_source() -> &'static str {
r#"
const readline = require("readline");
const { pathToFileURL } = require("url");
const moduleCache = new Map();
const MAX_CAPTURE_BYTES = 256 * 1024;
const MAX_ENVELOPE_BYTES = 2 * 1024 * 1024;
// Create one bounded UTF-8 capture with explicit dropped-byte accounting.
// 创建一个带显式丢弃字节统计的有界 UTF-8 捕获器。
function createBoundedCapture() {
const chunks = [];
let retainedBytes = 0;
let droppedBytes = 0;
let lineCount = 0;
return {
write(value) {
const encoded = Buffer.from(String(value), "utf8");
const remaining = Math.max(0, MAX_CAPTURE_BYTES - retainedBytes);
const retained = Math.min(remaining, encoded.length);
if (retained > 0) {
chunks.push(encoded.subarray(0, retained));
retainedBytes += retained;
}
droppedBytes += encoded.length - retained;
},
writeLine(value) {
if (lineCount > 0) {
this.write("\n");
}
this.write(value);
lineCount += 1;
},
text() {
return Buffer.concat(chunks, retainedBytes).toString("utf8");
},
droppedBytes() {
return droppedBytes;
},
};
}
// Serialize one bounded response and replace oversized or invalid values with an error.
// 序列化一个有界响应,并用错误替换过大或无效的值。
function encodeEnvelope(envelope, stdout, stderr) {
try {
const encoded = JSON.stringify(envelope);
if (Buffer.byteLength(encoded, "utf8") <= MAX_ENVELOPE_BYTES) {
return encoded;
}
throw new Error(`managed runtime worker envelope exceeded ${MAX_ENVELOPE_BYTES} bytes`);
} catch (error) {
return JSON.stringify({
ok: false,
value: null,
stdout: stdout.text(),
stderr: stderr.text(),
stdout_dropped_bytes: stdout.droppedBytes(),
stderr_dropped_bytes: stderr.droppedBytes(),
error: error && error.message ? error.message : String(error),
trace: error && error.stack ? error.stack : null,
});
}
}
async function handle(request) {
const stdout = createBoundedCapture();
const stderr = createBoundedCapture();
// Preserve only the four methods replaced below. Copying and assigning the complete native
// Console object also rewrites its internal stream state and can crash Node 18.
// 仅保留下方会替换的四个方法。复制并回写完整原生 Console 对象还会改写其内部流状态,
// 并可能导致 Node 18 崩溃。
const originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
};
console.log = (...args) => stdout.writeLine(args.map(String).join(" "));
console.info = (...args) => stdout.writeLine(args.map(String).join(" "));
console.warn = (...args) => stderr.writeLine(args.map(String).join(" "));
console.error = (...args) => stderr.writeLine(args.map(String).join(" "));
try {
let module = moduleCache.get(request.file);
if (!module) {
module = await import(pathToFileURL(request.file).href);
moduleCache.set(request.file, module);
}
const handlerName = request.handler || "default";
const handler = handlerName === "default" ? (module.default || module.main) : module[handlerName];
if (typeof handler !== "function") {
throw new Error(`handler not found or not callable: ${handlerName}`);
}
const value = await handler(request.args || {}, request.ctx || {});
// JSON.stringify omits undefined object properties, so normalize absent handler results to null.
// JSON.stringify 会省略 undefined 对象属性,因此将 handler 的空返回归一化为 null。
const normalizedValue = value === undefined ? null : value;
return encodeEnvelope({
ok: true,
value: normalizedValue,
stdout: stdout.text(),
stderr: stderr.text(),
stdout_dropped_bytes: stdout.droppedBytes(),
stderr_dropped_bytes: stderr.droppedBytes(),
}, stdout, stderr);
} catch (error) {
return encodeEnvelope({
ok: false,
value: null,
stdout: stdout.text(),
stderr: stderr.text(),
stdout_dropped_bytes: stdout.droppedBytes(),
stderr_dropped_bytes: stderr.droppedBytes(),
error: error && error.message ? error.message : String(error),
trace: error && error.stack ? error.stack : null,
}, stdout, stderr);
} finally {
console.log = originalConsole.log;
console.info = originalConsole.info;
console.warn = originalConsole.warn;
console.error = originalConsole.error;
}
}
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
rl.on("line", async (line) => {
try {
const request = JSON.parse(line || "{}");
process.stdout.write(await handle(request) + "\n");
} catch (error) {
process.stdout.write(JSON.stringify({
ok: false,
value: null,
stdout: "",
stderr: "",
stdout_dropped_bytes: 0,
stderr_dropped_bytes: 0,
error: error && error.message ? error.message : String(error),
trace: error && error.stack ? error.stack : null,
}) + "\n");
}
});
"#
}
#[cfg(test)]
fn prepare_managed_node_import_root(
plan: &ManagedRuntimeEnvPlan,
package: &ManagedRuntimePackageContext,
) -> Result<ManagedPackageSnapshot, String> {
prepare_unleased_managed_package_snapshot(plan, package, ".ls-t")
}
#[cfg(test)]
fn copy_managed_node_package_import_root(source: &Path, destination: &Path) -> Result<(), String> {
crate::runtime::managed_runtime_session::copy_managed_package_tree(source, destination)
}
fn invoke_pooled_managed_runtime<F>(
worker_service: &ManagedRuntimeWorkerService,
key: ManagedRuntimeWorkerKey,
plan: &ManagedRuntimeEnvPlan,
payload: &Value,
timeout_ms: Option<u64>,
mut factory: F,
) -> Result<Value, String>
where
F: FnMut() -> Result<ManagedRuntimeWorker, String>,
{
let (worker, worker_reused) = worker_service.acquire(key.clone(), &mut factory)?;
let (worker, result) =
invoke_managed_runtime_worker(worker, payload, timeout_ms, worker_reused);
let discard_worker = result.discard_worker;
let result_json = managed_runtime_worker_result_to_json(result, plan);
if discard_worker {
worker_service.discard(&key, worker);
} else {
worker_service.release(key, worker);
}
Ok(result_json)
}
fn invoke_managed_python(lua: &Lua, spec: LuaValue) -> Result<LuaValue, mlua::Error> {
let api_name = "vulcan.runtime.python.invoke";
let package = current_lua_managed_package_context(lua, api_name)?;
let worker_service = current_lua_managed_runtime_worker_service(lua, api_name)?;
let request = parse_managed_runtime_invoke_request(
spec,
api_name,
"main",
worker_service.invoke_default_timeout_ms(),
)?;
let manifest = package.dependency_manifest().ok_or_else(|| {
mlua::Error::runtime(format!("{api_name}: dependencies.yaml is not present"))
})?;
let runtime_spec = manifest.python_runtime.as_ref().ok_or_else(|| {
mlua::Error::runtime(format!("{api_name}: python_runtime is not declared"))
})?;
let plan = resolve_python_env_plan(package.as_ref(), runtime_spec)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
ensure_managed_env(&plan)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let _source_file = package
.resolve_existing_file(&request.file, "file")
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let key = managed_runtime_worker_key(&plan, package.as_ref());
let package_snapshot = worker_service
.package_snapshot(&key, &plan, package.as_ref())
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let import_file = package_snapshot
.worker_source_path(&request.file)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let snapshot_root = package_snapshot
.python_worker_import_root()
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let payload = json!({
"file": import_file,
"snapshot_root": snapshot_root,
"handler": request.handler,
"args": request.args,
"ctx": package.worker_context_json(),
});
let result = invoke_pooled_managed_runtime(
worker_service.as_ref(),
key,
&plan,
&payload,
request.timeout_ms,
|| {
let mut command = Command::new(managed_python_venv_executable(&plan));
command.arg("-c").arg(managed_python_worker_source());
package_snapshot.configure_worker_command(&mut command)?;
configure_managed_python_command_environment(&mut command, &plan)?;
spawn_managed_runtime_worker_with_snapshot(
&mut command,
Some(Arc::clone(&package_snapshot)),
)
},
)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
json_value_to_lua(lua, &result)
}
fn invoke_managed_node(lua: &Lua, spec: LuaValue) -> Result<LuaValue, mlua::Error> {
let api_name = "vulcan.runtime.node.invoke";
let package = current_lua_managed_package_context(lua, api_name)?;
let worker_service = current_lua_managed_runtime_worker_service(lua, api_name)?;
let request = parse_managed_runtime_invoke_request(
spec,
api_name,
"default",
worker_service.invoke_default_timeout_ms(),
)?;
let manifest = package.dependency_manifest().ok_or_else(|| {
mlua::Error::runtime(format!("{api_name}: dependencies.yaml is not present"))
})?;
let runtime_spec = manifest
.node_runtime
.as_ref()
.ok_or_else(|| mlua::Error::runtime(format!("{api_name}: node_runtime is not declared")))?;
let plan = resolve_node_env_plan(package.as_ref(), runtime_spec)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
ensure_managed_env(&plan)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let _source_file = package
.resolve_existing_file(&request.file, "file")
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let key = managed_runtime_worker_key(&plan, package.as_ref());
let package_snapshot = worker_service
.package_snapshot(&key, &plan, package.as_ref())
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let import_file = package_snapshot
.node_worker_source_path(&request.file)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
let payload = json!({
"file": import_file,
"env_dir": plan.env_dir,
"handler": request.handler,
"args": request.args,
"ctx": package.worker_context_json(),
});
let result = invoke_pooled_managed_runtime(
worker_service.as_ref(),
key,
&plan,
&payload,
request.timeout_ms,
|| {
let mut command = Command::new(&plan.runtime_executable);
command.arg("-e").arg(managed_node_worker_source());
package_snapshot.configure_worker_command(&mut command)?;
configure_managed_node_command_environment(&mut command, &plan)?;
spawn_managed_runtime_worker_with_snapshot(
&mut command,
Some(Arc::clone(&package_snapshot)),
)
},
)
.map_err(|error| mlua::Error::runtime(format!("{api_name}: {error}")))?;
json_value_to_lua(lua, &result)
}
fn managed_python_status(lua: &Lua) -> Result<LuaValue, mlua::Error> {
let api_name = "vulcan.runtime.python.status";
let package = current_lua_managed_package_context(lua, api_name)?;
let Some(manifest) = package.dependency_manifest() else {
return json_value_to_lua(
lua,
&json!({
"available": false,
"configured": false,
"ready": false,
"runtime": "python",
"persistent_session": current_managed_runtime_persistent_session_capability(),
"message": "dependencies.yaml is not present",
}),
);
};
let Some(spec) = manifest.python_runtime.as_ref() else {
return json_value_to_lua(
lua,
&json!({
"available": false,
"configured": false,
"ready": false,
"runtime": "python",
"persistent_session": current_managed_runtime_persistent_session_capability(),
"message": "python_runtime is not declared",
}),
);
};
match resolve_python_env_plan(package.as_ref(), spec) {
Ok(plan) => json_value_to_lua(lua, &managed_runtime_status_from_plan(&plan)),
Err(error) => json_value_to_lua(
lua,
&json!({
"available": false,
"configured": true,
"ready": false,
"runtime": "python",
"persistent_session": current_managed_runtime_persistent_session_capability(),
"error": error,
}),
),
}
}
fn managed_node_status(lua: &Lua) -> Result<LuaValue, mlua::Error> {
let api_name = "vulcan.runtime.node.status";
let package = current_lua_managed_package_context(lua, api_name)?;
let Some(manifest) = package.dependency_manifest() else {
return json_value_to_lua(
lua,
&json!({
"available": false,
"configured": false,
"ready": false,
"runtime": "node",
"persistent_session": current_managed_runtime_persistent_session_capability(),
"message": "dependencies.yaml is not present",
}),
);
};
let Some(spec) = manifest.node_runtime.as_ref() else {
return json_value_to_lua(
lua,
&json!({
"available": false,
"configured": false,
"ready": false,
"runtime": "node",
"persistent_session": current_managed_runtime_persistent_session_capability(),
"message": "node_runtime is not declared",
}),
);
};
match resolve_node_env_plan(package.as_ref(), spec) {
Ok(plan) => json_value_to_lua(lua, &managed_runtime_status_from_plan(&plan)),
Err(error) => json_value_to_lua(
lua,
&json!({
"available": false,
"configured": true,
"ready": false,
"runtime": "node",
"persistent_session": current_managed_runtime_persistent_session_capability(),
"error": error,
}),
),
}
}
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 {}: {}",
render_log_friendly_path(&file_path),
error
)
})
}
struct LuaHelpPayloadSource {
helper_path: PathBuf,
source: String,
}
fn read_lua_help_payload_source(
skill_dir: &Path,
relative_path: &str,
) -> Result<LuaHelpPayloadSource, String> {
let helper_path = skill_dir.join(relative_path);
let source = std::fs::read_to_string(&helper_path).map_err(|error| {
format!(
"Failed to read help file {}: {}",
render_log_friendly_path(&helper_path),
error
)
})?;
Ok(LuaHelpPayloadSource {
helper_path,
source,
})
}
fn render_lua_help_payload_text(
lua: &Lua,
helper_path: &Path,
helper_source: &str,
chunk_name: &str,
) -> Result<String, String> {
let chunk = lua.load(helper_source).set_name(chunk_name);
let exported: LuaValue = chunk
.into_function()
.map_err(|error| {
format!(
"Help compile error for {}: {}",
render_log_friendly_path(helper_path),
error
)
})?
.call(())
.map_err(|error| {
format!(
"Help init error for {}: {}",
render_log_friendly_path(helper_path),
error
)
})?;
let rendered_value = match exported {
LuaValue::Function(function) => function.call(()).map_err(|error| {
format!(
"Help runtime error for {}: {}",
render_log_friendly_path(helper_path),
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: {}",
render_log_friendly_path(helper_path),
error
)
}),
other => Err(format!(
"Help {} must return a plain string, actual_type='{}'",
render_log_friendly_path(helper_path),
lua_value_type_name(&other)
)),
}
}
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,
runtime_python: Table,
runtime_node: 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))?,
runtime_python: runtime
.get("python")
.map_err(|error| format!("Failed to get vulcan.runtime.python: {}", error))?,
runtime_node: runtime
.get("node")
.map_err(|error| format!("Failed to get vulcan.runtime.node: {}", 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.runtime
.set("python", self.runtime_python.clone())
.map_err(|error| format!("Failed to restore vulcan.runtime.python: {}", error))?;
self.runtime
.set("node", self.runtime_node.clone())
.map_err(|error| format!("Failed to restore vulcan.runtime.node: {}", 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 capture_provider_skill_marker(vulcan: &Table, marker_key: &str) -> Result<String, String> {
vulcan
.get(marker_key)
.map_err(|error| format!("Failed to read vulcan.{}: {}", marker_key, error))
}
struct RestoredLuaProviderContexts<'a> {
lancedb_binding: Option<Arc<LanceDbSkillBinding>>,
lancedb_skill_name: Option<&'a str>,
sqlite_binding: Option<Arc<SqliteSkillBinding>>,
sqlite_skill_name: Option<&'a str>,
}
fn resolve_restored_lua_provider_contexts<'a>(
lancedb_host: Option<&Arc<LanceDbSkillHost>>,
sqlite_host: Option<&Arc<SqliteSkillHost>>,
lancedb_skill_marker: &'a str,
sqlite_skill_marker: &'a str,
) -> Result<RestoredLuaProviderContexts<'a>, String> {
let lancedb_skill_name = non_empty_skill_name(lancedb_skill_marker);
let sqlite_skill_name = non_empty_skill_name(sqlite_skill_marker);
let lancedb_binding = match (lancedb_host, lancedb_skill_name) {
(Some(host), Some(skill_name)) => host.binding_for_skill(skill_name)?,
_ => None,
};
let sqlite_binding = match (sqlite_host, sqlite_skill_name) {
(Some(host), Some(skill_name)) => host.binding_for_skill(skill_name)?,
_ => None,
};
Ok(RestoredLuaProviderContexts {
lancedb_binding,
lancedb_skill_name,
sqlite_binding,
sqlite_skill_name,
})
}
fn restore_lua_nested_provider_contexts(
lua: &Lua,
lancedb_host: Option<&Arc<LanceDbSkillHost>>,
sqlite_host: Option<&Arc<SqliteSkillHost>>,
lancedb_skill_marker: &str,
sqlite_skill_marker: &str,
) -> Result<(), String> {
let RestoredLuaProviderContexts {
lancedb_binding,
lancedb_skill_name,
sqlite_binding,
sqlite_skill_name,
} = resolve_restored_lua_provider_contexts(
lancedb_host,
sqlite_host,
lancedb_skill_marker,
sqlite_skill_marker,
)?;
LuaEngine::populate_vulcan_lancedb_context(lua, lancedb_binding, lancedb_skill_name)?;
LuaEngine::populate_vulcan_sqlite_context(lua, sqlite_binding, sqlite_skill_name)?;
Ok(())
}
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_anonymous_lua_context(
lua,
AnonymousLuaExecutionContext {
invocation_context: None,
internal_context: VulcanInternalExecutionContext::default(),
entry_file: None,
dependency_context: AnonymousLuaDependencyContext::ClearWithHostOptions(host_options),
managed_package_context: AnonymousLuaManagedPackageContext::Clear,
},
)?;
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) = guard
.lua()
.and_then(|lua| reset_pooled_vm_request_scope(lua, host_options))
{
guard.lease.discard();
guard.active = false;
return Err(error);
}
Ok(guard)
}
fn lua(&self) -> Result<&Lua, String> {
self.lease.lua()
}
fn finish(mut self) -> Result<(), String> {
let cleanup_result = self
.lua()
.and_then(|lua| reset_pooled_vm_request_scope(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) = self
.lua()
.and_then(|lua| reset_pooled_vm_request_scope(lua, self.host_options))
{
log_error(format!(
"[LuaSkill:error] Failed to reset pooled Lua VM request scope: {}",
error
));
self.lease.discard();
}
}
}
fn finish_pooled_vm_request_scope<T>(
main_result: Result<T, String>,
scope_guard: LuaVmRequestScopeGuard<'_>,
cleanup_error_label: &str,
) -> Result<T, String> {
let cleanup_result = scope_guard.finish();
match (main_result, cleanup_result) {
(Ok(result), Ok(())) => Ok(result),
(Ok(_), Err(cleanup_error)) => Err(cleanup_error),
(Err(main_error), Ok(())) => Err(main_error),
(Err(main_error), Err(cleanup_error)) => Err(format!(
"{}; {}: {}",
main_error, cleanup_error_label, cleanup_error
)),
}
}
struct LuaNestedCallScopeGuard {
lua: Lua,
host_options: Arc<LuaRuntimeHostOptions>,
lancedb_host: Option<Arc<LanceDbSkillHost>>,
sqlite_host: Option<Arc<SqliteSkillHost>>,
previous_state: LuaNestedOuterStateSnapshot,
active: bool,
}
struct PreparedLuaNestedCallScope {
guard: LuaNestedCallScopeGuard,
invocation_context: LuaInvocationContext,
}
impl LuaNestedCallScopeGuard {
fn new(
lua: &Lua,
host_options: Arc<LuaRuntimeHostOptions>,
lancedb_host: Option<Arc<LanceDbSkillHost>>,
sqlite_host: Option<Arc<SqliteSkillHost>>,
) -> Result<Self, String> {
Ok(Self {
lua: lua.clone(),
host_options,
lancedb_host,
sqlite_host,
previous_state: capture_lua_nested_outer_state(lua)?,
active: true,
})
}
fn previous_internal_context(&self) -> &VulcanInternalExecutionContext {
&self.previous_state.internal_context
}
fn previous_invocation_context(&self) -> mlua::Result<LuaInvocationContext> {
let request_context_json = lua_value_to_json(&self.previous_state.context.request)
.map_err(mlua::Error::runtime)?;
let request_context =
parse_runtime_request_context_json(request_context_json, "vulcan.context.request")
.map_err(mlua::Error::runtime)?;
let client_budget = lua_value_to_json(&self.previous_state.context.client_budget)
.map_err(mlua::Error::runtime)?;
let tool_config = lua_value_to_json(&self.previous_state.context.tool_config)
.map_err(mlua::Error::runtime)?;
Ok(LuaInvocationContext::new(
request_context,
client_budget,
tool_config,
))
}
fn enter_nested_call(&self, target: LuaNestedCallTarget<'_>) -> Result<(), String> {
LuaEngine::populate_vulcan_request_context(&self.lua, Some(target.invocation_context))?;
let nested_internal_context =
build_lua_nested_internal_execution_context(&target, self.previous_internal_context());
populate_vulcan_internal_execution_context(&self.lua, &nested_internal_context)?;
populate_lua_nested_resource_contexts(&self.lua, self.host_options.as_ref(), target)?;
Ok(())
}
fn restore_previous_state(&self) -> Result<(), String> {
replace_lua_managed_package_context(&self.lua, self.previous_state.managed_package.clone());
self.previous_state.core_state.restore(&self.lua)?;
restore_lua_nested_provider_contexts(
&self.lua,
self.lancedb_host.as_ref(),
self.sqlite_host.as_ref(),
&self.previous_state.lancedb_skill_name,
&self.previous_state.sqlite_skill_name,
)?;
restore_vulcan_context_snapshot(&self.lua, &self.previous_state.context)?;
populate_vulcan_internal_execution_context(&self.lua, self.previous_internal_context())?;
restore_vulcan_file_context_snapshot(&self.lua, &self.previous_state.file_context)?;
restore_lua_nested_dependency_context(
&self.lua,
self.host_options.as_ref(),
&self.previous_state.file_context,
self.previous_internal_context(),
)?;
Ok(())
}
fn finish(mut self) -> Result<(), String> {
let restore_result = self.restore_previous_state();
self.active = false;
restore_result
}
fn finish_nested_call<T>(self, call_result: mlua::Result<T>) -> mlua::Result<T> {
let restore_result = self.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
))),
}
}
}
fn prepare_lua_nested_call_scope(
lua: &Lua,
host_options: Arc<LuaRuntimeHostOptions>,
lancedb_host: Option<Arc<LanceDbSkillHost>>,
sqlite_host: Option<Arc<SqliteSkillHost>>,
) -> mlua::Result<PreparedLuaNestedCallScope> {
let guard = LuaNestedCallScopeGuard::new(lua, host_options, lancedb_host, sqlite_host)
.map_err(mlua::Error::runtime)?;
let invocation_context = guard.previous_invocation_context()?;
Ok(PreparedLuaNestedCallScope {
guard,
invocation_context,
})
}
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) -> Result<&Lua, String> {
self.vm
.as_ref()
.map(|vm| &vm.lua)
.ok_or_else(|| "pooled Lua VM lease has already been retired".to_string())
}
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.lock_state();
state.total_count += 1;
}
match factory() {
Ok(vm) => self.release(vm),
Err(error) => {
let mut state = self.lock_state();
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.lock_state();
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.lock_state();
state.total_count = state.total_count.saturating_sub(1);
self.condvar.notify_one();
return Err(error);
}
}
}
drop(self.wait_state(state));
}
}
fn release(&self, vm: LuaVm) {
let mut state = self.lock_state();
state.available.push(vm);
self.reap_idle_locked(&mut state);
self.condvar.notify_one();
}
fn discard(&self, _vm: LuaVm) {
let mut state = self.lock_state();
if state.total_count > 0 {
state.total_count -= 1;
}
self.condvar.notify_one();
}
fn total_count(&self) -> usize {
self.lock_state().total_count
}
fn lock_state(&self) -> MutexGuard<'_, LuaVmPoolState> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn wait_state<'a>(
&self,
state: MutexGuard<'a, LuaVmPoolState>,
) -> MutexGuard<'a, LuaVmPoolState> {
self.condvar
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
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 {
pub fn poll_managed_session_events(
&self,
max_events: usize,
) -> Result<RuntimeManagedSessionEventBatch, String> {
self.managed_runtime_services
.event_center()
.poll(max_events)
}
pub fn wait_managed_session_events(
&self,
max_events: usize,
timeout: Duration,
) -> Result<RuntimeManagedSessionEventBatch, String> {
let timeout_ms = timeout.as_nanos().div_ceil(1_000_000);
let timeout_ms = u64::try_from(timeout_ms)
.map_err(|_| "managed session event timeout is too large".to_string())?;
self.managed_runtime_services
.event_center()
.wait(max_events, timeout_ms)
}
pub fn set_managed_session_wake_callback(
&self,
callback: Option<RuntimeManagedSessionWakeCallback>,
) -> Result<(), String> {
self.managed_runtime_services
.event_center()
.set_wake_callback(callback)
}
pub(crate) fn managed_session_event_center(&self) -> Arc<ManagedSessionEventCenter> {
self.managed_runtime_services.event_center()
}
fn retire_managed_runtime_owner(&self, owner_token: u64) -> Result<(), String> {
let session_result = self.managed_runtime_services.retire_owner(owner_token);
self.managed_runtime_workers.retire_owner(owner_token);
session_result
}
fn retire_runtime_session_manager_owners(
&self,
manager: &RuntimeSessionManager,
operation: &str,
) {
let owner_tokens = match manager.take_retired_owner_tokens() {
Ok(owner_tokens) => owner_tokens,
Err(error) => {
log_error(format!(
"[LuaSkill] Failed to drain managed runtime owner retirements after {operation}: {}",
error.message
));
return;
}
};
for owner_token in owner_tokens {
if let Err(error) = self.retire_managed_runtime_owner(owner_token) {
log_error(format!(
"[LuaSkill] Failed to retire managed runtime owner {owner_token} after {operation}: {error}"
));
}
}
}
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 runtime_skill_root_dir_is_directory(root: &RuntimeSkillRoot) -> Result<bool, String> {
match fs::metadata(&root.skills_dir) {
Ok(metadata) if metadata.is_dir() => Ok(true),
Ok(_) => Err(format!(
"skill root '{}' is not a directory: {}",
root.name,
render_log_friendly_path(&root.skills_dir)
)),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
Err(error) => Err(format!(
"failed to inspect skill root '{}' at {}: {}",
root.name,
render_log_friendly_path(&root.skills_dir),
error
)),
}
}
fn any_runtime_skill_root_dir_exists(skill_roots: &[RuntimeSkillRoot]) -> Result<bool, String> {
for root in skill_roots {
if Self::runtime_skill_root_dir_is_directory(root)? {
return Ok(true);
}
}
Ok(false)
}
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 LuaEngineOptions {
pool_config,
host_options,
} = options;
let host_options = host_options.normalized();
host_options
.managed_runtime_config
.validate()
.map_err(std::io::Error::other)?;
let managed_runtime_roots =
resolve_engine_managed_runtime_roots(&host_options).map_err(std::io::Error::other)?;
let _default_text_encoding =
resolve_host_default_text_encoding(&host_options).map_err(std::io::Error::other)?;
let runlua_pool_config = 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(
host_options
.cache_config
.clone()
.unwrap_or_else(ToolCacheConfig::default),
);
let native_library_search_guard =
NativeLibrarySearchGuard::new(&host_options).map_err(std::io::Error::other)?;
let database_provider_callbacks =
Arc::new(RuntimeDatabaseProviderCallbacks::capture_process_defaults());
let managed_runtime_services =
ManagedRuntimeServices::new_with_config(host_options.managed_runtime_config)
.map_err(std::io::Error::other)?;
let managed_runtime_workers =
ManagedRuntimeWorkerService::new_with_config(host_options.managed_runtime_config)
.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(pool_config)),
runlua_pool: Arc::new(LuaVmPool::new(runlua_pool_config)),
public_runtime_sessions: Arc::new(RuntimeSessionManager::new()),
system_runtime_sessions: Arc::new(RuntimeSessionManager::new()),
managed_runtime_services,
managed_runtime_workers,
managed_runtime_roots,
skill_config_store: Arc::new(
SkillConfigStore::new(host_options.skill_config_file_path.clone())
.map_err(std::io::Error::other)?,
),
lancedb_host: None,
sqlite_host: None,
database_provider_callbacks,
native_library_search_guard,
host_options: Arc::new(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 managed_runtime_roots_for(
&self,
runtime_root: &Path,
) -> Result<Arc<ManagedRuntimeRoots>, String> {
match self.managed_runtime_roots.as_ref() {
Some(roots) => Ok(Arc::clone(roots)),
None => Ok(Arc::new(ManagedRuntimeRoots::new(
runtime_root,
None,
None,
)?)),
}
}
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)),
public_runtime_sessions: Arc::new(RuntimeSessionManager::new()),
system_runtime_sessions: Arc::clone(&self.system_runtime_sessions),
managed_runtime_services: Arc::clone(&self.managed_runtime_services),
managed_runtime_workers: Arc::clone(&self.managed_runtime_workers),
managed_runtime_roots: self.managed_runtime_roots.clone(),
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(),
native_library_search_guard: NativeLibrarySearchGuard::new(&self.host_options)
.map_err(std::io::Error::other)?,
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.public_runtime_sessions = next.public_runtime_sessions;
self.managed_runtime_roots = next.managed_runtime_roots;
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.native_library_search_guard = next.native_library_search_guard;
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"));
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,
official_skill_hub_base_url: self.host_options.official_skill_hub_base_url.clone(),
enable_private_url_skill_install: self.host_options.enable_private_url_skill_install,
private_skill_source_allowlist: self
.host_options
.private_skill_source_allowlist
.clone(),
}))
}
fn skill_manager_for_with_progress(
&self,
skill_root: &RuntimeSkillRoot,
progress: RuntimeSkillOperationProgressEmitter,
) -> 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_with_progress(
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,
official_skill_hub_base_url: self.host_options.official_skill_hub_base_url.clone(),
enable_private_url_skill_install: self
.host_options
.enable_private_url_skill_install,
private_skill_source_allowlist: self
.host_options
.private_skill_source_allowlist
.clone(),
},
Some(progress),
))
}
fn ensure_skill_dependencies(
&self,
skill_root: &RuntimeSkillRoot,
skill_dir: &Path,
) -> Result<Option<PackageDependencyManifest>, String> {
let Some(manifest) = self.load_skill_dependency_manifest(skill_dir)? else {
return Ok(None);
};
if manifest.is_empty() {
return Ok(Some(manifest));
}
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)?;
Ok(Some(manifest))
}
fn load_skill_dependency_manifest(
&self,
skill_dir: &Path,
) -> Result<Option<PackageDependencyManifest>, String> {
let dependencies_path = skill_dir.join("dependencies.yaml");
if !skill_dependency_manifest_path_exists(&dependencies_path)? {
return Ok(None);
}
PackageDependencyManifest::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,
render_log_friendly_path(&target_root.skills_dir)
)
})
}
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(std::slice::from_ref(root), 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,
render_log_friendly_path(&root_instance.actual_dir),
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())
.ok_or_else(|| {
format!(
"loaded skill '{}' has invalid directory name: {}",
skill.meta.effective_skill_id(),
render_log_friendly_path(&skill.dir)
)
})?
.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 !Self::any_runtime_skill_root_dir_exists(skill_roots)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
{
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
));
let dependency_manifest =
match self.ensure_skill_dependencies(&resolved_root, &actual_dir) {
Ok(manifest) => manifest,
Err(error) => {
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,
dependency_manifest,
) {
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(RunLuaVmBuildContext::from_engine(
self,
&self.skills,
&self.entry_registry,
))
})
.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()
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
let retired_skill_owner_tokens: Vec<_> = self
.skills
.values()
.map(|skill| skill.managed_package.owner_token())
.collect();
let mut next = self.empty_reload_candidate()?;
next.load_from_roots(skill_roots)?;
self.replace_runtime_state_from(next);
for owner_token in retired_skill_owner_tokens {
if let Err(error) = self.retire_managed_runtime_owner(owner_token) {
log_error(format!(
"[LuaSkill] Failed to retire managed runtime owner {owner_token} after Skill reload: {error}"
));
}
}
self.emit_entry_registry_delta(previous_entries)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
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 {
self.load_skill_dependency_manifest(&resolved_instance.actual_dir)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?
} else {
None
};
if let Err(error) = manager.guard_operation(plane, action, skill_id) {
self.emit_skill_lifecycle_event(SkillLifecycleEventDraft {
plane,
action,
skill_id,
root_name: Some(resolved_instance.root_name.clone()),
skill_dir: Some(render_host_visible_path(&resolved_instance.actual_dir)),
status: "blocked",
message: 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(SkillLifecycleEventDraft {
plane,
action,
skill_id,
root_name: Some(resolved_instance.root_name.clone()),
skill_dir: Some(render_host_visible_path(&resolved_instance.actual_dir)),
status: "failed",
message: 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(SkillLifecycleEventDraft {
plane,
action,
skill_id,
root_name: Some(resolved_instance.root_name.clone()),
skill_dir: Some(render_host_visible_path(&resolved_instance.actual_dir)),
status: "completed",
message: 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);
match fs::remove_dir_all(&database_dir) {
Ok(()) => Ok((true, false)),
Err(error) if error.kind() == ErrorKind::NotFound => Ok((false, false)),
Err(error) => Err(format!(
"failed to remove {database_label} directory {}: {}",
render_log_friendly_path(&database_dir),
error
)
.into()),
}
}
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 removed_dependency_manifest = self
.load_skill_dependency_manifest(&resolved_instance.actual_dir)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
if let Err(error) = manager.guard_operation(
plane,
crate::skill::manager::SkillLifecycleAction::Uninstall,
skill_id,
) {
self.emit_skill_lifecycle_event(SkillLifecycleEventDraft {
plane,
action: crate::skill::manager::SkillLifecycleAction::Uninstall,
skill_id,
root_name: Some(resolved_instance.root_name.clone()),
skill_dir: Some(render_host_visible_path(&resolved_instance.actual_dir)),
status: "blocked",
message: 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(SkillLifecycleEventDraft {
plane,
action: crate::skill::manager::SkillLifecycleAction::Uninstall,
skill_id,
root_name: Some(resolved_instance.root_name.clone()),
skill_dir: Some(render_host_visible_path(&resolved_instance.actual_dir)),
status: "failed",
message: 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 message = format_lifecycle_recovery_error(
format!(
"Failed to reload LuaSkills after uninstall: {}",
reload_error
),
rollback_error,
restore_error,
);
self.emit_skill_lifecycle_event(SkillLifecycleEventDraft {
plane,
action: crate::skill::manager::SkillLifecycleAction::Uninstall,
skill_id,
root_name: Some(resolved_instance.root_name.clone()),
skill_dir: Some(render_host_visible_path(&resolved_instance.actual_dir)),
status: "failed",
message: 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 message = format_lifecycle_recovery_error(
format!("Failed to finalize uninstall: {}", error),
rollback_error,
restore_error,
);
self.emit_skill_lifecycle_event(SkillLifecycleEventDraft {
plane,
action: crate::skill::manager::SkillLifecycleAction::Uninstall,
skill_id,
root_name: Some(resolved_instance.root_name.clone()),
skill_dir: Some(render_host_visible_path(&resolved_instance.actual_dir)),
status: "failed",
message: 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(SkillLifecycleEventDraft {
plane,
action: crate::skill::manager::SkillLifecycleAction::Uninstall,
skill_id,
root_name: Some(resolved_instance.root_name.clone()),
skill_dir: Some(render_host_visible_path(&resolved_instance.actual_dir)),
status: "completed",
message: 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>> {
let apply_action = SkillApplyLifecycleAction::from_lifecycle_action(action)
.map_err(|error| -> Box<dyn std::error::Error> { error.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 apply_action {
SkillApplyLifecycleAction::Install => {
if let Some(target_root) = explicit_target_root {
target_root.clone()
} else {
self.default_install_skill_root(plane, skill_roots)?.clone()
}
}
SkillApplyLifecycleAction::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,
}
}
};
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 apply_action == SkillApplyLifecycleAction::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 apply_action == SkillApplyLifecycleAction::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 progress = RuntimeSkillOperationProgressEmitter::new(
plane,
action,
Some(target_root.name.clone()),
Some(requested_skill_id.clone()),
)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
progress.emit(
"validating_request",
"completed",
Some("skill lifecycle request accepted".to_string()),
);
let manager = match self.skill_manager_for_with_progress(&target_root, progress.clone()) {
Ok(manager) => manager,
Err(error) => {
progress.emit("failed", "failed", Some(error.clone()));
return Err(error.into());
}
};
let prepared = match apply_action {
SkillApplyLifecycleAction::Install => {
match manager.prepare_install_skill(plane, operation_roots, request) {
Ok(prepared) => prepared,
Err(error) => {
progress.emit("failed", "failed", Some(error.clone()));
return Err(error.into());
}
}
}
SkillApplyLifecycleAction::Update => {
match manager.prepare_update_skill(plane, operation_roots, request) {
Ok(prepared) => prepared,
Err(error) => {
progress.emit("failed", "failed", Some(error.clone()));
return Err(error.into());
}
}
}
};
let mut result = match &prepared {
PreparedSkillApply::Immediate(result) => result.clone(),
PreparedSkillApply::Install(_) | PreparedSkillApply::Update(_) => {
progress.emit(
"reloading_runtime",
"started",
Some("reloading LuaSkills runtime after staged change".to_string()),
);
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 message = format_lifecycle_recovery_error(
format!(
"Failed to reload LuaSkills after {:?}: {}",
action, reload_error
),
rollback_error,
restore_error,
);
progress.emit("failed", "failed", Some(message.clone()));
return Err(message.into());
}
progress.emit(
"committing",
"started",
Some("committing staged skill lifecycle change".to_string()),
);
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 message = format_lifecycle_recovery_error(
format!("Failed to finalize {:?}: {}", action, error),
rollback_error,
restore_error,
);
progress.emit("failed", "failed", Some(message.clone()));
message.into()
},
)?;
progress.emit(
"committing",
"completed",
Some("skill lifecycle change committed".to_string()),
);
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(SkillLifecycleEventDraft {
plane,
action,
skill_id: &result.skill_id,
root_name: resolved_instance
.as_ref()
.map(|instance| instance.root_name.clone()),
skill_dir: resolved_instance
.as_ref()
.map(|instance| render_host_visible_path(&instance.actual_dir)),
status: &result.status,
message: Some(result.message.clone()),
});
progress.emit("completed", "completed", 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, event: SkillLifecycleEventDraft<'_>) {
crate::host::callbacks::emit_skill_lifecycle_event(&RuntimeSkillLifecycleEvent {
plane: event.plane,
action: event.action,
skill_id: event.skill_id.to_string(),
root_name: event.root_name,
skill_dir: event.skill_dir,
status: event.status.to_string(),
message: event.message,
});
}
fn emit_entry_registry_delta(
&self,
previous_entries: Vec<RuntimeEntryDescriptor>,
) -> Result<(), String> {
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 Ok(());
}
crate::host::callbacks::emit_entry_registry_delta(&RuntimeEntryRegistryDelta {
added_entries,
removed_entry_names,
updated_entries,
});
Ok(())
}
fn load_single_skill(
&mut self,
dir: &Path,
root_name: &str,
dependency_manifest: Option<PackageDependencyManifest>,
) -> Result<(), Box<dyn std::error::Error>> {
let skill_yaml = dir.join("skill.yaml");
let skill_yaml_exists = required_skill_file_path_exists(&skill_yaml, "skill.yaml", dir)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
if !skill_yaml_exists {
return Err(
format!("skill.yaml not found in {}", render_log_friendly_path(dir)).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",
render_log_friendly_path(dir)
)
.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: {}",
render_log_friendly_path(dir)
)
})?
.trim()
.to_string();
validate_luaskills_identifier(&directory_skill_id, "skill directory name")
.map_err(|error| format!("skill {}: {}", render_log_friendly_path(dir), 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(());
}
meta.resolve_entry_input_schemas(dir)
.map_err(|error| format!("skill {}: {}", meta.name, error))?;
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);
let lua_entry_label = format!("Lua entry {}", tool.lua_entry);
let lua_entry_exists =
required_skill_file_path_exists(&lua_path, &lua_entry_label, dir)
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
if !lua_entry_exists {
return Err(format!(
"Lua entry {} not found in {}",
tool.lua_entry,
render_log_friendly_path(dir)
)
.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
};
let skill_root = self
.runtime_skill_roots
.iter()
.find(|skill_root| skill_root.name == root_name)
.ok_or_else(|| format!("runtime skill root `{root_name}` is not registered"))?;
let runtime_root = self.runtime_root_for(skill_root);
let managed_runtime_roots = self.managed_runtime_roots_for(&runtime_root)?;
let managed_package = ManagedRuntimePackageContext::for_skill_with_roots(
meta.effective_skill_id(),
dir,
managed_runtime_roots,
dependency_manifest,
)?;
self.skills.insert(
meta.effective_skill_id().to_string(),
LoadedSkill {
meta,
dir: dir.to_path_buf(),
root_name: root_name.to_string(),
managed_package,
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() };
lua.set_app_data(Arc::clone(&self.managed_runtime_services));
lua.set_app_data(Arc::clone(&self.managed_runtime_workers));
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,
RunLuaRuntimeContext::from_engine(self, skills.clone(), entry_registry.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(context: RunLuaVmBuildContext<'_>) -> Result<LuaVm, String> {
let RunLuaVmBuildContext {
skills,
entry_registry,
host_options,
skill_config_store,
runtime_skill_roots,
lancedb_host,
sqlite_host,
managed_runtime_services,
managed_runtime_workers,
} = context;
let lua = unsafe { Lua::unsafe_new() };
lua.set_app_data(managed_runtime_services);
lua.set_app_data(managed_runtime_workers);
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 create_system_runtime_vm(&self) -> Result<LuaVm, String> {
let skills = HashMap::new();
let entry_registry = BTreeMap::new();
let vm = Self::create_runlua_vm(RunLuaVmBuildContext {
skills: &skills,
entry_registry: &entry_registry,
host_options: Arc::clone(&self.host_options),
skill_config_store: Arc::clone(&self.skill_config_store),
runtime_skill_roots: Vec::new(),
lancedb_host: None,
sqlite_host: None,
managed_runtime_services: Arc::clone(&self.managed_runtime_services),
managed_runtime_workers: Arc::clone(&self.managed_runtime_workers),
})?;
lease::install_system_runtime_context_boundary(&vm.lua)?;
Ok(vm)
}
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 {}: {}",
render_log_friendly_path(&lua_path),
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(lua_skill_handler_global_name(&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) -> Result<Vec<RuntimeEntryDescriptor>, String> {
let mut descriptors = Vec::with_capacity(self.entry_registry.len());
for target in self.entry_registry.values() {
let skill = self.skills.get(&target.skill_storage_key).ok_or_else(|| {
format!(
"entry registry target '{}' references missing loaded skill storage key '{}'",
target.canonical_name, target.skill_storage_key
)
})?;
let tool = skill
.meta
.find_tool_by_local_name(&target.local_name)
.ok_or_else(|| {
format!(
"entry registry target '{}' references missing local entry '{}' in skill '{}'",
target.canonical_name, target.local_name, target.skill_id
)
})?;
let input_schema = tool.resolved_input_schema().map_err(|error| {
format!(
"entry registry target '{}' has invalid resolved input schema: {}",
target.canonical_name, error
)
})?;
descriptors.push(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: render_host_visible_path(&skill.dir),
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(),
input_schema: input_schema.clone(),
});
}
Ok(descriptors)
}
pub fn list_entries_for_authority(
&self,
authority: SkillManagementAuthority,
) -> Result<Vec<RuntimeEntryDescriptor>, String> {
Ok(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) -> Result<Vec<RuntimeSkillHelpDescriptor>, String> {
let mut descriptors = Vec::with_capacity(self.skills.len());
for skill in self.skills.values() {
let main = self.build_help_node_descriptor(skill, skill.meta.main_help(), true)?;
let mut flows = Vec::new();
for topic in skill.meta.help_topics() {
flows.push(self.build_help_node_descriptor(skill, topic, false)?);
}
descriptors.push(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: render_host_visible_path(&skill.dir),
main,
flows,
});
}
descriptors.sort_by(|left, right| left.skill_id.cmp(&right.skill_id));
Ok(descriptors)
}
pub fn list_skill_help_for_authority(
&self,
authority: SkillManagementAuthority,
) -> Result<Vec<RuntimeSkillHelpDescriptor>, String> {
Ok(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: render_host_visible_path(&skill.dir),
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,
) -> Result<RuntimeHelpNodeDescriptor, String> {
let flow_name = if is_main {
"main".to_string()
} else {
help_node.name.trim().to_string()
};
let mut related_entries = Vec::new();
if is_main {
for entry in skill.meta.entries() {
related_entries.push(Self::resolve_help_related_entry_name(
skill,
entry.name.trim(),
&flow_name,
)?);
}
} else {
for entry in skill.meta.entries_for_help_topic(help_node.name.trim()) {
related_entries.push(Self::resolve_help_related_entry_name(
skill,
entry.name.trim(),
&flow_name,
)?);
}
}
Ok(RuntimeHelpNodeDescriptor {
flow_name,
description: help_node.description.trim().to_string(),
related_entries,
is_main,
})
}
fn resolve_help_related_entry_name(
skill: &LoadedSkill,
local_name: &str,
flow_name: &str,
) -> Result<String, String> {
skill
.resolved_tool_name(local_name)
.map(str::to_string)
.ok_or_else(|| {
format!(
"help flow '{}' in skill '{}' references local entry '{}' without a resolved canonical name",
flow_name,
skill.meta.effective_skill_id(),
local_name
)
})
}
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,
) -> Result<bool, String> {
let skill = self.skills.get(&target.skill_storage_key).ok_or_else(|| {
format!(
"entry registry target '{}' references missing loaded skill storage key '{}'",
target.canonical_name, target.skill_storage_key
)
})?;
if authority == SkillManagementAuthority::System {
return Ok(true);
}
Ok(Self::normalized_skill_root_name(&skill.root_name) != "ROOT")
}
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,
) -> Result<bool, String> {
match self.entry_registry.get(name) {
Some(target) => self.entry_target_visible_to_authority(authority, target),
None => Ok(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,
) -> Result<Option<String>, String> {
match self.entry_registry.get(tool_name) {
Some(target) => {
if self.entry_target_visible_to_authority(authority, target)? {
Ok(Some(target.skill_id.clone()))
} else {
Ok(None)
}
}
None => Ok(None),
}
}
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, lancedb_table) = create_provider_context_table(
lua,
"lancedb",
"__lancedb_skill_name",
current_skill_name,
)?;
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();
register_provider_json_noarg_method(
lua,
&lancedb_table,
"info",
"vulcan.lancedb.info",
"vulcan.lancedb.info",
move || info_binding.info_json(),
)?;
let status_binding = binding.clone();
register_provider_json_noarg_method(
lua,
&lancedb_table,
"status",
"vulcan.lancedb.status",
"vulcan.lancedb.status",
move || status_binding.status_json(),
)?;
let create_binding = binding.clone();
register_provider_json_method(
lua,
&lancedb_table,
"lancedb",
"create_table",
move |input_json| create_binding.create_table_json(input_json),
)?;
let upsert_binding = binding.clone();
let vector_upsert_fn = lua
.create_function(move |lua, input: LuaValue| {
let mut input_json =
provider_input_table_to_json(input, "lancedb.vector_upsert")?;
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 mut input_json =
provider_input_table_to_json(input, "lancedb.vector_search")?;
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();
register_provider_json_method(
lua,
&lancedb_table,
"lancedb",
"delete",
move |input_json| delete_binding.delete_json(input_json),
)?;
let drop_binding = binding;
register_provider_json_method(
lua,
&lancedb_table,
"lancedb",
"drop_table",
move |input_json| drop_binding.drop_table_json(input_json),
)?;
} else {
install_disabled_provider_context(
lua,
&lancedb_table,
"lancedb",
disabled_skill_status_json(current_skill_name),
"current skill has not enabled lancedb",
&[
"create_table",
"vector_upsert",
"vector_search",
"delete",
"drop_table",
],
)?;
}
install_provider_context_table(&vulcan, "lancedb", lancedb_table)?;
Ok(())
}
fn populate_vulcan_sqlite_context(
lua: &Lua,
binding: Option<Arc<SqliteSkillBinding>>,
current_skill_name: Option<&str>,
) -> Result<(), String> {
let (vulcan, sqlite_table) = create_provider_context_table(
lua,
"sqlite",
"__sqlite_skill_name",
current_skill_name,
)?;
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();
register_provider_json_noarg_method(
lua,
&sqlite_table,
"info",
"vulcan.sqlite.info",
"vulcan.sqlite.info",
move || info_binding.info_json(),
)?;
let status_binding = binding.clone();
register_provider_json_noarg_method(
lua,
&sqlite_table,
"status",
"vulcan.sqlite.status",
"vulcan.sqlite.status",
move || status_binding.status_json(),
)?;
let tokenize_binding = binding.clone();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"tokenize_text",
move |input_json| tokenize_binding.tokenize_text_json(input_json),
)?;
let execute_script_binding = binding.clone();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"execute_script",
move |input_json| execute_script_binding.execute_script(input_json),
)?;
let execute_batch_binding = binding.clone();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"execute_batch",
move |input_json| execute_batch_binding.execute_batch(input_json),
)?;
let query_json_binding = binding.clone();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"query_json",
move |input_json| query_json_binding.query_json(input_json),
)?;
let query_stream_binding = binding.clone();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"query_stream",
move |input_json| query_stream_binding.query_stream(input_json),
)?;
let query_stream_wait_metrics_binding = binding.clone();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"query_stream_wait_metrics",
move |input_json| {
query_stream_wait_metrics_binding.query_stream_wait_metrics(input_json)
},
)?;
let query_stream_chunk_binding = binding.clone();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"query_stream_chunk",
move |input_json| query_stream_chunk_binding.query_stream_chunk(input_json),
)?;
let query_stream_close_binding = binding.clone();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"query_stream_close",
move |input_json| query_stream_close_binding.query_stream_close(input_json),
)?;
let upsert_word_binding = binding.clone();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"upsert_custom_word",
move |input_json| upsert_word_binding.upsert_custom_word_json(input_json),
)?;
let remove_word_binding = binding.clone();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"remove_custom_word",
move |input_json| remove_word_binding.remove_custom_word_json(input_json),
)?;
let list_words_binding = binding.clone();
let list_words_fn = lua
.create_function(move |lua, ()| {
provider_json_result_to_lua(lua, list_words_binding.list_custom_words_json())
})
.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();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"ensure_fts_index",
move |input_json| ensure_index_binding.ensure_fts_index_json(input_json),
)?;
let rebuild_index_binding = binding.clone();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"rebuild_fts_index",
move |input_json| rebuild_index_binding.rebuild_fts_index_json(input_json),
)?;
let upsert_doc_binding = binding.clone();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"upsert_fts_document",
move |input_json| upsert_doc_binding.upsert_fts_document_json(input_json),
)?;
let delete_doc_binding = binding.clone();
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"delete_fts_document",
move |input_json| delete_doc_binding.delete_fts_document_json(input_json),
)?;
let search_binding = binding;
register_provider_json_method(
lua,
&sqlite_table,
"sqlite",
"search_fts",
move |input_json| search_binding.search_fts_json(input_json),
)?;
} else {
install_disabled_provider_context(
lua,
&sqlite_table,
"sqlite",
disabled_sqlite_skill_status_json(current_skill_name),
"current skill has not enabled sqlite",
&[
"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",
],
)?;
}
install_provider_context_table(&vulcan, "sqlite", sqlite_table)?;
Ok(())
}
fn populate_loaded_skill_lua_context(
&self,
lua: &Lua,
skill: &LoadedSkill,
context: LoadedSkillLuaContext<'_>,
) -> Result<(), String> {
let skill_name = skill.meta.effective_skill_id();
replace_lua_managed_package_context(lua, Some(skill.managed_package.clone()));
Self::populate_vulcan_request_context(lua, context.invocation_context)?;
populate_vulcan_internal_execution_context(
lua,
&VulcanInternalExecutionContext {
tool_name: Some(context.display_tool_name.to_string()),
skill_name: Some(skill_name.to_string()),
entry_name: Some(context.entry_name.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(context.entry_path))?;
populate_vulcan_dependency_context(
lua,
self.host_options.as_ref(),
Some(&skill.dir),
Some(skill_name),
)?;
Self::populate_vulcan_lancedb_context(
lua,
skill.lancedb_binding.clone(),
Some(skill_name),
)?;
Self::populate_vulcan_sqlite_context(lua, skill.sqlite_binding.clone(), Some(skill_name))?;
Ok(())
}
fn populate_anonymous_lua_context(
lua: &Lua,
context: AnonymousLuaExecutionContext<'_>,
) -> Result<(), String> {
Self::populate_vulcan_request_context(lua, context.invocation_context)?;
populate_vulcan_internal_execution_context(lua, &context.internal_context)?;
populate_vulcan_file_context(lua, None, context.entry_file)?;
match context.dependency_context {
AnonymousLuaDependencyContext::ClearWithHostOptions(host_options) => {
populate_vulcan_dependency_context(lua, host_options, None, None)?;
}
AnonymousLuaDependencyContext::PreserveCurrent => {}
}
match context.managed_package_context {
AnonymousLuaManagedPackageContext::Clear => {
replace_lua_managed_package_context(lua, None);
}
AnonymousLuaManagedPackageContext::Set(package) => {
replace_lua_managed_package_context(lua, Some(package.clone()));
}
AnonymousLuaManagedPackageContext::PreserveCurrent => {}
}
Self::populate_vulcan_lancedb_context(lua, None, None)?;
Self::populate_vulcan_sqlite_context(lua, None, None)?;
Ok(())
}
fn resolve_call_skill_invocation_target<'a>(
&'a self,
tool_name: &str,
) -> Result<CallSkillInvocationTarget<'a>, 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))?;
Ok(CallSkillInvocationTarget {
skill,
tool,
display_tool_name: &resolved_target.canonical_name,
local_entry_name: &resolved_target.local_name,
})
}
fn prepare_call_skill_lua_context(
&self,
lua: &Lua,
invocation_target: &CallSkillInvocationTarget<'_>,
invocation_context: Option<&LuaInvocationContext>,
) -> Result<(), String> {
if invocation_target.skill.meta.debug {
Self::compile_skill_into_lua(
lua,
invocation_target.skill,
invocation_target.tool,
true,
)?;
}
let entry_path = tool_entry_path(&invocation_target.skill.dir, invocation_target.tool);
self.populate_loaded_skill_lua_context(
lua,
invocation_target.skill,
LoadedSkillLuaContext {
display_tool_name: invocation_target.display_tool_name,
entry_name: invocation_target.local_entry_name,
entry_path: &entry_path,
invocation_context,
},
)
}
fn prepare_lua_help_context(
&self,
lua: &Lua,
skill: &LoadedSkill,
relative_path: &str,
helper_path: &Path,
request_context: Option<&RuntimeRequestContext>,
) -> Result<(), String> {
let help_invocation_context = LuaInvocationContext::new(
request_context.cloned(),
Value::Object(serde_json::Map::new()),
Value::Object(serde_json::Map::new()),
);
self.populate_loaded_skill_lua_context(
lua,
skill,
LoadedSkillLuaContext {
display_tool_name: "vulcan-help",
entry_name: relative_path,
entry_path: helper_path,
invocation_context: Some(&help_invocation_context),
},
)
}
pub fn call_skill(
&self,
tool_name: &str,
args: &Value,
invocation_context: Option<&LuaInvocationContext>,
) -> Result<RuntimeInvocationResult, String> {
let invocation_target = self.resolve_call_skill_invocation_target(tool_name)?;
let module_name = invocation_target.tool.lua_module.clone();
let mut lease = self.acquire_vm()?;
let scope_guard = LuaVmRequestScopeGuard::new(&mut lease, self.host_options.as_ref())?;
let lua = scope_guard.lua()?;
self.prepare_call_skill_lua_context(lua, &invocation_target, invocation_context)?;
let invocation_input = prepare_call_skill_lua_invocation_input(lua, &module_name, args)?;
let call_result = invoke_loaded_lua_skill_handler(
invocation_input.handler,
invocation_input.args_table,
invocation_target.display_tool_name,
invocation_context,
);
finish_pooled_vm_request_scope(call_result, scope_guard, "pooled Lua VM cleanup failed")
}
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 help_payload_source = read_lua_help_payload_source(&skill.dir, relative_path)?;
let mut lease = self.acquire_vm()?;
let scope_guard = LuaVmRequestScopeGuard::new(&mut lease, self.host_options.as_ref())?;
let lua = scope_guard.lua()?;
self.prepare_lua_help_context(
lua,
skill,
relative_path,
&help_payload_source.helper_path,
request_context,
)?;
let chunk_name = format!("{}-{}", skill.meta.effective_skill_id(), relative_path);
let rendered_result = render_lua_help_payload_text(
lua,
&help_payload_source.helper_path,
&help_payload_source.source,
&chunk_name,
);
finish_pooled_vm_request_scope(rendered_result, scope_guard, "pooled Lua VM cleanup failed")
}
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))?;
let dispatch_entries = build_lua_call_dispatch_entries(skills_map, entry_registry)?;
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 = resolve_lua_call_dispatch_entry(&dispatch_entries, &name)?;
let owner_skill_name = &dispatch_entry.owner_skill_id;
let func = resolve_lua_call_dispatch_handler(lua, dispatch_entry)?;
let PreparedLuaNestedCallScope {
guard: nested_scope_guard,
invocation_context: nested_invocation_context,
} = prepare_lua_nested_call_scope(
lua,
host_options.clone(),
lancedb_host.clone(),
sqlite_host.clone(),
)?;
dispatch_entry.reject_forbidden_luaexec_call(
nested_scope_guard.previous_internal_context(),
)?;
let provider_bindings = resolve_lua_call_provider_bindings(
owner_skill_name,
lancedb_host.as_ref(),
sqlite_host.as_ref(),
)
.map_err(mlua::Error::runtime)?;
nested_scope_guard
.enter_nested_call(build_lua_nested_call_target(
dispatch_entry,
&nested_invocation_context,
provider_bindings,
))
.map_err(mlua::Error::runtime)?;
let call_result = func.call::<MultiValue>(args);
nested_scope_guard.finish_nested_call(call_result)
})
.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 !configured_package_search_directory_exists(lua_packages, "lua_packages_dir")? {
return Ok(());
}
let host_provided_ffi_root = match host_options.host_provided_ffi_root.as_ref() {
Some(root) => {
if configured_package_search_directory_exists(root, "host_provided_ffi_root")? {
Some(root.as_path())
} else {
None
}
}
None => None,
};
#[cfg(windows)]
let cpath_pattern = {
let mut 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()
);
if let Some(root) = host_provided_ffi_root {
pattern.push_str(&format!(
"{}\\?.dll;{}\\?\\init.dll;",
root.display(),
root.display()
));
}
pattern
};
#[cfg(target_os = "linux")]
let cpath_pattern = {
let mut 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()
);
if let Some(root) = host_provided_ffi_root {
pattern.push_str(&format!(
"{}/?.so;{}/?/init.so;",
root.display(),
root.display()
));
}
pattern
};
#[cfg(target_os = "macos")]
let cpath_pattern = {
let mut 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()
);
if let Some(root) = host_provided_ffi_root {
pattern.push_str(&format!(
"{}/?.dylib;{}/?/init.dylib;",
root.display(),
root.display()
));
}
pattern
};
#[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()?);
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()?);
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 runtime_python = lua.create_table()?;
let runtime_node = 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() {
parts.push(render_lua_print_argument(val));
}
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();
let dir_path = Path::new(&dir);
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_vulcan_fs_list_non_utf8_file_name_error(
dir_path,
name.as_os_str(),
))
})?;
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_write_bytes_fn =
lua.create_function(|_, (path, content): (LuaValue, LuaValue)| {
let path = require_path_arg(path, "fs.write_bytes", "path")?;
let content = require_string_arg(content, "fs.write_bytes", "content", false)?;
let bytes = BASE64_STANDARD
.decode(content.as_bytes())
.map_err(|error| {
mlua::Error::runtime(format!(
"fs.write_bytes: base64 decode failed: {error}"
))
})?;
fs::write(&path, bytes)
.map_err(|error| mlua::Error::runtime(format!("fs.write_bytes: {}", error)))?;
Ok(true)
})?;
fs.set("write_bytes", fs_write_bytes_fn)?;
let fs_rename_fn =
lua.create_function(|_, (old_path, new_path): (LuaValue, LuaValue)| {
let old_path = require_path_arg(old_path, "fs.rename", "old_path")?;
let new_path = require_path_arg(new_path, "fs.rename", "new_path")?;
fs::rename(&old_path, &new_path)
.map_err(|error| mlua::Error::runtime(format!("fs.rename: {}", error)))?;
Ok(true)
})?;
fs.set("rename", fs_rename_fn)?;
let fs_remove_fn = lua.create_function(|_, args: MultiValue| {
let mut values = args.into_iter();
let path =
require_path_arg(values.next().unwrap_or(LuaValue::Nil), "fs.remove", "path")?;
let recursive = parse_vulcan_fs_recursive_option(
values.next().unwrap_or(LuaValue::Nil),
"fs.remove",
)?;
let target_path = Path::new(&path);
let metadata = match fs::symlink_metadata(target_path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == ErrorKind::NotFound => return Ok(false),
Err(error) => {
return Err(mlua::Error::runtime(format!("fs.remove: {}", error)));
}
};
let file_type = metadata.file_type();
if file_type.is_dir() {
if recursive {
fs::remove_dir_all(target_path)
.map_err(|error| mlua::Error::runtime(format!("fs.remove: {}", error)))?;
} else {
fs::remove_dir(target_path)
.map_err(|error| mlua::Error::runtime(format!("fs.remove: {}", error)))?;
}
} else {
fs::remove_file(target_path)
.map_err(|error| mlua::Error::runtime(format!("fs.remove: {}", error)))?;
}
Ok(true)
})?;
fs.set("remove", fs_remove_fn)?;
let fs_mkdir_fn = lua.create_function(|_, args: MultiValue| {
let mut values = args.into_iter();
let path =
require_path_arg(values.next().unwrap_or(LuaValue::Nil), "fs.mkdir", "path")?;
let recursive = parse_vulcan_fs_recursive_option(
values.next().unwrap_or(LuaValue::Nil),
"fs.mkdir",
)?;
let target_path = Path::new(&path);
match vulcan_fs_mkdir_target_status(target_path).map_err(mlua::Error::runtime)? {
VulcanFsMkdirTargetStatus::Missing => {}
VulcanFsMkdirTargetStatus::ExistingDirectory => return Ok(false),
VulcanFsMkdirTargetStatus::ExistingNonDirectory => {
return Err(mlua::Error::runtime(format!(
"fs.mkdir: target already exists and is not a directory: {}",
render_log_friendly_path(target_path)
)));
}
}
if recursive {
fs::create_dir_all(target_path)
.map_err(|error| mlua::Error::runtime(format!("fs.mkdir: {}", error)))?;
} else {
fs::create_dir(target_path)
.map_err(|error| mlua::Error::runtime(format!("fs.mkdir: {}", error)))?;
}
Ok(true)
})?;
fs.set("mkdir", fs_mkdir_fn)?;
let fs_copy_fn = lua.create_function(|_, args: MultiValue| {
let mut values = args.into_iter();
let source_path = require_path_arg(
values.next().unwrap_or(LuaValue::Nil),
"fs.copy",
"src_path",
)?;
let target_path = require_path_arg(
values.next().unwrap_or(LuaValue::Nil),
"fs.copy",
"dst_path",
)?;
let overwrite = parse_vulcan_fs_overwrite_option(
values.next().unwrap_or(LuaValue::Nil),
"fs.copy",
)?;
let source = Path::new(&source_path);
let target = Path::new(&target_path);
let source_metadata = fs::metadata(source)
.map_err(|error| mlua::Error::runtime(format!("fs.copy: {}", error)))?;
let target_exists =
path_entry_exists(target, "fs.copy").map_err(mlua::Error::runtime)?;
if target_exists && !overwrite {
return Ok(false);
}
if source_metadata.is_dir() {
validate_vulcan_fs_copy_directory_target(
source,
target,
overwrite && target_exists,
)
.map_err(mlua::Error::runtime)?;
}
if target_exists {
remove_vulcan_fs_copy_target(target).map_err(mlua::Error::runtime)?;
}
if source_metadata.is_dir() {
copy_vulcan_fs_directory_recursive(source, target).map_err(mlua::Error::runtime)?;
} else {
fs::copy(source, target)
.map_err(|error| mlua::Error::runtime(format!("fs.copy: {}", error)))?;
}
Ok(true)
})?;
fs.set("copy", fs_copy_fn)?;
let fs_stat_fn = lua.create_function(|lua, path: LuaValue| {
let path = require_path_arg(path, "fs.stat", "path")?;
match fs::symlink_metadata(&path) {
Ok(metadata) => Ok(LuaValue::Table(create_vulcan_fs_stat_table(
lua,
&metadata,
Path::new(&path),
)?)),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(LuaValue::Nil),
Err(error) => Err(mlua::Error::runtime(format!("fs.stat: {}", error))),
}
})?;
fs.set("stat", fs_stat_fn)?;
let fs_read_bytes_fn = lua.create_function(|lua, path: LuaValue| {
let path = require_path_arg(path, "fs.read_bytes", "path")?;
let bytes = fs::read(&path)
.map_err(|error| mlua::Error::runtime(format!("fs.read_bytes: {}", error)))?;
lua.create_string(BASE64_STANDARD.encode(bytes))
})?;
fs.set("read_bytes", fs_read_bytes_fn)?;
let fs_exists_fn = lua.create_function(|_, path: LuaValue| {
let path = require_path_arg(path, "fs.exists", "path")?;
vulcan_fs_target_exists(Path::new(&path), "fs.exists").map_err(mlua::Error::runtime)
})?;
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")?;
vulcan_fs_target_is_dir(Path::new(&path), "fs.is_dir").map_err(mlua::Error::runtime)
})?;
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 path_dirname_fn = lua.create_function(|lua, path: LuaValue| {
let path = require_path_arg(path, "path.dirname", "path")?;
let rendered = render_vulcan_path_dirname(Path::new(&path));
lua.create_string(&rendered)
})?;
path.set("dirname", path_dirname_fn)?;
let path_basename_fn = lua.create_function(|lua, path: LuaValue| {
let path = require_path_arg(path, "path.basename", "path")?;
let rendered =
render_vulcan_path_component(Path::new(&path).file_name(), "path.basename")?;
lua.create_string(&rendered)
})?;
path.set("basename", path_basename_fn)?;
let path_stem_fn = lua.create_function(|lua, path: LuaValue| {
let path = require_path_arg(path, "path.stem", "path")?;
let rendered = render_vulcan_path_component(Path::new(&path).file_stem(), "path.stem")?;
lua.create_string(&rendered)
})?;
path.set("stem", path_stem_fn)?;
let path_extname_fn = lua.create_function(|lua, path: LuaValue| {
let path = require_path_arg(path, "path.extname", "path")?;
let extension =
render_vulcan_path_component(Path::new(&path).extension(), "path.extname")?;
let rendered = if extension.is_empty() {
String::new()
} else {
format!(".{}", extension)
};
lua.create_string(&rendered)
})?;
path.set("extname", path_extname_fn)?;
let path_normalize_fn = lua.create_function(|lua, path: LuaValue| {
let path = require_path_arg(path, "path.normalize", "path")?;
let rendered = render_vulcan_normalized_path(Path::new(&path));
lua.create_string(&rendered)
})?;
path.set("normalize", path_normalize_fn)?;
let path_is_abs_fn = lua.create_function(|_, path: LuaValue| {
let path = require_path_arg(path, "path.is_abs", "path")?;
Ok(Path::new(&path).is_absolute())
})?;
path.set("is_abs", path_is_abs_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)?;
let python_status_fn = lua.create_function(|lua, ()| managed_python_status(lua))?;
runtime_python.set("status", python_status_fn)?;
let python_invoke_fn =
lua.create_function(|lua, spec: LuaValue| invoke_managed_python(lua, spec))?;
runtime_python.set("invoke", python_invoke_fn)?;
let runtime_python_session = lua.create_table()?;
let python_session_open_fn = lua.create_function(move |lua, spec: LuaValue| {
open_managed_python_session(lua, spec, default_text_encoding)
})?;
runtime_python_session.set("open", python_session_open_fn)?;
runtime_python.set("session", runtime_python_session)?;
let node_status_fn = lua.create_function(|lua, ()| managed_node_status(lua))?;
runtime_node.set("status", node_status_fn)?;
let node_invoke_fn =
lua.create_function(|lua, spec: LuaValue| invoke_managed_node(lua, spec))?;
runtime_node.set("invoke", node_invoke_fn)?;
let runtime_node_session = lua.create_table()?;
let node_session_open_fn = lua.create_function(move |lua, spec: LuaValue| {
open_managed_node_session(lua, spec, default_text_encoding)
})?;
runtime_node_session.set("open", node_session_open_fn)?;
runtime_node.set("session", runtime_node_session)?;
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 launchers_fn = lua.create_function(|lua, ()| {
let info = lua.create_table()?;
let shells = lua.create_table()?;
for (index, shell_name) in supported_exec_shell_names().into_iter().enumerate() {
shells.set(index + 1, shell_name)?;
}
info.set("default", default_exec_shell_name())?;
info.set("shells", shells)?;
Ok(info)
})?;
process.set("launchers", launchers_fn)?;
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)?;
let which_fn = lua.create_function(|lua, program: LuaValue| {
let program = require_string_arg(program, "process.which", "program", false)?;
match resolve_vulcan_process_which(&program) {
Ok(Some(found)) => {
let rendered = render_host_visible_path(&found);
Ok(LuaValue::String(lua.create_string(&rendered)?))
}
Ok(None) => Ok(LuaValue::Nil),
Err(error) => Err(mlua::Error::runtime(error)),
}
})?;
process.set("which", which_fn)?;
process.set(
"session",
create_process_session_table(lua, default_text_encoding)?,
)?;
let os_info_fn = lua.create_function(|lua, ()| {
let current_os = 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) => {
let text = serde_json::to_string(&value).map_err(|error| {
mlua::Error::runtime(format!(
"json.encode: failed to serialize JSON value: {error}"
))
})?;
lua.create_string(text)
}
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)))?;
global_tool_cache()
.create(&scope, payload, ttl_secs)
.map_err(|error| mlua::Error::runtime(format!("cache.put: {}", error)))
})?;
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();
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)?;
runtime.set("python", runtime_python)?;
runtime.set("node", runtime_node)?;
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 provider_input_table_to_json(input: LuaValue, api_name: &str) -> mlua::Result<Value> {
let input_table = require_table_arg(input, api_name, "input")?;
lua_value_to_json(&LuaValue::Table(input_table)).map_err(mlua::Error::runtime)
}
fn provider_json_result_to_lua(lua: &Lua, result: Result<Value, String>) -> mlua::Result<LuaValue> {
let result = result.map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &result).map_err(mlua::Error::external)
}
fn create_provider_context_table(
lua: &Lua,
provider_name: &str,
skill_marker_key: &str,
current_skill_name: Option<&str>,
) -> Result<(Table, Table), String> {
let vulcan = get_vulcan_table(lua)?;
let provider_table = lua
.create_table()
.map_err(|error| format!("Failed to create vulcan.{} table: {}", provider_name, error))?;
let current_skill = current_skill_name.unwrap_or("");
vulcan
.set(skill_marker_key, current_skill)
.map_err(|error| format!("Failed to set vulcan.{}: {}", skill_marker_key, error))?;
Ok((vulcan, provider_table))
}
fn install_provider_context_table(
vulcan: &Table,
provider_name: &str,
provider_table: Table,
) -> Result<(), String> {
vulcan
.set(provider_name, provider_table)
.map_err(|error| format!("Failed to set vulcan.{}: {}", provider_name, error))?;
Ok(())
}
fn register_provider_json_method<F>(
lua: &Lua,
provider_table: &Table,
provider_name: &str,
method_name: &str,
invoke: F,
) -> Result<(), String>
where
F: Fn(&Value) -> Result<Value, String> + Send + 'static,
{
let api_name = format!("{}.{}", provider_name, method_name);
let lua_path = format!("vulcan.{}", api_name);
let input_api_name = api_name;
let method_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_json = provider_input_table_to_json(input, &input_api_name)?;
provider_json_result_to_lua(lua, invoke(&input_json))
})
.map_err(|error| format!("Failed to create {}: {}", lua_path, error))?;
provider_table
.set(method_name, method_fn)
.map_err(|error| format!("Failed to set {}: {}", lua_path, error))?;
Ok(())
}
fn register_provider_json_noarg_method<F>(
lua: &Lua,
provider_table: &Table,
method_name: &str,
create_error_subject: &str,
set_error_subject: &str,
resolve: F,
) -> Result<(), String>
where
F: Fn() -> Result<Value, String> + Send + 'static,
{
let method_fn = lua
.create_function(move |lua, ()| {
let value = resolve().map_err(mlua::Error::runtime)?;
json_value_to_lua(lua, &value).map_err(mlua::Error::external)
})
.map_err(|error| format!("Failed to create {}: {}", create_error_subject, error))?;
provider_table
.set(method_name, method_fn)
.map_err(|error| format!("Failed to set {}: {}", set_error_subject, error))?;
Ok(())
}
fn install_disabled_provider_context(
lua: &Lua,
provider_table: &Table,
provider_name: &str,
disabled_status: Value,
disabled_error: &str,
method_names: &[&str],
) -> Result<(), String> {
provider_table
.set("enabled", false)
.map_err(|error| format!("Failed to set vulcan.{}.enabled: {}", provider_name, error))?;
let status_value = disabled_status.clone();
register_provider_json_noarg_method(
lua,
provider_table,
"status",
&format!("disabled vulcan.{}.status", provider_name),
&format!("vulcan.{}.status", provider_name),
move || Ok(status_value.clone()),
)?;
register_provider_json_noarg_method(
lua,
provider_table,
"info",
&format!("disabled vulcan.{}.info", provider_name),
&format!("disabled vulcan.{}.info", provider_name),
move || Ok(disabled_status.clone()),
)?;
for method_name in method_names {
let error_text = disabled_error.to_string();
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.{} proxy: {}",
provider_name, error
)
})?;
provider_table
.set(*method_name, fn_value)
.map_err(|error| format!("Failed to set disabled method {}: {}", method_name, error))?;
}
Ok(())
}
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) => s
.to_str()
.map(|text| Value::String(text.to_string()))
.map_err(|error| format!("Cannot convert Lua string to JSON: invalid UTF-8: {error}")),
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(userdata) => {
if let Ok(context) = userdata.borrow::<self::lease::ReadonlyJsonContextValue>() {
return Ok(context.json_value().clone());
}
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)]
pub(crate) mod tests;