use crate::host::database::{LuaRuntimeDatabaseCallbackMode, LuaRuntimeDatabaseProviderMode};
use crate::runtime_context::RuntimeRequestContext;
use crate::tool_cache::ToolCacheConfig;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LuaRuntimeLayout {
pub runtime_root: PathBuf,
}
impl LuaRuntimeLayout {
pub fn new(runtime_root: impl Into<PathBuf>) -> Self {
Self {
runtime_root: runtime_root.into(),
}
}
pub fn bin_dir(&self) -> PathBuf {
self.runtime_root.join("bin")
}
pub fn libs_dir(&self) -> PathBuf {
self.runtime_root.join("libs")
}
pub fn lua_packages_dir(&self) -> PathBuf {
self.runtime_root.join("lua_packages")
}
pub fn resources_dir(&self) -> PathBuf {
self.runtime_root.join("resources")
}
pub fn skills_dir(&self) -> PathBuf {
self.runtime_root.join("skills")
}
pub fn temp_dir(&self) -> PathBuf {
self.runtime_root.join("temp")
}
pub fn downloads_dir(&self) -> PathBuf {
self.temp_dir().join("downloads")
}
pub fn dependencies_dir(&self) -> PathBuf {
self.runtime_root.join("dependencies")
}
pub fn state_dir(&self) -> PathBuf {
self.runtime_root.join("state")
}
pub fn databases_dir(&self) -> PathBuf {
self.runtime_root.join("databases")
}
pub fn config_dir(&self) -> PathBuf {
self.runtime_root.join("config")
}
pub fn skill_config_file_path(&self) -> PathBuf {
self.config_dir().join("skill_config.json")
}
pub fn system_lua_lib_dir(&self) -> PathBuf {
self.runtime_root.join("system_lua_lib")
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum LuaRuntimeSpaceControllerProcessMode {
Service,
#[default]
Managed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LuaRuntimeSpaceControllerOptions {
pub endpoint: Option<String>,
pub auto_spawn: bool,
pub executable_path: Option<PathBuf>,
pub process_mode: LuaRuntimeSpaceControllerProcessMode,
pub minimum_uptime_secs: u64,
pub idle_timeout_secs: u64,
pub default_lease_ttl_secs: u64,
pub connect_timeout_secs: u64,
pub startup_timeout_secs: u64,
pub startup_retry_interval_ms: u64,
pub lease_renew_interval_secs: u64,
}
impl Default for LuaRuntimeSpaceControllerOptions {
fn default() -> Self {
Self {
endpoint: None,
auto_spawn: true,
executable_path: None,
process_mode: LuaRuntimeSpaceControllerProcessMode::Managed,
minimum_uptime_secs: 300,
idle_timeout_secs: 900,
default_lease_ttl_secs: 120,
connect_timeout_secs: 5,
startup_timeout_secs: 15,
startup_retry_interval_ms: 250,
lease_renew_interval_secs: 30,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LuaRuntimeCapabilityOptions {
#[serde(default)]
pub enable_skill_management_bridge: bool,
pub enable_managed_io_compat: bool,
}
impl Default for LuaRuntimeCapabilityOptions {
fn default() -> Self {
Self {
enable_skill_management_bridge: false,
enable_managed_io_compat: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct RuntimeSkillRoot {
pub name: String,
pub skills_dir: PathBuf,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct LuaRuntimeRunLuaPoolConfig {
pub min_size: usize,
pub max_size: usize,
pub idle_ttl_secs: u64,
}
pub const DEFAULT_MANAGED_RUNTIME_WORKER_POOL_MAX_SIZE_PER_ENVIRONMENT: usize = 4;
pub const DEFAULT_MANAGED_RUNTIME_WORKER_IDLE_TTL_SECS: u64 = 60;
pub const DEFAULT_MANAGED_RUNTIME_PERSISTENT_SESSION_LIMIT_PER_ENGINE: usize = 256;
pub const DEFAULT_MANAGED_RUNTIME_PERSISTENT_SESSION_BUFFER_LIMIT_BYTES_PER_STREAM: usize =
1024 * 1024;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct LuaRuntimeManagedRuntimeConfig {
pub worker_pool_max_size_per_environment: usize,
pub worker_idle_ttl_secs: u64,
pub persistent_session_limit_per_engine: usize,
pub persistent_session_default_buffer_limit_bytes_per_stream: usize,
pub invoke_default_timeout_ms: Option<u64>,
}
impl Default for LuaRuntimeManagedRuntimeConfig {
fn default() -> Self {
Self {
worker_pool_max_size_per_environment:
DEFAULT_MANAGED_RUNTIME_WORKER_POOL_MAX_SIZE_PER_ENVIRONMENT,
worker_idle_ttl_secs: DEFAULT_MANAGED_RUNTIME_WORKER_IDLE_TTL_SECS,
persistent_session_limit_per_engine:
DEFAULT_MANAGED_RUNTIME_PERSISTENT_SESSION_LIMIT_PER_ENGINE,
persistent_session_default_buffer_limit_bytes_per_stream:
DEFAULT_MANAGED_RUNTIME_PERSISTENT_SESSION_BUFFER_LIMIT_BYTES_PER_STREAM,
invoke_default_timeout_ms: None,
}
}
}
impl LuaRuntimeManagedRuntimeConfig {
pub fn validate(&self) -> Result<(), String> {
if self.worker_pool_max_size_per_environment == 0 {
return Err(
"managed_runtime_config.worker_pool_max_size_per_environment must be greater than zero"
.to_string(),
);
}
if self.worker_idle_ttl_secs == 0 {
return Err(
"managed_runtime_config.worker_idle_ttl_secs must be greater than zero".to_string(),
);
}
if self.persistent_session_limit_per_engine == 0 {
return Err(
"managed_runtime_config.persistent_session_limit_per_engine must be greater than zero"
.to_string(),
);
}
if self.persistent_session_default_buffer_limit_bytes_per_stream == 0 {
return Err(
"managed_runtime_config.persistent_session_default_buffer_limit_bytes_per_stream must be greater than zero"
.to_string(),
);
}
if self.invoke_default_timeout_ms == Some(0) {
return Err(
"managed_runtime_config.invoke_default_timeout_ms must be greater than zero when configured"
.to_string(),
);
}
Ok(())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LuaRuntimeHostOptions {
#[serde(default)]
pub runtime_root: Option<PathBuf>,
#[serde(default)]
pub managed_runtime_distribution_root: Option<PathBuf>,
#[serde(default)]
pub managed_runtime_environment_root: Option<PathBuf>,
#[serde(default)]
pub managed_runtime_config: LuaRuntimeManagedRuntimeConfig,
pub temp_dir: Option<PathBuf>,
pub resources_dir: Option<PathBuf>,
pub lua_packages_dir: Option<PathBuf>,
pub host_provided_tool_root: Option<PathBuf>,
pub host_provided_lua_root: Option<PathBuf>,
pub host_provided_ffi_root: Option<PathBuf>,
pub system_lua_lib_dir: Option<PathBuf>,
pub download_cache_root: Option<PathBuf>,
#[serde(default)]
pub dependency_dir_name: String,
#[serde(default)]
pub state_dir_name: String,
#[serde(default)]
pub database_dir_name: String,
pub skill_config_file_path: Option<PathBuf>,
pub allow_network_download: bool,
pub github_base_url: Option<String>,
pub github_api_base_url: Option<String>,
#[serde(default)]
pub official_skill_hub_base_url: Option<String>,
#[serde(default)]
pub enable_private_url_skill_install: bool,
#[serde(default)]
pub private_skill_source_allowlist: Vec<String>,
#[serde(default)]
pub default_text_encoding: Option<String>,
pub sqlite_library_path: Option<PathBuf>,
#[serde(default)]
pub sqlite_provider_mode: LuaRuntimeDatabaseProviderMode,
#[serde(default)]
pub sqlite_callback_mode: LuaRuntimeDatabaseCallbackMode,
pub lancedb_library_path: Option<PathBuf>,
#[serde(default)]
pub lancedb_provider_mode: LuaRuntimeDatabaseProviderMode,
#[serde(default)]
pub lancedb_callback_mode: LuaRuntimeDatabaseCallbackMode,
#[serde(default)]
pub space_controller: LuaRuntimeSpaceControllerOptions,
pub cache_config: Option<ToolCacheConfig>,
#[serde(default)]
pub runlua_pool_config: Option<LuaRuntimeRunLuaPoolConfig>,
pub reserved_entry_names: Vec<String>,
#[serde(default)]
pub ignored_skill_ids: Vec<String>,
#[serde(default)]
pub capabilities: LuaRuntimeCapabilityOptions,
}
impl LuaRuntimeHostOptions {
pub fn with_runtime_root(runtime_root: impl Into<PathBuf>) -> Self {
let mut options = Self {
runtime_root: Some(runtime_root.into()),
..Self::default()
};
options.apply_runtime_root_layout();
options
}
pub fn runtime_layout(&self) -> Option<LuaRuntimeLayout> {
self.runtime_root.clone().map(LuaRuntimeLayout::new)
}
pub fn apply_runtime_root_layout(&mut self) {
let Some(layout) = self.runtime_layout() else {
return;
};
self.temp_dir = Some(layout.temp_dir());
self.resources_dir = Some(layout.resources_dir());
self.lua_packages_dir = Some(layout.lua_packages_dir());
self.host_provided_tool_root = Some(layout.bin_dir());
self.host_provided_lua_root = Some(layout.lua_packages_dir());
self.host_provided_ffi_root = Some(layout.libs_dir());
self.system_lua_lib_dir = Some(layout.system_lua_lib_dir());
self.download_cache_root = Some(layout.downloads_dir());
self.dependency_dir_name = "dependencies".to_string();
self.state_dir_name = "state".to_string();
self.database_dir_name = "databases".to_string();
self.skill_config_file_path = Some(layout.skill_config_file_path());
}
pub fn normalized(mut self) -> Self {
self.apply_runtime_root_layout();
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LuaInvocationContext {
pub request_context: Option<RuntimeRequestContext>,
pub client_budget: Value,
pub tool_config: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct EffectiveBudgetScope {
pub bytes: u64,
pub lines: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ClientBudgetSnapshot {
pub client_name: Option<String>,
pub tool_name: Option<String>,
pub skill_name: Option<String>,
pub matched_client_pattern: Option<String>,
pub tool_result: EffectiveBudgetScope,
pub file_read: EffectiveBudgetScope,
pub tool_config: Value,
}
impl LuaInvocationContext {
pub fn new(
request_context: Option<RuntimeRequestContext>,
client_budget: Value,
tool_config: Value,
) -> Self {
Self {
request_context,
client_budget: normalize_context_object(client_budget),
tool_config: normalize_context_object(tool_config),
}
}
pub fn empty() -> Self {
Self::default()
}
}
fn normalize_context_object(value: Value) -> Value {
match value {
Value::Object(_) => value,
_ => Value::Object(Map::new()),
}
}
#[cfg(test)]
mod tests {
use super::{
DEFAULT_MANAGED_RUNTIME_PERSISTENT_SESSION_BUFFER_LIMIT_BYTES_PER_STREAM,
DEFAULT_MANAGED_RUNTIME_PERSISTENT_SESSION_LIMIT_PER_ENGINE,
DEFAULT_MANAGED_RUNTIME_WORKER_IDLE_TTL_SECS,
DEFAULT_MANAGED_RUNTIME_WORKER_POOL_MAX_SIZE_PER_ENVIRONMENT, LuaRuntimeCapabilityOptions,
LuaRuntimeHostOptions, LuaRuntimeManagedRuntimeConfig,
};
use serde_json::json;
use std::path::PathBuf;
#[test]
fn capability_options_require_explicit_managed_io_compat_flag() {
let error = serde_json::from_value::<LuaRuntimeCapabilityOptions>(json!({
"enable_skill_management_bridge": true
}))
.expect_err("partial capability options should fail without managed io flag");
assert!(
error.to_string().contains("enable_managed_io_compat"),
"missing-field error should mention enable_managed_io_compat, got: {error}"
);
}
#[test]
fn runtime_root_expands_fixed_layout() {
let runtime_root = PathBuf::from("D:/runtime");
let options = LuaRuntimeHostOptions::with_runtime_root(runtime_root.clone());
assert_eq!(options.temp_dir, Some(runtime_root.join("temp")));
assert_eq!(options.resources_dir, Some(runtime_root.join("resources")));
assert_eq!(
options.lua_packages_dir,
Some(runtime_root.join("lua_packages"))
);
assert_eq!(
options.host_provided_tool_root,
Some(runtime_root.join("bin"))
);
assert_eq!(
options.host_provided_lua_root,
Some(runtime_root.join("lua_packages"))
);
assert_eq!(
options.host_provided_ffi_root,
Some(runtime_root.join("libs"))
);
assert_eq!(
options.system_lua_lib_dir,
Some(runtime_root.join("system_lua_lib"))
);
assert_eq!(
options.download_cache_root,
Some(runtime_root.join("temp").join("downloads"))
);
assert_eq!(
options.skill_config_file_path,
Some(runtime_root.join("config").join("skill_config.json"))
);
assert_eq!(options.dependency_dir_name, "dependencies");
assert_eq!(options.state_dir_name, "state");
assert_eq!(options.database_dir_name, "databases");
}
#[test]
fn runtime_root_normalization_preserves_explicit_managed_roots() {
let runtime_root = PathBuf::from("D:/runtime-data");
let distribution_root = PathBuf::from("D:/application/dependencies/runtimes");
let environment_root = PathBuf::from("D:/user-data/managed-runtime-envs");
let mut options = LuaRuntimeHostOptions::with_runtime_root(runtime_root);
options.managed_runtime_distribution_root = Some(distribution_root.clone());
options.managed_runtime_environment_root = Some(environment_root.clone());
let normalized = options.normalized();
assert_eq!(
normalized.managed_runtime_distribution_root,
Some(distribution_root)
);
assert_eq!(
normalized.managed_runtime_environment_root,
Some(environment_root)
);
}
#[test]
fn managed_runtime_config_defaults_match_documented_policy() {
let config = LuaRuntimeManagedRuntimeConfig::default();
assert_eq!(
config.worker_pool_max_size_per_environment,
DEFAULT_MANAGED_RUNTIME_WORKER_POOL_MAX_SIZE_PER_ENVIRONMENT
);
assert_eq!(
config.worker_idle_ttl_secs,
DEFAULT_MANAGED_RUNTIME_WORKER_IDLE_TTL_SECS
);
assert_eq!(
config.persistent_session_limit_per_engine,
DEFAULT_MANAGED_RUNTIME_PERSISTENT_SESSION_LIMIT_PER_ENGINE
);
assert_eq!(
config.persistent_session_default_buffer_limit_bytes_per_stream,
DEFAULT_MANAGED_RUNTIME_PERSISTENT_SESSION_BUFFER_LIMIT_BYTES_PER_STREAM
);
assert_eq!(config.invoke_default_timeout_ms, None);
config.validate().expect("default policy must validate");
}
#[test]
fn managed_runtime_config_rejects_zero_boundaries() {
let config = LuaRuntimeManagedRuntimeConfig {
worker_pool_max_size_per_environment: 0,
..LuaRuntimeManagedRuntimeConfig::default()
};
assert!(
config
.validate()
.expect_err("zero worker capacity must fail")
.contains("worker_pool_max_size_per_environment")
);
let config = LuaRuntimeManagedRuntimeConfig {
worker_idle_ttl_secs: 0,
..LuaRuntimeManagedRuntimeConfig::default()
};
assert!(
config
.validate()
.expect_err("zero worker TTL must fail")
.contains("worker_idle_ttl_secs")
);
let config = LuaRuntimeManagedRuntimeConfig {
persistent_session_limit_per_engine: 0,
..LuaRuntimeManagedRuntimeConfig::default()
};
assert!(
config
.validate()
.expect_err("zero session capacity must fail")
.contains("persistent_session_limit_per_engine")
);
let config = LuaRuntimeManagedRuntimeConfig {
persistent_session_default_buffer_limit_bytes_per_stream: 0,
..LuaRuntimeManagedRuntimeConfig::default()
};
assert!(
config
.validate()
.expect_err("zero session buffer must fail")
.contains("persistent_session_default_buffer_limit_bytes_per_stream")
);
let config = LuaRuntimeManagedRuntimeConfig {
invoke_default_timeout_ms: Some(0),
..LuaRuntimeManagedRuntimeConfig::default()
};
assert!(
config
.validate()
.expect_err("zero default invoke timeout must fail")
.contains("invoke_default_timeout_ms")
);
}
}