#[cfg(windows)]
use super::runlua::has_invalid_windows_path_syntax;
use super::runlua::{
default_runlua_timeout_ms, resolve_host_default_text_encoding, runlua_cwd_guard,
};
use super::*;
#[derive(Debug, Deserialize)]
struct RuntimeSessionCreateRequest {
sid: String,
#[serde(default)]
ttl_sec: Option<u64>,
#[serde(default)]
replace: bool,
#[serde(default)]
cwd: Option<String>,
#[serde(default)]
workspace_root: Option<String>,
#[serde(default)]
lua_roots: Vec<String>,
#[serde(default)]
c_roots: Vec<String>,
#[serde(default = "default_runlua_exec_args")]
mounts: Value,
}
#[derive(Debug, Deserialize)]
struct RuntimeSessionEvalRequest {
lease_id: String,
#[serde(default)]
sid: Option<String>,
#[serde(default)]
generation: Option<u64>,
code: String,
#[serde(default = "default_runlua_exec_args")]
args: Value,
#[serde(default = "default_runlua_timeout_ms")]
timeout_ms: u64,
#[serde(default)]
request_context: Option<RuntimeRequestContext>,
#[serde(default = "default_runlua_exec_args")]
client_budget: Value,
#[serde(default = "default_runlua_exec_args")]
tool_config: Value,
}
#[derive(Debug, Deserialize)]
struct RuntimeSessionLeaseRequest {
lease_id: String,
#[serde(default)]
sid: Option<String>,
#[serde(default)]
generation: Option<u64>,
}
#[derive(Debug, Deserialize)]
struct RuntimeSessionListRequest {
#[serde(default)]
sid: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum RuntimeLeaseProfile {
Public,
SystemLuaLib,
}
impl RuntimeLeaseProfile {
fn as_str(self) -> &'static str {
match self {
Self::Public => "public",
Self::SystemLuaLib => "system_lua_lib",
}
}
}
#[derive(Debug, Clone)]
struct RuntimeLeasePathContext {
cwd: Option<PathBuf>,
workspace_root: Option<PathBuf>,
lua_roots: Vec<PathBuf>,
c_roots: Vec<PathBuf>,
mounts: Value,
system_lua_lib_dir: Option<PathBuf>,
}
pub(super) struct RuntimeSessionManager {
state: Mutex<RuntimeSessionManagerState>,
}
struct RuntimeSessionManagerState {
leases: HashMap<String, RuntimeSessionEntry>,
sid_index: HashMap<String, String>,
tombstones: HashMap<String, RuntimeSessionTombstone>,
generations: HashMap<String, u64>,
next_sequence: u64,
}
pub(super) struct RuntimeSession {
sid: String,
lease_id: String,
generation: u64,
profile: RuntimeLeaseProfile,
ttl_sec: Option<u64>,
expires_at: Option<Instant>,
expires_at_unix_ms: Option<u128>,
path_context: RuntimeLeasePathContext,
vm: LuaVm,
terminal_state: Arc<AtomicU8>,
closed: bool,
}
struct RuntimeSessionEntry {
session: Arc<Mutex<RuntimeSession>>,
terminal_state: Arc<AtomicU8>,
snapshot: Value,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u8)]
enum RuntimeSessionTerminalState {
Active = 0,
Closed = 1,
Expired = 2,
Replaced = 3,
}
#[derive(Debug)]
pub(super) struct RuntimeSessionError {
pub(super) code: &'static str,
pub(super) message: String,
}
struct RuntimeSessionTombstone {
sid: String,
lease_id: String,
generation: u64,
profile: RuntimeLeaseProfile,
code: &'static str,
retired_at: Instant,
}
pub(super) fn default_runlua_exec_args() -> Value {
Value::Object(serde_json::Map::new())
}
fn default_runtime_session_ttl_sec() -> u64 {
600
}
impl RuntimeSessionEvalRequest {
fn to_invocation_context(&self) -> LuaInvocationContext {
LuaInvocationContext::new(
self.request_context.clone(),
self.client_budget.clone(),
self.tool_config.clone(),
)
}
}
impl RuntimeSessionManager {
pub(super) fn new() -> Self {
Self {
state: Mutex::new(RuntimeSessionManagerState {
leases: HashMap::new(),
sid_index: HashMap::new(),
tombstones: HashMap::new(),
generations: HashMap::new(),
next_sequence: 0,
}),
}
}
fn insert(
&self,
profile: RuntimeLeaseProfile,
sid: String,
ttl_sec: Option<u64>,
replace: bool,
path_context: RuntimeLeasePathContext,
vm: LuaVm,
) -> Result<Value, RuntimeSessionError> {
let mut state = self.lock_state()?;
Self::prune_inactive_locked(&mut state);
if let Some(existing_lease_id) = state.sid_index.get(&sid).cloned() {
if let Some(existing_session) = state
.leases
.get(&existing_lease_id)
.map(|entry| Arc::clone(&entry.session))
{
match existing_session.try_lock() {
Ok(existing_session) => {
if let Some(error) = existing_session.inactive_error() {
Self::retire_active_lease_locked(
&mut state,
&existing_lease_id,
error.code,
);
} else if !replace {
return Err(RuntimeSessionError {
code: "lease_exists",
message: format!(
"runtime session SID `{sid}` already has lease `{existing_lease_id}`"
),
});
} else {
Self::retire_active_lease_locked(
&mut state,
&existing_lease_id,
"lease_replaced",
);
}
}
Err(TryLockError::WouldBlock) => {
if replace {
return Err(RuntimeSessionError {
code: "lease_busy",
message: format!(
"runtime session SID `{sid}` cannot replace busy lease `{existing_lease_id}`"
),
});
}
return Err(RuntimeSessionError {
code: "lease_exists",
message: format!(
"runtime session SID `{sid}` already has lease `{existing_lease_id}`"
),
});
}
Err(TryLockError::Poisoned(_)) => {
return Err(RuntimeSessionError {
code: "lease_busy",
message: format!(
"runtime session lease `{existing_lease_id}` is unavailable because its lock is poisoned"
),
});
}
}
} else {
state.sid_index.remove(&sid);
}
}
if state.leases.len() >= 8 {
return Err(RuntimeSessionError {
code: "lease_limit_exceeded",
message: "runtime session lease limit exceeded".to_string(),
});
}
state.next_sequence = state.next_sequence.saturating_add(1);
let generation = state
.generations
.get(&sid)
.copied()
.unwrap_or(0)
.saturating_add(1);
state.generations.insert(sid.clone(), generation);
let lease_id = format!("rt_{}_{}", unix_time_millis(), state.next_sequence);
let ttl_sec = ttl_sec.map(|value| value.clamp(1, 3_600));
let (expires_at, expires_at_unix_ms) = runtime_session_expiry(ttl_sec);
let terminal_state = Arc::new(AtomicU8::new(RuntimeSessionTerminalState::Active as u8));
let session = RuntimeSession {
sid: sid.clone(),
lease_id: lease_id.clone(),
generation,
profile,
ttl_sec,
expires_at,
expires_at_unix_ms,
path_context,
vm,
terminal_state: Arc::clone(&terminal_state),
closed: false,
};
let snapshot = session.status_payload();
let response_snapshot = snapshot.clone();
state.leases.insert(
lease_id.clone(),
RuntimeSessionEntry {
session: Arc::new(Mutex::new(session)),
terminal_state,
snapshot,
},
);
state.sid_index.insert(sid.clone(), lease_id.clone());
Ok(json!({
"ok": true,
"sid": sid,
"lease_id": lease_id,
"generation": generation,
"profile": profile.as_str(),
"lifetime": if ttl_sec.is_some() { "finite" } else { "infinite" },
"cwd": response_snapshot.get("cwd").cloned().unwrap_or(Value::Null),
"workspace_root": response_snapshot
.get("workspace_root")
.cloned()
.unwrap_or(Value::Null),
"system_lua_lib": response_snapshot.get("system_lua_lib").cloned().unwrap_or(Value::Null),
"ttl_sec": ttl_sec,
"expires_at_unix_ms": expires_at_unix_ms
}))
}
pub(super) fn get(
&self,
lease_id: &str,
expected_sid: Option<&str>,
expected_generation: Option<u64>,
expected_profile: Option<RuntimeLeaseProfile>,
) -> Result<Arc<Mutex<RuntimeSession>>, RuntimeSessionError> {
let mut state = self.lock_state()?;
Self::prune_inactive_locked(&mut state);
if let Some(entry) = state.leases.get(lease_id) {
let session = Arc::clone(&entry.session);
let session_guard = session.try_lock().map_err(|_| RuntimeSessionError {
code: "lease_busy",
message: format!("runtime session lease `{lease_id}` is busy"),
})?;
Self::validate_session_identity(&session_guard, expected_sid, expected_generation)?;
Self::validate_session_profile(&session_guard, expected_profile)?;
drop(session_guard);
return Ok(session);
}
if let Some(tombstone) = state.tombstones.get(lease_id) {
Self::validate_tombstone_identity(tombstone, expected_sid, expected_generation)?;
Self::validate_tombstone_profile(tombstone, expected_profile)?;
return Err(tombstone.as_error());
}
Err(RuntimeSessionError {
code: "lease_not_found",
message: format!("runtime session lease `{lease_id}` was not found"),
})
}
fn status(
&self,
lease_id: &str,
expected_sid: Option<&str>,
expected_generation: Option<u64>,
expected_profile: Option<RuntimeLeaseProfile>,
) -> Result<Value, RuntimeSessionError> {
let session = self.get(
lease_id,
expected_sid,
expected_generation,
expected_profile,
)?;
let session = session.try_lock().map_err(|_| RuntimeSessionError {
code: "lease_busy",
message: format!("runtime session lease `{lease_id}` is busy"),
})?;
if let Some(error) = session.inactive_error() {
return Err(error);
}
Ok(session.status_payload())
}
fn list(
&self,
sid: Option<&str>,
expected_profile: Option<RuntimeLeaseProfile>,
) -> Result<Value, RuntimeSessionError> {
let mut state = self.lock_state()?;
Self::prune_inactive_locked(&mut state);
let mut leases = Vec::new();
for entry in state.leases.values() {
if sid.is_some_and(|expected_sid| entry.snapshot["sid"].as_str() != Some(expected_sid))
{
continue;
}
if expected_profile.is_some_and(|expected_profile| {
entry.snapshot.get("profile").and_then(Value::as_str)
!= Some(expected_profile.as_str())
}) {
continue;
}
leases.push(entry.snapshot.clone());
}
leases.sort_by(compare_runtime_session_payloads);
Ok(json!({
"ok": true,
"leases": leases,
}))
}
fn close(
&self,
lease_id: &str,
expected_sid: Option<&str>,
expected_generation: Option<u64>,
expected_profile: Option<RuntimeLeaseProfile>,
) -> Result<Value, RuntimeSessionError> {
let mut state = self.lock_state()?;
Self::prune_inactive_locked(&mut state);
let Some((session, terminal_state)) = state.leases.get(lease_id).map(|entry| {
(
Arc::clone(&entry.session),
Arc::clone(&entry.terminal_state),
)
}) else {
if let Some(tombstone) = state.tombstones.get(lease_id) {
Self::validate_tombstone_identity(tombstone, expected_sid, expected_generation)?;
Self::validate_tombstone_profile(tombstone, expected_profile)?;
return Err(tombstone.as_error());
}
return Err(RuntimeSessionError {
code: "lease_not_found",
message: format!("runtime session lease `{lease_id}` was not found"),
});
};
let mut session = session.try_lock().map_err(|_| RuntimeSessionError {
code: "lease_busy",
message: format!("runtime session lease `{lease_id}` is busy"),
})?;
Self::validate_session_identity(&session, expected_sid, expected_generation)?;
Self::validate_session_profile(&session, expected_profile)?;
terminal_state.store(RuntimeSessionTerminalState::Closed as u8, Ordering::Release);
session.closed = true;
let payload = session.close_payload();
let tombstone = RuntimeSessionTombstone::from_session(&session, "lease_closed");
let sid = session.sid.clone();
drop(session);
state.leases.remove(lease_id);
if state
.sid_index
.get(&sid)
.is_some_and(|current| current == lease_id)
{
state.sid_index.remove(&sid);
}
state.tombstones.insert(lease_id.to_string(), tombstone);
Ok(payload)
}
fn update_active_snapshot(
&self,
lease_id: &str,
snapshot: Value,
) -> Result<(), RuntimeSessionError> {
let mut state = self.lock_state()?;
if let Some(entry) = state.leases.get_mut(lease_id) {
entry.snapshot = snapshot;
}
Ok(())
}
fn lock_state(
&self,
) -> Result<std::sync::MutexGuard<'_, RuntimeSessionManagerState>, RuntimeSessionError> {
self.state.lock().map_err(|_| RuntimeSessionError {
code: "lease_manager_poisoned",
message: "runtime session manager lock poisoned".to_string(),
})
}
fn prune_inactive_locked(state: &mut RuntimeSessionManagerState) {
let now = Instant::now();
let mut removed = Vec::new();
for (lease_id, entry) in &state.leases {
let should_remove = entry
.session
.try_lock()
.map(|session| session.expires_at.is_some() && session.is_expired())
.unwrap_or(false);
if should_remove {
removed.push(lease_id.clone());
}
}
for lease_id in removed {
Self::retire_active_lease_locked(state, &lease_id, "lease_expired");
}
let tombstone_ttl = runtime_session_tombstone_ttl();
state
.tombstones
.retain(|_, tombstone| now.duration_since(tombstone.retired_at) < tombstone_ttl);
}
fn retire_active_lease_locked(
state: &mut RuntimeSessionManagerState,
lease_id: &str,
code: &'static str,
) {
if let Some(entry) = state.leases.get(lease_id) {
entry.terminal_state.store(
runtime_session_terminal_state_from_code(code) as u8,
Ordering::Release,
);
}
let Some(entry) = state.leases.remove(lease_id) else {
return;
};
let tombstone = RuntimeSessionTombstone::from_snapshot(&entry.snapshot, code);
if state
.sid_index
.get(&tombstone.sid)
.is_some_and(|current| current == lease_id)
{
state.sid_index.remove(&tombstone.sid);
}
state.tombstones.insert(lease_id.to_string(), tombstone);
}
fn validate_session_identity(
session: &RuntimeSession,
expected_sid: Option<&str>,
expected_generation: Option<u64>,
) -> Result<(), RuntimeSessionError> {
Self::validate_identity_parts(
&session.lease_id,
&session.sid,
session.generation,
expected_sid,
expected_generation,
)
}
fn validate_tombstone_identity(
tombstone: &RuntimeSessionTombstone,
expected_sid: Option<&str>,
expected_generation: Option<u64>,
) -> Result<(), RuntimeSessionError> {
Self::validate_identity_parts(
&tombstone.lease_id,
&tombstone.sid,
tombstone.generation,
expected_sid,
expected_generation,
)
}
fn validate_session_profile(
session: &RuntimeSession,
expected_profile: Option<RuntimeLeaseProfile>,
) -> Result<(), RuntimeSessionError> {
if let Some(expected_profile) = expected_profile {
if session.profile != expected_profile {
return Err(RuntimeSessionError {
code: "lease_profile_mismatch",
message: format!(
"runtime session lease `{}` belongs to profile `{}`, not `{}`",
session.lease_id,
session.profile.as_str(),
expected_profile.as_str()
),
});
}
}
Ok(())
}
fn validate_tombstone_profile(
tombstone: &RuntimeSessionTombstone,
expected_profile: Option<RuntimeLeaseProfile>,
) -> Result<(), RuntimeSessionError> {
if let Some(expected_profile) = expected_profile {
if tombstone.profile != expected_profile {
return Err(RuntimeSessionError {
code: "lease_profile_mismatch",
message: format!(
"runtime session lease `{}` belongs to profile `{}`, not `{}`",
tombstone.lease_id,
tombstone.profile.as_str(),
expected_profile.as_str()
),
});
}
}
Ok(())
}
fn validate_identity_parts(
lease_id: &str,
actual_sid: &str,
actual_generation: u64,
expected_sid: Option<&str>,
expected_generation: Option<u64>,
) -> Result<(), RuntimeSessionError> {
if let Some(expected_sid) = expected_sid {
if actual_sid != expected_sid {
return Err(RuntimeSessionError {
code: "lease_sid_mismatch",
message: format!(
"runtime session lease `{lease_id}` belongs to sid `{actual_sid}`, not `{expected_sid}`"
),
});
}
}
if let Some(expected_generation) = expected_generation {
if actual_generation != expected_generation {
return Err(RuntimeSessionError {
code: "lease_generation_mismatch",
message: format!(
"runtime session lease `{lease_id}` generation mismatch: expected {expected_generation}, actual {actual_generation}"
),
});
}
}
Ok(())
}
}
impl RuntimeSession {
fn inactive_error(&self) -> Option<RuntimeSessionError> {
if let Some(code) =
runtime_session_terminal_code_from_state(self.terminal_state.load(Ordering::Acquire))
{
return Some(RuntimeSessionError {
code,
message: format!(
"{} (sid `{}`, generation {})",
runtime_session_terminal_message(code, &self.lease_id),
self.sid,
self.generation
),
});
}
if self.closed {
return Some(RuntimeSessionError {
code: "lease_closed",
message: format!("runtime session lease `{}` is closed", self.lease_id),
});
}
if self.is_expired() {
return Some(RuntimeSessionError {
code: "lease_expired",
message: format!("runtime session lease `{}` is expired", self.lease_id),
});
}
None
}
fn refresh(&mut self) {
let (expires_at, expires_at_unix_ms) = runtime_session_expiry(self.ttl_sec);
self.expires_at = expires_at;
self.expires_at_unix_ms = expires_at_unix_ms;
}
fn is_expired(&self) -> bool {
self.expires_at
.is_some_and(|expires_at| Instant::now() >= expires_at)
}
fn status_payload(&self) -> Value {
json!({
"ok": runtime_session_terminal_code_from_state(
self.terminal_state.load(Ordering::Acquire),
).is_none() && !self.closed && !self.is_expired(),
"sid": self.sid.clone(),
"lease_id": self.lease_id.clone(),
"generation": self.generation,
"profile": self.profile.as_str(),
"lifetime": if self.ttl_sec.is_some() { "finite" } else { "infinite" },
"cwd": session_status_cwd_text(self),
"workspace_root": session_status_workspace_root_text(self),
"system_lua_lib": session_status_system_lua_lib_text(self),
"ttl_sec": self.ttl_sec,
"expires_at_unix_ms": self.expires_at_unix_ms,
"closed": self.closed,
"expired": self.is_expired()
})
}
fn close_payload(&self) -> Value {
json!({
"ok": true,
"sid": self.sid.clone(),
"lease_id": self.lease_id.clone(),
"generation": self.generation,
"profile": self.profile.as_str(),
"lifetime": if self.ttl_sec.is_some() { "finite" } else { "infinite" },
"cwd": session_status_cwd_text(self),
"workspace_root": session_status_workspace_root_text(self),
"system_lua_lib": session_status_system_lua_lib_text(self),
"ttl_sec": self.ttl_sec,
"expires_at_unix_ms": self.expires_at_unix_ms,
"closed": self.closed,
"expired": self.is_expired()
})
}
}
impl RuntimeSessionTombstone {
fn from_session(session: &RuntimeSession, code: &'static str) -> Self {
Self {
sid: session.sid.clone(),
lease_id: session.lease_id.clone(),
generation: session.generation,
profile: session.profile,
code,
retired_at: Instant::now(),
}
}
fn from_snapshot(snapshot: &Value, code: &'static str) -> Self {
Self {
sid: snapshot
.get("sid")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
lease_id: snapshot
.get("lease_id")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
generation: snapshot
.get("generation")
.and_then(Value::as_u64)
.unwrap_or(0),
profile: match snapshot
.get("profile")
.and_then(Value::as_str)
.unwrap_or("public")
{
"system_lua_lib" => RuntimeLeaseProfile::SystemLuaLib,
_ => RuntimeLeaseProfile::Public,
},
code,
retired_at: Instant::now(),
}
}
fn as_error(&self) -> RuntimeSessionError {
RuntimeSessionError {
code: self.code,
message: format!(
"{} (sid `{}`, generation {})",
runtime_session_terminal_message(self.code, &self.lease_id),
self.sid,
self.generation
),
}
}
}
fn unix_time_millis() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
.unwrap_or(0)
}
fn session_status_cwd_text(session: &RuntimeSession) -> Option<String> {
session
.path_context
.cwd
.as_deref()
.map(render_host_visible_path)
}
fn session_status_workspace_root_text(session: &RuntimeSession) -> Option<String> {
session
.path_context
.workspace_root
.as_deref()
.map(render_host_visible_path)
}
fn session_status_system_lua_lib_text(session: &RuntimeSession) -> Option<String> {
session
.path_context
.system_lua_lib_dir
.as_deref()
.map(render_host_visible_path)
}
fn runtime_session_expiry(ttl_sec: Option<u64>) -> (Option<Instant>, Option<u128>) {
let Some(ttl_sec) = ttl_sec else {
return (None, None);
};
let ttl = Duration::from_secs(ttl_sec);
(
Some(Instant::now() + ttl),
Some(unix_time_millis().saturating_add(ttl.as_millis())),
)
}
fn runtime_session_tombstone_ttl() -> Duration {
Duration::from_secs(3_600)
}
fn runtime_session_terminal_state_from_code(code: &'static str) -> RuntimeSessionTerminalState {
match code {
"lease_closed" => RuntimeSessionTerminalState::Closed,
"lease_expired" => RuntimeSessionTerminalState::Expired,
"lease_replaced" => RuntimeSessionTerminalState::Replaced,
_ => RuntimeSessionTerminalState::Active,
}
}
fn runtime_session_terminal_code_from_state(state: u8) -> Option<&'static str> {
match state {
value if value == RuntimeSessionTerminalState::Closed as u8 => Some("lease_closed"),
value if value == RuntimeSessionTerminalState::Expired as u8 => Some("lease_expired"),
value if value == RuntimeSessionTerminalState::Replaced as u8 => Some("lease_replaced"),
_ => None,
}
}
fn compare_runtime_session_payloads(left: &Value, right: &Value) -> std::cmp::Ordering {
let left_sid = left.get("sid").and_then(Value::as_str).unwrap_or_default();
let right_sid = right.get("sid").and_then(Value::as_str).unwrap_or_default();
left_sid
.cmp(right_sid)
.then_with(|| {
left.get("generation")
.and_then(Value::as_u64)
.unwrap_or(0)
.cmp(&right.get("generation").and_then(Value::as_u64).unwrap_or(0))
})
.then_with(|| {
left.get("lease_id")
.and_then(Value::as_str)
.unwrap_or_default()
.cmp(
right
.get("lease_id")
.and_then(Value::as_str)
.unwrap_or_default(),
)
})
}
fn runtime_session_terminal_message(code: &'static str, lease_id: &str) -> String {
match code {
"lease_closed" => format!("runtime session lease `{lease_id}` is closed"),
"lease_expired" => format!("runtime session lease `{lease_id}` is expired"),
"lease_replaced" => format!("runtime session lease `{lease_id}` was replaced"),
_ => format!("runtime session lease `{lease_id}` is not active"),
}
}
fn runtime_session_error_payload(error: RuntimeSessionError) -> Value {
json!({
"ok": false,
"error_code": error.code,
"message": error.message
})
}
fn normalize_runtime_session_sid(value: &str) -> Result<String, String> {
let sid = value.trim();
if sid.is_empty() {
return Err("runtime session sid must not be empty".to_string());
}
if sid.len() > 128 {
return Err("runtime session sid must not exceed 128 bytes".to_string());
}
if sid.contains('\0') {
return Err("runtime session sid must not contain NUL bytes".to_string());
}
Ok(sid.to_string())
}
fn normalize_optional_runtime_lease_path(
value: Option<&str>,
field_name: &str,
) -> Result<Option<PathBuf>, String> {
let Some(value) = value else {
return Ok(None);
};
let trimmed = value.trim();
if trimmed.is_empty() {
return Ok(None);
}
if trimmed.contains('\0') {
return Err(format!("{field_name} must not contain NUL bytes"));
}
#[cfg(windows)]
if has_invalid_windows_path_syntax(trimmed) {
return Err(format!(
"{field_name} contains unsupported Windows path syntax"
));
}
Ok(Some(PathBuf::from(trimmed)))
}
fn normalize_runtime_lease_path_list(
values: &[String],
field_name: &str,
) -> Result<Vec<PathBuf>, String> {
let mut normalized = Vec::new();
for (index, value) in values.iter().enumerate() {
let item = normalize_optional_runtime_lease_path(
Some(value.as_str()),
&format!("{field_name}[{index}]"),
)?;
if let Some(item) = item {
normalized.push(item);
}
}
Ok(normalized)
}
impl LuaEngine {
fn resolve_system_lua_lib_dir(&self) -> PathBuf {
self.host_options
.system_lua_lib_dir
.clone()
.or_else(|| {
self.runtime_skill_roots
.first()
.map(|root| root.skills_dir.clone())
})
.unwrap_or_else(|| PathBuf::from("skills"))
}
fn resolve_runtime_lease_ttl(
profile: RuntimeLeaseProfile,
requested_ttl_sec: Option<u64>,
) -> Result<Option<u64>, String> {
match profile {
RuntimeLeaseProfile::Public => match requested_ttl_sec {
Some(0) => Err("runtime lease ttl_sec must be greater than 0".to_string()),
Some(value) => Ok(Some(value.clamp(1, 3_600))),
None => Ok(Some(default_runtime_session_ttl_sec())),
},
RuntimeLeaseProfile::SystemLuaLib => match requested_ttl_sec {
None | Some(0) => Ok(None),
Some(value) => Ok(Some(value.clamp(1, 3_600))),
},
}
}
fn resolve_runtime_lease_path_context(
&self,
request: &RuntimeSessionCreateRequest,
profile: RuntimeLeaseProfile,
) -> Result<RuntimeLeasePathContext, String> {
let mut context = RuntimeLeasePathContext {
cwd: normalize_optional_runtime_lease_path(request.cwd.as_deref(), "cwd")?,
workspace_root: normalize_optional_runtime_lease_path(
request.workspace_root.as_deref(),
"workspace_root",
)?,
lua_roots: normalize_runtime_lease_path_list(&request.lua_roots, "lua_roots")?,
c_roots: normalize_runtime_lease_path_list(&request.c_roots, "c_roots")?,
mounts: request.mounts.clone(),
system_lua_lib_dir: None,
};
if !context.mounts.is_object() && !context.mounts.is_null() {
return Err("runtime lease mounts must be one JSON object when present".to_string());
}
if profile == RuntimeLeaseProfile::SystemLuaLib {
let system_dir = self.resolve_system_lua_lib_dir();
std::fs::create_dir_all(&system_dir).map_err(|error| {
format!(
"failed to create system_lua_lib_dir {}: {}",
system_dir.display(),
error
)
})?;
context.system_lua_lib_dir = Some(system_dir.clone());
if context.cwd.is_none() {
context.cwd = Some(system_dir.clone());
}
if !context.lua_roots.iter().any(|root| root == &system_dir) {
context.lua_roots.insert(0, system_dir);
}
}
Ok(context)
}
fn configure_runtime_lease_vm(
lua: &Lua,
path_context: &RuntimeLeasePathContext,
) -> Result<(), String> {
let package: Table = lua.globals().get("package").map_err(|error| {
format!(
"Failed to get Lua package table for runtime lease: {}",
error
)
})?;
let old_cpath: mlua::String = package.get("cpath").map_err(|error| {
format!("Failed to read package.cpath for runtime lease: {}", error)
})?;
let old_path: mlua::String = package
.get("path")
.map_err(|error| format!("Failed to read package.path for runtime lease: {}", error))?;
let mut cpath_prefix = String::new();
for root in &path_context.c_roots {
#[cfg(windows)]
{
cpath_prefix.push_str(&format!(
"{}\\?.dll;{}\\?\\init.dll;",
root.display(),
root.display()
));
}
#[cfg(target_os = "linux")]
{
cpath_prefix.push_str(&format!(
"{}/?.so;{}/?/init.so;",
root.display(),
root.display()
));
}
#[cfg(target_os = "macos")]
{
cpath_prefix.push_str(&format!(
"{}/?.dylib;{}/?/init.dylib;",
root.display(),
root.display()
));
}
}
let mut path_prefix = String::new();
for root in &path_context.lua_roots {
#[cfg(windows)]
{
path_prefix.push_str(&format!(
"{}\\?.lua;{}\\?\\init.lua;",
root.display(),
root.display()
));
}
#[cfg(unix)]
{
path_prefix.push_str(&format!(
"{}/?.lua;{}/?/init.lua;",
root.display(),
root.display()
));
}
}
if !cpath_prefix.is_empty() {
let old_cpath_text = old_cpath
.to_str()
.map(|value| value.to_string())
.unwrap_or_default();
let new_cpath = format!("{}{}", cpath_prefix, old_cpath_text);
package
.set(
"cpath",
lua.create_string(&new_cpath).map_err(|error| {
format!("Failed to build runtime lease cpath string: {}", error)
})?,
)
.map_err(|error| format!("Failed to set runtime lease package.cpath: {}", error))?;
}
if !path_prefix.is_empty() {
let old_path_text = old_path
.to_str()
.map(|value| value.to_string())
.unwrap_or_default();
let new_path = format!("{}{}", path_prefix, old_path_text);
package
.set(
"path",
lua.create_string(&new_path).map_err(|error| {
format!("Failed to build runtime lease path string: {}", error)
})?,
)
.map_err(|error| format!("Failed to set runtime lease package.path: {}", error))?;
}
Ok(())
}
fn eval_lua_value_with_optional_cwd(
lua: &Lua,
wrapper: &str,
cwd: Option<&Path>,
) -> Result<LuaValue, mlua::Error> {
match cwd {
Some(cwd) => {
let _cwd_guard = runlua_cwd_guard()
.lock()
.map_err(|_| mlua::Error::runtime("runtime lease cwd guard lock poisoned"))?;
let original_dir = std::env::current_dir().map_err(|error| {
mlua::Error::runtime(format!("runtime lease cwd: {}", error))
})?;
std::env::set_current_dir(cwd).map_err(|error| {
mlua::Error::runtime(format!(
"runtime lease set cwd {}: {}",
cwd.display(),
error
))
})?;
let execution = lua.load(wrapper).eval::<LuaValue>();
let restore_result = std::env::set_current_dir(&original_dir).map_err(|error| {
mlua::Error::runtime(format!("runtime lease restore cwd: {}", error))
});
match (execution, restore_result) {
(Ok(value), Ok(())) => Ok(value),
(Err(error), Ok(())) => Err(error),
(_, Err(error)) => Err(error),
}
}
None => lua.load(wrapper).eval::<LuaValue>(),
}
}
pub fn create_runtime_lease_json(&self, request_json: &str) -> Result<String, String> {
self.create_runtime_session_with_profile_json(request_json, RuntimeLeaseProfile::Public)
}
fn create_runtime_session_with_profile_json(
&self,
request_json: &str,
profile: RuntimeLeaseProfile,
) -> Result<String, String> {
let mut request: RuntimeSessionCreateRequest = serde_json::from_str(request_json)
.map_err(|error| format!("Invalid runtime session create JSON: {}", error))?;
request.sid = normalize_runtime_session_sid(&request.sid)?;
let ttl_sec = Self::resolve_runtime_lease_ttl(profile, request.ttl_sec)?;
let path_context = self.resolve_runtime_lease_path_context(&request, profile)?;
let vm = Self::create_runlua_vm(
&self.skills,
&self.entry_registry,
self.host_options.clone(),
self.skill_config_store.clone(),
self.runtime_skill_roots.clone(),
self.lancedb_host.clone(),
self.sqlite_host.clone(),
)?;
Self::configure_runtime_lease_vm(&vm.lua, &path_context)?;
Self::install_managed_io_compat_for_runtime(&vm.lua, self.host_options.as_ref())?;
let payload = self
.runtime_sessions
.insert(
profile,
request.sid,
ttl_sec,
request.replace,
path_context,
vm,
)
.unwrap_or_else(runtime_session_error_payload);
serde_json::to_string(&payload)
.map_err(|error| format!("Runtime session create JSON encode failed: {}", error))
}
pub fn eval_runtime_lease_json(&self, request_json: &str) -> Result<String, String> {
self.eval_runtime_session_with_profile_json(request_json, RuntimeLeaseProfile::Public)
}
fn eval_runtime_session_with_profile_json(
&self,
request_json: &str,
expected_profile: RuntimeLeaseProfile,
) -> Result<String, String> {
let mut request: RuntimeSessionEvalRequest = serde_json::from_str(request_json)
.map_err(|error| format!("Invalid runtime session eval JSON: {}", error))?;
if let Some(sid) = request.sid.as_mut() {
*sid = normalize_runtime_session_sid(sid)?;
}
if request.timeout_ms == 0 {
return Err("runtime session eval timeout_ms must be greater than 0".to_string());
}
let session = match self.runtime_sessions.get(
&request.lease_id,
request.sid.as_deref(),
request.generation,
Some(expected_profile),
) {
Ok(session) => session,
Err(error) => {
return serde_json::to_string(&runtime_session_error_payload(error)).map_err(
|encode_error| {
format!("Runtime session eval JSON encode failed: {}", encode_error)
},
);
}
};
let mut session = match session.try_lock() {
Ok(session) => session,
Err(_) => {
let payload = runtime_session_error_payload(RuntimeSessionError {
code: "lease_busy",
message: format!("runtime session lease `{}` is busy", request.lease_id),
});
return serde_json::to_string(&payload).map_err(|error| {
format!("Runtime session eval JSON encode failed: {}", error)
});
}
};
let (payload, refreshed_snapshot) = match Self::ensure_runtime_session_active(&mut session)
{
Ok(()) => match self.eval_runtime_session_locked(&mut session, &request) {
Ok(result) => {
session.refresh();
(
json!({
"ok": true,
"sid": session.sid.clone(),
"lease_id": session.lease_id.clone(),
"generation": session.generation,
"profile": session.profile.as_str(),
"lifetime": if session.ttl_sec.is_some() { "finite" } else { "infinite" },
"cwd": session_status_cwd_text(&session),
"system_lua_lib": session_status_system_lua_lib_text(&session),
"expires_at_unix_ms": session.expires_at_unix_ms,
"result": result
}),
Some(session.status_payload()),
)
}
Err(message) => (
runtime_session_error_payload(RuntimeSessionError {
code: "eval_failed",
message,
}),
Some(session.status_payload()),
),
},
Err(error) => (runtime_session_error_payload(error), None),
};
drop(session);
if let Some(snapshot) = refreshed_snapshot {
let _ = self
.runtime_sessions
.update_active_snapshot(&request.lease_id, snapshot);
}
serde_json::to_string(&payload)
.map_err(|error| format!("Runtime session eval JSON encode failed: {}", error))
}
pub fn runtime_lease_status_json(&self, request_json: &str) -> Result<String, String> {
self.runtime_session_status_with_profile_json(request_json, RuntimeLeaseProfile::Public)
}
fn runtime_session_status_with_profile_json(
&self,
request_json: &str,
expected_profile: RuntimeLeaseProfile,
) -> Result<String, String> {
let mut request: RuntimeSessionLeaseRequest = serde_json::from_str(request_json)
.map_err(|error| format!("Invalid runtime session status JSON: {}", error))?;
if let Some(sid) = request.sid.as_mut() {
*sid = normalize_runtime_session_sid(sid)?;
}
let payload = self
.runtime_sessions
.status(
&request.lease_id,
request.sid.as_deref(),
request.generation,
Some(expected_profile),
)
.unwrap_or_else(runtime_session_error_payload);
serde_json::to_string(&payload)
.map_err(|error| format!("Runtime session status JSON encode failed: {}", error))
}
pub fn list_runtime_leases_json(&self, request_json: &str) -> Result<String, String> {
self.list_runtime_sessions_with_profile_json(request_json, RuntimeLeaseProfile::Public)
}
fn list_runtime_sessions_with_profile_json(
&self,
request_json: &str,
expected_profile: RuntimeLeaseProfile,
) -> Result<String, String> {
let mut request: RuntimeSessionListRequest = serde_json::from_str(request_json)
.map_err(|error| format!("Invalid runtime session list JSON: {}", error))?;
if let Some(sid) = request.sid.as_mut() {
*sid = normalize_runtime_session_sid(sid)?;
}
let payload = self
.runtime_sessions
.list(request.sid.as_deref(), Some(expected_profile))
.unwrap_or_else(runtime_session_error_payload);
serde_json::to_string(&payload)
.map_err(|error| format!("Runtime session list JSON encode failed: {}", error))
}
pub fn close_runtime_lease_json(&self, request_json: &str) -> Result<String, String> {
self.close_runtime_session_with_profile_json(request_json, RuntimeLeaseProfile::Public)
}
fn close_runtime_session_with_profile_json(
&self,
request_json: &str,
expected_profile: RuntimeLeaseProfile,
) -> Result<String, String> {
let mut request: RuntimeSessionLeaseRequest = serde_json::from_str(request_json)
.map_err(|error| format!("Invalid runtime session close JSON: {}", error))?;
if let Some(sid) = request.sid.as_mut() {
*sid = normalize_runtime_session_sid(sid)?;
}
let payload = self
.runtime_sessions
.close(
&request.lease_id,
request.sid.as_deref(),
request.generation,
Some(expected_profile),
)
.unwrap_or_else(runtime_session_error_payload);
serde_json::to_string(&payload)
.map_err(|error| format!("Runtime session close JSON encode failed: {}", error))
}
pub fn create_system_runtime_lease_json(&self, request_json: &str) -> Result<String, String> {
self.create_runtime_session_with_profile_json(
request_json,
RuntimeLeaseProfile::SystemLuaLib,
)
}
pub fn eval_system_runtime_lease_json(&self, request_json: &str) -> Result<String, String> {
self.eval_runtime_session_with_profile_json(request_json, RuntimeLeaseProfile::SystemLuaLib)
}
pub fn system_runtime_lease_status_json(&self, request_json: &str) -> Result<String, String> {
self.runtime_session_status_with_profile_json(
request_json,
RuntimeLeaseProfile::SystemLuaLib,
)
}
pub fn list_system_runtime_leases_json(&self, request_json: &str) -> Result<String, String> {
self.list_runtime_sessions_with_profile_json(
request_json,
RuntimeLeaseProfile::SystemLuaLib,
)
}
pub fn close_system_runtime_lease_json(&self, request_json: &str) -> Result<String, String> {
self.close_runtime_session_with_profile_json(
request_json,
RuntimeLeaseProfile::SystemLuaLib,
)
}
fn install_managed_io_compat_for_runtime(
lua: &Lua,
host_options: &LuaRuntimeHostOptions,
) -> Result<(), String> {
if !host_options.capabilities.enable_managed_io_compat {
return Ok(());
}
let default_encoding = resolve_host_default_text_encoding(host_options)?;
let vulcan = get_vulcan_table(lua)?;
let vulcan_io = vulcan
.get::<Table>("io")
.map_err(|error| format!("Failed to get vulcan.io: {}", error))?;
install_managed_io_compat(lua, &vulcan_io, default_encoding).map_err(|error| {
format!(
"Failed to install managed io compatibility for runtime session: {}",
error
)
})
}
pub(super) fn ensure_runtime_session_active(
session: &mut RuntimeSession,
) -> Result<(), RuntimeSessionError> {
if let Some(error) = session.inactive_error() {
return Err(error);
}
if session.ttl_sec.is_some() {
session.refresh();
}
Ok(())
}
fn eval_runtime_session_locked(
&self,
session: &mut RuntimeSession,
request: &RuntimeSessionEvalRequest,
) -> Result<Value, String> {
reset_pooled_vm_request_scope(&session.vm.lua, self.host_options.as_ref())?;
let invocation_context = request.to_invocation_context();
Self::populate_vulcan_request_context(&session.vm.lua, Some(&invocation_context))?;
populate_vulcan_internal_execution_context(
&session.vm.lua,
&VulcanInternalExecutionContext {
tool_name: None,
skill_name: None,
entry_name: None,
root_name: None,
luaexec_active: true,
luaexec_caller_tool_name: None,
},
)?;
populate_vulcan_file_context(&session.vm.lua, None, None)?;
populate_vulcan_dependency_context(
&session.vm.lua,
self.host_options.as_ref(),
None,
None,
)?;
Self::populate_vulcan_lancedb_context(&session.vm.lua, None, None)?;
Self::populate_vulcan_sqlite_context(&session.vm.lua, None, None)?;
let args_table = json_to_lua_table(&session.vm.lua, &request.args)?;
session
.vm
.lua
.globals()
.set("__runlua_args", args_table)
.map_err(|error| format!("Failed to set runtime session args: {}", error))?;
let wrapper = format!(
"return (function()\n local args = __runlua_args\n {}\nend)()",
request.code
);
Self::install_runlua_timeout_guard(&session.vm.lua, request.timeout_ms)
.map_err(|error| error.to_string())?;
let eval_result = Self::eval_lua_value_with_optional_cwd(
&session.vm.lua,
&wrapper,
session.path_context.cwd.as_deref(),
);
Self::remove_runlua_timeout_guard(&session.vm.lua);
let result = eval_result.map_err(|error| {
let msg = format!("Runtime session eval error: {}", error);
log_error(format!("[LuaSkill:error] {}", msg));
msg
})?;
let json_result = lua_value_to_json(&result)?;
clear_runlua_args_global(&session.vm.lua)?;
Ok(json_result)
}
}