use super::lease::default_runlua_exec_args;
use super::*;
#[derive(Debug, Deserialize, Serialize)]
struct RunLuaExecRequest {
#[serde(default)]
task: String,
#[serde(default)]
code: Option<String>,
#[serde(default)]
file: Option<String>,
#[serde(default = "default_runlua_exec_args")]
args: Value,
#[serde(default = "default_runlua_timeout_ms")]
timeout_ms: u64,
#[serde(default)]
caller_tool_name: Option<String>,
}
pub(super) fn default_runlua_timeout_ms() -> u64 {
60_000
}
pub(super) fn runlua_cwd_guard() -> &'static Mutex<()> {
static RUNLUA_CWD_GUARD: OnceLock<Mutex<()>> = OnceLock::new();
RUNLUA_CWD_GUARD.get_or_init(|| Mutex::new(()))
}
fn build_luaexec_call_request_context() -> RuntimeRequestContext {
RuntimeRequestContext {
request_id: None,
client_name: None,
transport_name: Some("luaexec_call".to_string()),
session_id: Some("luaexec-call-internal".to_string()),
client_info: Some(RuntimeClientInfo {
kind: Some("runtime".to_string()),
name: Some("luaexec_call".to_string()),
version: Some("internal-runtime".to_string()),
}),
client_capabilities: json!({}),
}
}
#[derive(Debug)]
struct RunLuaRenderedValue {
format: &'static str,
content: String,
}
fn looks_like_lua_debug_value(text: &str) -> bool {
["table: 0x", "function: 0x", "thread: 0x", "userdata: 0x"]
.iter()
.any(|prefix| text.starts_with(prefix))
}
#[cfg(windows)]
pub(super) fn has_invalid_windows_path_syntax(text: &str) -> bool {
let trimmed = text.trim();
if trimmed.starts_with(r"\\?\") {
return false;
}
let first_char = trimmed.chars().next();
for (index, ch) in trimmed.char_indices() {
if ch.is_control() {
return true;
}
if matches!(ch, '<' | '>' | '"' | '|' | '?' | '*') {
return true;
}
if ch == ':' {
let is_drive_prefix =
index == 1 && first_char.map(|c| c.is_ascii_alphabetic()).unwrap_or(false);
if !is_drive_prefix {
return true;
}
}
}
false
}
pub(super) fn require_string_arg(
value: LuaValue,
fn_name: &str,
param_name: &str,
allow_blank: bool,
) -> mlua::Result<String> {
let raw = match value {
LuaValue::String(text) => text
.to_str()
.map_err(|_| {
mlua::Error::runtime(format!(
"{fn_name}: {param_name} must be a valid UTF-8 string"
))
})?
.to_string(),
other => {
return Err(mlua::Error::runtime(format!(
"{fn_name}: {param_name} must be a string, got {}",
lua_value_type_name(&other)
)));
}
};
if !allow_blank && raw.trim().is_empty() {
return Err(mlua::Error::runtime(format!(
"{fn_name}: {param_name} must not be empty"
)));
}
if raw.contains('\0') {
return Err(mlua::Error::runtime(format!(
"{fn_name}: {param_name} must not contain NUL bytes"
)));
}
Ok(raw)
}
fn validate_path_text(text: &str, fn_name: &str, param_name: &str) -> mlua::Result<()> {
if looks_like_lua_debug_value(text) {
return Err(mlua::Error::runtime(format!(
"{fn_name}: {param_name} looks like a coerced Lua object string `{text}`"
)));
}
#[cfg(windows)]
if has_invalid_windows_path_syntax(text) {
return Err(mlua::Error::runtime(format!(
"{fn_name}: {param_name} contains invalid Windows path syntax"
)));
}
Ok(())
}
pub(super) fn require_path_arg(
value: LuaValue,
fn_name: &str,
param_name: &str,
) -> mlua::Result<String> {
let text = require_string_arg(value, fn_name, param_name, false)?;
validate_path_text(&text, fn_name, param_name)?;
Ok(text)
}
pub(super) fn optional_u64_arg(
value: LuaValue,
fn_name: &str,
param_name: &str,
) -> mlua::Result<Option<u64>> {
match value {
LuaValue::Nil => Ok(None),
LuaValue::Integer(v) if v >= 0 => Ok(Some(v as u64)),
LuaValue::Number(v) if v.is_finite() && v >= 0.0 && v.fract() == 0.0 => Ok(Some(v as u64)),
other => Err(mlua::Error::runtime(format!(
"{fn_name}: {param_name} must be a non-negative integer: {}",
lua_value_type_name(&other)
))),
}
}
pub(super) fn require_table_arg(
value: LuaValue,
fn_name: &str,
param_name: &str,
) -> mlua::Result<Table> {
match value {
LuaValue::Table(table) => Ok(table),
other => Err(mlua::Error::runtime(format!(
"{fn_name}: {param_name} must be a table, got {}",
lua_value_type_name(&other)
))),
}
}
pub(super) enum ExecMode {
Shell { command: String },
Program { program: String, args: Vec<String> },
}
pub(super) struct ExecRequest {
mode: ExecMode,
cwd: Option<String>,
env: HashMap<String, String>,
stdin: Option<String>,
timeout_ms: Option<u64>,
stdout_encoding: RuntimeTextEncoding,
stderr_encoding: RuntimeTextEncoding,
stdin_encoding: RuntimeTextEncoding,
}
pub(super) struct ExecResult {
ok: bool,
success: bool,
code: Option<i32>,
stdout: String,
stderr: String,
timed_out: bool,
error: Option<String>,
stdout_encoding: String,
stderr_encoding: String,
stdout_lossy: bool,
stderr_lossy: bool,
stdout_base64: Option<String>,
stderr_base64: Option<String>,
}
fn require_exec_scalar_text(
value: LuaValue,
fn_name: &str,
param_name: &str,
allow_blank: bool,
) -> mlua::Result<String> {
match value {
LuaValue::String(_) => require_string_arg(value, fn_name, param_name, allow_blank),
LuaValue::Integer(number) => Ok(number.to_string()),
LuaValue::Number(number) => {
if !number.is_finite() {
return Err(mlua::Error::runtime(format!(
"{fn_name}: {param_name} must be a finite number"
)));
}
Ok(number.to_string())
}
LuaValue::Boolean(flag) => Ok(flag.to_string()),
other => Err(mlua::Error::runtime(format!(
"{fn_name}: {param_name} must be a string: {}",
lua_value_type_name(&other)
))),
}
}
fn table_get_optional_string_field(
table: &Table,
fn_name: &str,
field_name: &str,
allow_blank: bool,
) -> mlua::Result<Option<String>> {
let value: LuaValue = table.get(field_name)?;
match value {
LuaValue::Nil => Ok(None),
other => Ok(Some(require_string_arg(
other,
fn_name,
field_name,
allow_blank,
)?)),
}
}
fn table_get_optional_bool_field(
table: &Table,
fn_name: &str,
field_name: &str,
) -> mlua::Result<Option<bool>> {
let value: LuaValue = table.get(field_name)?;
match value {
LuaValue::Nil => Ok(None),
LuaValue::Boolean(flag) => Ok(Some(flag)),
other => Err(mlua::Error::runtime(format!(
"{fn_name}: {field_name} must be a boolean when provided: {}",
lua_value_type_name(&other)
))),
}
}
fn table_get_optional_timeout_field(
table: &Table,
fn_name: &str,
field_name: &str,
) -> mlua::Result<Option<u64>> {
let value: LuaValue = table.get(field_name)?;
match value {
LuaValue::Nil => Ok(None),
LuaValue::Integer(number) if number > 0 => Ok(Some(number as u64)),
LuaValue::Number(number) if number.is_finite() && number.fract() == 0.0 && number > 0.0 => {
Ok(Some(number as u64))
}
other => Err(mlua::Error::runtime(format!(
"{fn_name}: {field_name} must be a positive integer in milliseconds: {}",
lua_value_type_name(&other)
))),
}
}
fn table_get_optional_encoding_field(
table: &Table,
fn_name: &str,
field_name: &str,
) -> mlua::Result<Option<RuntimeTextEncoding>> {
let Some(label) = table_get_optional_string_field(table, fn_name, field_name, false)? else {
return Ok(None);
};
RuntimeTextEncoding::parse(&label)
.map(Some)
.map_err(|error| mlua::Error::runtime(format!("{fn_name}: {field_name}: {error}")))
}
fn table_get_string_list_field(
table: &Table,
fn_name: &str,
field_name: &str,
) -> mlua::Result<Vec<String>> {
let value: LuaValue = table.get(field_name)?;
match value {
LuaValue::Nil => Ok(Vec::new()),
other => {
let list = require_table_arg(other, fn_name, field_name)?;
let mut items = Vec::new();
for (index, item) in list.sequence_values::<LuaValue>().enumerate() {
let item = item.map_err(|error| {
mlua::Error::runtime(format!(
"{fn_name}: failed to read {field_name}[{}]: {}, {}",
index + 1,
index + 1,
error
))
})?;
items.push(require_exec_scalar_text(
item,
fn_name,
&format!("{field_name}[{}]", index + 1),
true,
)?);
}
Ok(items)
}
}
}
fn table_get_string_map_field(
table: &Table,
fn_name: &str,
field_name: &str,
) -> mlua::Result<HashMap<String, String>> {
let value: LuaValue = table.get(field_name)?;
match value {
LuaValue::Nil => Ok(HashMap::new()),
other => {
let map_table = require_table_arg(other, fn_name, field_name)?;
let mut items = HashMap::new();
for pair in map_table.pairs::<LuaValue, LuaValue>() {
let (key_value, field_value) = pair.map_err(|_error| {
mlua::Error::runtime(format!("{fn_name}: failed to read {field_name}"))
})?;
let key =
require_string_arg(key_value, fn_name, &format!("{field_name}.<key>"), false)?;
let value_text = require_exec_scalar_text(
field_value,
fn_name,
&format!("{field_name}.{key}"),
true,
)?;
items.insert(key, value_text);
}
Ok(items)
}
}
}
pub(super) fn resolve_host_default_text_encoding(
host_options: &LuaRuntimeHostOptions,
) -> Result<RuntimeTextEncoding, String> {
match host_options.default_text_encoding.as_deref() {
Some(label) if !label.trim().is_empty() => RuntimeTextEncoding::parse(label),
_ => Ok(default_runtime_text_encoding()),
}
}
pub(super) fn parse_exec_request(
value: LuaValue,
fn_name: &str,
default_encoding: RuntimeTextEncoding,
) -> mlua::Result<ExecRequest> {
match value {
LuaValue::String(command_text) => Ok(ExecRequest {
mode: ExecMode::Shell {
command: require_string_arg(
LuaValue::String(command_text),
fn_name,
"command",
false,
)?,
},
cwd: None,
env: HashMap::new(),
stdin: None,
timeout_ms: None,
stdout_encoding: default_encoding,
stderr_encoding: default_encoding,
stdin_encoding: default_encoding,
}),
LuaValue::Table(spec) => {
let command = table_get_optional_string_field(&spec, fn_name, "command", false)?;
let program = table_get_optional_string_field(&spec, fn_name, "program", false)?;
let args = table_get_string_list_field(&spec, fn_name, "args")?;
let cwd = table_get_optional_string_field(&spec, fn_name, "cwd", false)?;
let env = table_get_string_map_field(&spec, fn_name, "env")?;
let stdin = table_get_optional_string_field(&spec, fn_name, "stdin", true)?;
let timeout_ms = table_get_optional_timeout_field(&spec, fn_name, "timeout_ms")?;
let shell_override = table_get_optional_bool_field(&spec, fn_name, "shell")?;
let encoding = table_get_optional_encoding_field(&spec, fn_name, "encoding")?
.unwrap_or(default_encoding);
let stdout_encoding =
table_get_optional_encoding_field(&spec, fn_name, "stdout_encoding")?
.unwrap_or(encoding);
let stderr_encoding =
table_get_optional_encoding_field(&spec, fn_name, "stderr_encoding")?
.unwrap_or(encoding);
let stdin_encoding =
table_get_optional_encoding_field(&spec, fn_name, "stdin_encoding")?
.unwrap_or(encoding);
if let Some(current_dir) = cwd.as_deref() {
validate_path_text(current_dir, fn_name, "cwd")?;
}
let mode = match (command, program) {
(Some(command_text), None) => {
if matches!(shell_override, Some(false)) {
return Err(mlua::Error::runtime(format!(
"{fn_name}: shell=false cannot be used with command mode"
)));
}
if !args.is_empty() {
return Err(mlua::Error::runtime(format!(
"{fn_name}: args is only supported with program mode"
)));
}
ExecMode::Shell {
command: command_text,
}
}
(None, Some(program_path)) => {
if matches!(shell_override, Some(true)) {
return Err(mlua::Error::runtime(format!(
"{fn_name}: shell=true requires command mode"
)));
}
ExecMode::Program {
program: program_path,
args,
}
}
(Some(_), Some(_)) => {
return Err(mlua::Error::runtime(format!(
"{fn_name}: command and program are mutually exclusive"
)));
}
(None, None) => {
return Err(mlua::Error::runtime(format!(
"{fn_name}: expected a string command or a table with command"
)));
}
};
Ok(ExecRequest {
mode,
cwd,
env,
stdin,
timeout_ms,
stdout_encoding,
stderr_encoding,
stdin_encoding,
})
}
other => Err(mlua::Error::runtime(format!(
"{fn_name}: expected a string or table, got {}",
lua_value_type_name(&other)
))),
}
}
#[cfg(windows)]
fn default_shell_launcher() -> (&'static str, &'static str) {
("cmd.exe", "/C")
}
#[cfg(not(windows))]
fn default_shell_launcher() -> (&'static str, &'static str) {
("sh", "-c")
}
fn spawn_pipe_reader<R>(mut reader: R) -> thread::JoinHandle<Vec<u8>>
where
R: Read + Send + 'static,
{
thread::spawn(move || {
let mut buffer = Vec::new();
let _ = reader.read_to_end(&mut buffer);
buffer
})
}
fn spawn_stdin_writer<W>(mut writer: W, input: Vec<u8>) -> thread::JoinHandle<()>
where
W: Write + Send + 'static,
{
thread::spawn(move || {
let _ = writer.write_all(&input);
let _ = writer.flush();
})
}
fn exec_error_result(error_text: String, request: &ExecRequest, timed_out: bool) -> ExecResult {
ExecResult {
ok: false,
success: false,
code: None,
stdout: String::new(),
stderr: error_text.clone(),
timed_out,
error: Some(error_text),
stdout_encoding: request.stdout_encoding.requested_label().to_string(),
stderr_encoding: request.stderr_encoding.requested_label().to_string(),
stdout_lossy: false,
stderr_lossy: false,
stdout_base64: None,
stderr_base64: None,
}
}
pub(super) fn execute_exec_request(request: ExecRequest) -> ExecResult {
let stdin_bytes = match request.stdin.as_deref() {
Some(input) => match encode_runtime_text(input, request.stdin_encoding) {
Ok(bytes) => Some(bytes),
Err(error) => {
let error_text = format!("failed to encode process stdin: {error}");
return exec_error_result(error_text, &request, false);
}
},
None => None,
};
let mut command = match &request.mode {
ExecMode::Shell { command } => {
let (shell_program, shell_flag) = default_shell_launcher();
let mut process = Command::new(shell_program);
process.arg(shell_flag).arg(command);
process
}
ExecMode::Program { program, args } => {
let mut process = Command::new(program);
process.args(args);
process
}
};
if let Some(current_dir) = &request.cwd {
command.current_dir(current_dir);
}
if !request.env.is_empty() {
command.envs(&request.env);
}
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
command.stdin(if stdin_bytes.is_some() {
Stdio::piped()
} else {
Stdio::null()
});
let mut child = match command.spawn() {
Ok(child) => child,
Err(error) => {
let error_text = format!("failed to spawn process: {}", error);
return exec_error_result(error_text, &request, false);
}
};
let stdout_handle = child.stdout.take().map(spawn_pipe_reader);
let stderr_handle = child.stderr.take().map(spawn_pipe_reader);
let stdin_handle = match (stdin_bytes, child.stdin.take()) {
(Some(input), Some(stdin)) => Some(spawn_stdin_writer(stdin, input)),
_ => None,
};
let mut timed_out = false;
let timeout = request.timeout_ms.map(Duration::from_millis);
let started_at = Instant::now();
let final_status = loop {
match child.try_wait() {
Ok(Some(status)) => {
break Some(status);
}
Ok(None) => {
if let Some(limit) = timeout {
if started_at.elapsed() >= limit {
timed_out = true;
let _ = child.kill();
break child.wait().ok();
}
}
thread::sleep(Duration::from_millis(10));
}
Err(error) => {
let error_text = format!("failed to wait for process: {}", error);
return exec_error_result(error_text, &request, timed_out);
}
}
};
if let Some(handle) = stdin_handle {
let _ = handle.join();
}
let stdout_bytes = stdout_handle
.map(|handle| handle.join().unwrap_or_default())
.unwrap_or_default();
let stderr_bytes = stderr_handle
.map(|handle| handle.join().unwrap_or_default())
.unwrap_or_default();
let decoded_stdout = decode_runtime_text(&stdout_bytes, request.stdout_encoding);
let decoded_stderr = decode_runtime_text(&stderr_bytes, request.stderr_encoding);
let stdout = decoded_stdout.text;
let mut stderr = decoded_stderr.text;
let status = match final_status {
Some(status) => status,
None => {
let error_text = "process finished without status".to_string();
return ExecResult {
ok: false,
success: false,
code: None,
stdout,
stderr: error_text.clone(),
timed_out,
error: Some(error_text),
stdout_encoding: decoded_stdout.encoding,
stderr_encoding: decoded_stderr.encoding,
stdout_lossy: decoded_stdout.lossy,
stderr_lossy: decoded_stderr.lossy,
stdout_base64: decoded_stdout.base64,
stderr_base64: decoded_stderr.base64,
};
}
};
let code = status.code();
let success = !timed_out && status.success();
let mut error = None;
if timed_out {
let timeout_value = request.timeout_ms.unwrap_or_default();
let timeout_text = format!("process execution timed out after {} ms", timeout_value);
if !stderr.is_empty() {
stderr.push('\n');
}
stderr.push_str(&timeout_text);
error = Some(timeout_text);
} else if !success {
error = Some(match code {
Some(exit_code) => format!("process exited with code {}", exit_code),
None => "process terminated without an exit code".to_string(),
});
}
ExecResult {
ok: success,
success,
code,
stdout,
stderr,
timed_out,
error,
stdout_encoding: decoded_stdout.encoding,
stderr_encoding: decoded_stderr.encoding,
stdout_lossy: decoded_stdout.lossy,
stderr_lossy: decoded_stderr.lossy,
stdout_base64: decoded_stdout.base64,
stderr_base64: decoded_stderr.base64,
}
}
pub(super) fn exec_result_to_lua_table(lua: &Lua, result: ExecResult) -> mlua::Result<Table> {
let table = lua.create_table()?;
table.set("ok", result.ok)?;
table.set("success", result.success)?;
table.set("stdout", result.stdout)?;
table.set("stderr", result.stderr)?;
table.set("stdout_encoding", result.stdout_encoding)?;
table.set("stderr_encoding", result.stderr_encoding)?;
table.set("stdout_lossy", result.stdout_lossy)?;
table.set("stderr_lossy", result.stderr_lossy)?;
match result.stdout_base64 {
Some(stdout_base64) => table.set("stdout_base64", stdout_base64)?,
None => table.set("stdout_base64", LuaValue::Nil)?,
}
match result.stderr_base64 {
Some(stderr_base64) => table.set("stderr_base64", stderr_base64)?,
None => table.set("stderr_base64", LuaValue::Nil)?,
}
table.set("timed_out", result.timed_out)?;
match result.code {
Some(code) => table.set("code", code)?,
None => table.set("code", LuaValue::Nil)?,
}
match result.error {
Some(error_text) => table.set("error", error_text)?,
None => table.set("error", LuaValue::Nil)?,
}
Ok(table)
}
impl LuaEngine {
pub(super) fn populate_vulcan_luaexec_bridge(
lua: &Lua,
host_options: Arc<LuaRuntimeHostOptions>,
runlua_pool: Arc<LuaVmPool>,
skill_config_store: Arc<SkillConfigStore>,
skills: Arc<HashMap<String, LoadedSkill>>,
entry_registry: Arc<BTreeMap<String, ResolvedEntryTarget>>,
runtime_skill_roots: Vec<RuntimeSkillRoot>,
lancedb_host: Option<Arc<LanceDbSkillHost>>,
sqlite_host: Option<Arc<SqliteSkillHost>>,
) -> Result<(), String> {
let runtime_lua = get_vulcan_runtime_lua_table(lua)?;
let exec_fn = lua
.create_function(move |lua, input: LuaValue| {
let input_table = require_table_arg(input, "runtime.lua.exec", "input")?;
let input_json = lua_value_to_json(&LuaValue::Table(input_table))
.map_err(mlua::Error::runtime)?;
let mut request: RunLuaExecRequest =
serde_json::from_value(input_json).map_err(|error| {
mlua::Error::runtime(format!("luaexec input is invalid: {}", error))
})?;
let internal =
get_vulcan_runtime_internal_table(lua).map_err(mlua::Error::runtime)?;
let caller_tool_name: Option<String> =
internal.get("tool_name").map_err(mlua::Error::runtime)?;
request.caller_tool_name = caller_tool_name
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let rendered = LuaEngine::execute_runlua_request_inline_with_runtime(
&request,
runlua_pool.clone(),
skills.clone(),
entry_registry.clone(),
host_options.clone(),
skill_config_store.clone(),
runtime_skill_roots.clone(),
lancedb_host.clone(),
sqlite_host.clone(),
)
.map_err(mlua::Error::runtime)?;
Ok(LuaValue::String(
lua.create_string(&rendered).map_err(mlua::Error::runtime)?,
))
})
.map_err(|error| format!("Failed to create vulcan.runtime.lua.exec: {}", error))?;
runtime_lua
.set("exec", exec_fn)
.map_err(|error| format!("Failed to set vulcan.runtime.lua.exec: {}", error))?;
Ok(())
}
fn run_lua_with_lease(
&self,
lease: &mut LuaVmLease,
code: &str,
args: &Value,
invocation_context: Option<&LuaInvocationContext>,
) -> Result<Value, String> {
let scope_guard = LuaVmRequestScopeGuard::new(lease, self.host_options.as_ref())?;
let lua = scope_guard.lua();
Self::populate_vulcan_request_context(lua, invocation_context)?;
populate_vulcan_internal_execution_context(
lua,
&VulcanInternalExecutionContext::default(),
)?;
populate_vulcan_file_context(lua, None, None)?;
populate_vulcan_dependency_context(lua, self.host_options.as_ref(), None, None)?;
Self::populate_vulcan_lancedb_context(lua, None, None)?;
Self::populate_vulcan_sqlite_context(lua, None, None)?;
let args_table = json_to_lua_table(lua, args)?;
lua.globals()
.set("__runlua_args", args_table)
.map_err(|e| format!("Failed to set args: {}", e))?;
let wrapper = format!(
"return (function()\n local args = __runlua_args\n {}\nend)()",
code
);
let run_result = (|| {
let result = lua.load(&wrapper).eval::<LuaValue>().map_err(|e| {
let msg = format!("Lua run_lua error: {}", e);
log_error(format!("[LuaSkill:error] {}", msg));
msg
})?;
lua_value_to_json(&result)
})();
let cleanup_result = scope_guard.finish();
match (run_result, cleanup_result) {
(Ok(result), Ok(())) => Ok(result),
(Ok(_), Err(cleanup_error)) => Err(cleanup_error),
(Err(run_error), Ok(())) => Err(run_error),
(Err(run_error), Err(cleanup_error)) => Err(format!(
"{}; pooled Lua VM cleanup failed: {}",
run_error, cleanup_error
)),
}
}
pub fn run_lua(
&self,
code: &str,
args: &Value,
invocation_context: Option<&LuaInvocationContext>,
) -> Result<Value, String> {
let mut lease = self.acquire_vm()?;
self.run_lua_with_lease(&mut lease, code, args, invocation_context)
}
fn acquire_runlua_vm(
runlua_pool: Arc<LuaVmPool>,
skills: Arc<HashMap<String, LoadedSkill>>,
entry_registry: Arc<BTreeMap<String, ResolvedEntryTarget>>,
host_options: Arc<LuaRuntimeHostOptions>,
skill_config_store: Arc<SkillConfigStore>,
runtime_skill_roots: Vec<RuntimeSkillRoot>,
lancedb_host: Option<Arc<LanceDbSkillHost>>,
sqlite_host: Option<Arc<SqliteSkillHost>>,
) -> Result<LuaVmLease, String> {
runlua_pool.acquire(move || {
Self::create_runlua_vm(
skills.as_ref(),
entry_registry.as_ref(),
host_options.clone(),
skill_config_store.clone(),
runtime_skill_roots.clone(),
lancedb_host.clone(),
sqlite_host.clone(),
)
})
}
fn execute_runlua_request_inline_with_runtime(
request: &RunLuaExecRequest,
runlua_pool: Arc<LuaVmPool>,
skills: Arc<HashMap<String, LoadedSkill>>,
entry_registry: Arc<BTreeMap<String, ResolvedEntryTarget>>,
host_options: Arc<LuaRuntimeHostOptions>,
skill_config_store: Arc<SkillConfigStore>,
runtime_skill_roots: Vec<RuntimeSkillRoot>,
lancedb_host: Option<Arc<LanceDbSkillHost>>,
sqlite_host: Option<Arc<SqliteSkillHost>>,
) -> Result<String, String> {
if request.timeout_ms == 0 {
return Err("luaexec timeout_ms must be greater than 0".to_string());
}
let (resolved_code, entry_file) = Self::resolve_runlua_source(request)?;
let mut lease = Self::acquire_runlua_vm(
runlua_pool,
skills,
entry_registry,
host_options.clone(),
skill_config_store,
runtime_skill_roots,
lancedb_host,
sqlite_host,
)?;
let scope_guard = LuaVmRequestScopeGuard::new(&mut lease, host_options.as_ref())?;
let lua = scope_guard.lua();
let simulated_request_context = build_luaexec_call_request_context();
let simulated_invocation_context = LuaInvocationContext::new(
Some(simulated_request_context),
Value::Object(serde_json::Map::new()),
Value::Object(serde_json::Map::new()),
);
Self::populate_vulcan_request_context(lua, Some(&simulated_invocation_context))?;
populate_vulcan_internal_execution_context(
lua,
&VulcanInternalExecutionContext {
tool_name: None,
skill_name: None,
entry_name: None,
root_name: None,
luaexec_active: true,
luaexec_caller_tool_name: request.caller_tool_name.clone(),
},
)?;
populate_vulcan_file_context(lua, None, entry_file.as_deref())?;
Self::populate_vulcan_lancedb_context(lua, None, None)?;
Self::populate_vulcan_sqlite_context(lua, None, None)?;
let captured_output: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
Self::configure_runlua_execution_environment(
lua,
captured_output.clone(),
host_options.as_ref(),
)?;
let args_table = json_to_lua_table(lua, &request.args)?;
lua.globals()
.set("__runlua_args", args_table)
.map_err(|error| format!("Failed to set runlua args: {}", error))?;
let wrapper = format!(
"return (function()\n local args = __runlua_args\n return table.pack((function()\n{}\nend)())\nend)()",
resolved_code
);
Self::install_runlua_timeout_guard(lua, request.timeout_ms)
.map_err(|error| error.to_string())?;
let execution_result = Self::execute_runlua_wrapper(lua, &wrapper, entry_file.as_deref());
Self::remove_runlua_timeout_guard(lua);
let printed_output = captured_output
.lock()
.map_err(|_| "Failed to lock runlua output capture".to_string())?
.clone();
let render_result = match execution_result {
Ok(returned_values) => {
let rendered_values = Self::collect_runlua_return_values(&returned_values)?;
Ok(Self::render_runlua_success_markdown(
request,
&printed_output,
&rendered_values,
))
}
Err(error) => Ok(Self::render_runlua_error_markdown(
request,
&printed_output,
error.to_string().as_str(),
)),
};
let cleanup_result = scope_guard.finish();
match (render_result, cleanup_result) {
(Ok(rendered), Ok(())) => Ok(rendered),
(Ok(_), Err(cleanup_error)) => Err(cleanup_error),
(Err(render_error), Ok(())) => Err(render_error),
(Err(render_error), Err(cleanup_error)) => Err(format!(
"{}; pooled runlua VM cleanup failed: {}",
render_error, cleanup_error
)),
}
}
fn execute_runlua_request_inline(&self, request: &RunLuaExecRequest) -> Result<String, String> {
Self::execute_runlua_request_inline_with_runtime(
request,
self.runlua_pool.clone(),
Arc::new(self.skills.clone()),
Arc::new(self.entry_registry.clone()),
self.host_options.clone(),
self.skill_config_store.clone(),
self.runtime_skill_roots.clone(),
self.lancedb_host.clone(),
self.sqlite_host.clone(),
)
}
fn resolve_runlua_source(
request: &RunLuaExecRequest,
) -> Result<(String, Option<PathBuf>), String> {
let inline_code = request
.code
.as_ref()
.map(|value| value.trim())
.filter(|value| !value.is_empty())
.map(|value| value.to_string());
let file_path = request
.file
.as_ref()
.map(|value| value.trim())
.filter(|value| !value.is_empty())
.map(|value| value.to_string());
match (inline_code, file_path) {
(Some(_), Some(_)) => {
Err("luaexec accepts either code or file, but not both".to_string())
}
(None, None) => Err("luaexec requires code or file".to_string()),
(Some(code), None) => Ok((code, None)),
(None, Some(file_text)) => {
validate_path_text(&file_text, "luaexec", "file")
.map_err(|error| error.to_string())?;
let raw_file_path = PathBuf::from(&file_text);
let file_path = if raw_file_path.is_absolute() {
raw_file_path
} else {
std::env::current_dir()
.map_err(|error| {
format!("Failed to resolve luaexec relative file path: {}", error)
})?
.join(raw_file_path)
};
let source = std::fs::read_to_string(&file_path).map_err(|error| {
format!(
"Failed to read luaexec file {}: {}: {}",
file_path.display(),
error,
error
)
})?;
Ok((source, Some(file_path)))
}
}
}
pub fn execute_runlua_request_json_inline(&self, request_json: &str) -> Result<String, String> {
let request: RunLuaExecRequest = serde_json::from_str(request_json)
.map_err(|error| format!("Invalid luaexec request JSON: {}", error))?;
self.execute_runlua_request_inline(&request)
}
fn execute_runlua_wrapper(
lua: &Lua,
wrapper: &str,
entry_file: Option<&Path>,
) -> Result<Table, mlua::Error> {
match entry_file.and_then(Path::parent) {
Some(entry_dir) => {
let _cwd_guard = runlua_cwd_guard()
.lock()
.map_err(|_| mlua::Error::runtime("luaexec cwd guard lock poisoned"))?;
let original_dir = std::env::current_dir()
.map_err(|error| mlua::Error::runtime(format!("luaexec cwd: {}", error)))?;
std::env::set_current_dir(entry_dir)
.map_err(|error| mlua::Error::runtime(format!("luaexec set cwd: {}", error)))?;
let execution = lua.load(wrapper).eval::<Table>();
let restore_result = std::env::set_current_dir(&original_dir).map_err(|error| {
mlua::Error::runtime(format!("luaexec restore cwd: {}", error))
});
match (execution, restore_result) {
(Ok(table), Ok(())) => Ok(table),
(Err(error), Ok(())) => Err(error),
(_, Err(error)) => Err(error),
}
}
None => lua.load(wrapper).eval::<Table>(),
}
}
fn configure_runlua_execution_environment(
lua: &Lua,
captured_output: Arc<Mutex<Vec<String>>>,
host_options: &LuaRuntimeHostOptions,
) -> Result<(), String> {
let runtime = get_vulcan_runtime_table(lua)?;
let runtime_lua = get_vulcan_runtime_lua_table(lua)?;
let vulcan = get_vulcan_table(lua)?;
let cache = vulcan
.get::<Table>("cache")
.map_err(|error| format!("Failed to get vulcan.cache: {}", error))?;
let vulcan_io = vulcan
.get::<Table>("io")
.map_err(|error| format!("Failed to get vulcan.io: {}", error))?;
let print_capture = captured_output.clone();
let print_fn = lua
.create_function(move |_, args: MultiValue| {
let mut parts = Vec::new();
for value in args.into_iter() {
parts.push(LuaEngine::render_lua_value_inline(&value));
}
let mut guard = print_capture
.lock()
.map_err(|_| mlua::Error::runtime("runlua print capture lock poisoned"))?;
guard.push(parts.join("\t"));
Ok(())
})
.map_err(|error| format!("Failed to create runlua print capture: {}", error))?;
lua.globals()
.set("print", print_fn)
.map_err(|error| format!("Failed to override global print for runlua: {}", error))?;
lua.load(
r#"
if jit and type(jit.off) == "function" then
jit.off(true, true)
end
if jit and type(jit.flush) == "function" then
jit.flush()
end
"#,
)
.exec()
.map_err(|error| format!("Failed to disable JIT for runlua: {}", error))?;
runtime
.set("log", LuaValue::Nil)
.map_err(|error| format!("Failed to clear vulcan.runtime.log for runlua: {}", error))?;
cache
.set("put", LuaValue::Nil)
.map_err(|error| format!("Failed to clear vulcan.cache.put for runlua: {}", error))?;
cache
.set("get", LuaValue::Nil)
.map_err(|error| format!("Failed to clear vulcan.cache.get for runlua: {}", error))?;
cache.set("delete", LuaValue::Nil).map_err(|error| {
format!("Failed to clear vulcan.cache.delete for runlua: {}", error)
})?;
runtime_lua.set("exec", LuaValue::Nil).map_err(|error| {
format!(
"Failed to clear vulcan.runtime.lua.exec for runlua: {}",
error
)
})?;
if host_options.capabilities.enable_managed_io_compat {
let default_encoding = resolve_host_default_text_encoding(host_options)?;
install_managed_io_compat(lua, &vulcan_io, default_encoding).map_err(|error| {
format!(
"Failed to install managed io compatibility for runlua: {}",
error
)
})?;
}
Ok(())
}
pub(super) fn install_runlua_timeout_guard(lua: &Lua, timeout_ms: u64) -> mlua::Result<()> {
let deadline = Instant::now() + Duration::from_millis(timeout_ms);
let timeout_text = format!("luaexec execution timed out after {} ms", timeout_ms);
lua.set_hook(
HookTriggers::new().every_nth_instruction(1_000),
move |_, _| {
if Instant::now() >= deadline {
return Err(mlua::Error::runtime(timeout_text.clone()));
}
Ok(VmState::Continue)
},
)
}
pub(super) fn remove_runlua_timeout_guard(lua: &Lua) {
lua.remove_hook();
}
fn collect_runlua_return_values(
result_table: &Table,
) -> Result<Vec<RunLuaRenderedValue>, String> {
let value_count = result_table
.get::<i64>("n")
.map_err(|error| format!("Failed to read runlua return count: {}", error))?
.max(0) as usize;
let mut rendered_values = Vec::new();
if value_count == 0 {
rendered_values.push(RunLuaRenderedValue {
format: "json",
content: "null".to_string(),
});
return Ok(rendered_values);
}
for index in 1..=value_count {
let value: LuaValue = result_table.raw_get(index).map_err(|error| {
format!("Failed to read runlua return value {}: {}", index, error)
})?;
rendered_values.push(Self::render_runlua_value(&value));
}
Ok(rendered_values)
}
fn render_runlua_value(value: &LuaValue) -> RunLuaRenderedValue {
match value {
LuaValue::String(text) => RunLuaRenderedValue {
format: "text",
content: text
.to_str()
.map(|value| value.to_string())
.unwrap_or_default(),
},
_ => match lua_value_to_json(value) {
Ok(json_value) => RunLuaRenderedValue {
format: "json",
content: serde_json::to_string_pretty(&json_value)
.unwrap_or_else(|_| "null".to_string()),
},
Err(_) => RunLuaRenderedValue {
format: "text",
content: Self::render_lua_value_inline(value),
},
},
}
}
fn render_lua_value_inline(value: &LuaValue) -> String {
match value {
LuaValue::String(text) => text
.to_str()
.map(|value| value.to_string())
.unwrap_or_default(),
LuaValue::Integer(number) => number.to_string(),
LuaValue::Number(number) => number.to_string(),
LuaValue::Boolean(flag) => flag.to_string(),
LuaValue::Nil => "nil".to_string(),
_ => format!("{:?}", value),
}
}
fn render_runlua_success_markdown(
request: &RunLuaExecRequest,
printed_output: &[String],
rendered_values: &[RunLuaRenderedValue],
) -> String {
let mut lines = vec![
"# Runtime Execution Result".to_string(),
"".to_string(),
"## Task".to_string(),
if request.task.trim().is_empty() {
"Execute Lua runtime code".to_string()
} else {
request.task.trim().to_string()
},
"".to_string(),
"## Status".to_string(),
"SUCCESS".to_string(),
];
if !printed_output.is_empty() {
lines.extend([
"".to_string(),
"## Printed Output".to_string(),
"```text".to_string(),
printed_output.join("\n"),
"```".to_string(),
]);
}
lines.extend(["".to_string(), "## Returned Values".to_string()]);
for (index, value) in rendered_values.iter().enumerate() {
lines.push(format!("{}. ", index + 1));
lines.push(format!("```{}", value.format));
lines.push(value.content.clone());
lines.push("```".to_string());
if index + 1 < rendered_values.len() {
lines.push("".to_string());
}
}
lines.join("\n")
}
fn render_runlua_error_markdown(
request: &RunLuaExecRequest,
printed_output: &[String],
error_text: &str,
) -> String {
let mut lines = vec![
"# Runtime Execution Error".to_string(),
"".to_string(),
"## Task".to_string(),
if request.task.trim().is_empty() {
"Execute Lua runtime code".to_string()
} else {
request.task.trim().to_string()
},
"".to_string(),
"## Status".to_string(),
"FAILED".to_string(),
"".to_string(),
"## Error".to_string(),
"```text".to_string(),
error_text.to_string(),
"```".to_string(),
];
if !printed_output.is_empty() {
lines.extend([
"".to_string(),
"## Printed Output".to_string(),
"```text".to_string(),
printed_output.join("\n"),
"```".to_string(),
]);
}
lines.join("\n")
}
}