-- code_executor.lua -- Secure Lua code execution for LLM-Connect / Smart Lua IDE -- Privileges: -- llm_dev → Sandbox + Whitelist, no persistent registrations -- llm_root → Unrestricted execution + persistent registrations possible local core = core local M = {} M.execution_history = {} -- per player: {timestamp, code_snippet, success, output/error} local STARTUP_FILE = core.get_worldpath() .. DIR_DELIM .. "llm_startup.lua" -- ============================================================= -- Helper functions -- ============================================================= local function player_has_priv(name, priv) local privs = core.get_player_privs(name) or {} return privs[priv] == true end -- llm_root is a super-role: implies llm_dev and all others local function has_llm_priv(name, priv) if player_has_priv(name, "llm_root") then return true end return player_has_priv(name, priv) end local function is_llm_root(name) return player_has_priv(name, "llm_root") end -- ============================================================= -- Sandbox environment (for normal llm_dev / llm users) -- ============================================================= local function create_sandbox_env(player_name) local safe_core = { -- Logging & Chat log = core.log, chat_send_player = core.chat_send_player, -- Secure read access get_node = core.get_node, get_node_or_nil = core.get_node_or_nil, find_node_near = core.find_node_near, find_nodes_in_area = core.find_nodes_in_area, get_meta = core.get_meta, get_player_by_name = core.get_player_by_name, get_connected_players = core.get_connected_players, } -- Block registration functions (require restart) local function blocked_registration(name) return function(...) core.log("warning", ("[code_executor] Blocked registration call: %s by %s"):format(name, player_name)) core.chat_send_player(player_name, "Registrations are forbidden in sandbox mode.\nOnly llm_root may execute these persistently.") return nil end end safe_core.register_node = blocked_registration("register_node") safe_core.register_craftitem = blocked_registration("register_craftitem") safe_core.register_tool = blocked_registration("register_tool") safe_core.register_craft = blocked_registration("register_craft") safe_core.register_entity = blocked_registration("register_entity") -- Allowed dynamic registrations (very restricted) safe_core.register_chatcommand = core.register_chatcommand safe_core.register_on_chat_message = core.register_on_chat_message -- Safe standard libraries (without dangerous functions) local env = { -- Lua basics assert = assert, error = error, pairs = pairs, ipairs = ipairs, next = next, select = select, type = type, tostring = tostring, tonumber = tonumber, unpack = table.unpack or unpack, -- Safe string/table/math functions string = { byte=string.byte, char=string.char, find=string.find, format=string.format, gmatch=string.gmatch, gsub=string.gsub, len=string.len, lower=string.lower, match=string.match, rep=string.rep, reverse=string.reverse, sub=string.sub, upper=string.upper }, table = { concat=table.concat, insert=table.insert, remove=table.remove, sort=table.sort }, math = math, -- Minetest-safe API core = safe_core, -- Redirect print print = function(...) end, -- will be overwritten later } -- Output buffer with limit local output_buffer = {} local output_size = 0 local MAX_OUTPUT = 100000 -- ~100 KB env.print = function(...) local parts = {} for i = 1, select("#", ...) do parts[i] = tostring(select(i, ...)) end local line = table.concat(parts, "\t") if output_size + #line > MAX_OUTPUT then table.insert(output_buffer, "\n[OUTPUT TRUNCATED – 100 KB limit reached]") return end table.insert(output_buffer, line) output_size = output_size + #line end return env, output_buffer end -- ============================================================= -- Append persistent startup code (llm_root only) -- ============================================================= local function append_to_startup(code, player_name) local f, err = io.open(STARTUP_FILE, "a") if not f then core.log("error", ("[code_executor] Cannot open startup file: %s"):format(tostring(err))) return false, err end f:write(("\n-- Added by %s at %s\n"):format(player_name, os.date("%Y-%m-%d %H:%M:%S"))) f:write(code) f:write("\n\n") f:close() core.log("action", ("[code_executor] Appended code to %s by %s"):format(STARTUP_FILE, player_name)) return true end -- ============================================================= -- Main execution function -- ============================================================= function M.execute(player_name, code, options) options = options or {} local result = { success = false } if type(code) ~= "string" or code:trim() == "" then result.error = "No or empty code provided" return result end local is_root = is_llm_root(player_name) local use_sandbox = options.sandbox ~= false local allow_persist = options.allow_persist or is_root -- Check whether the player has execution rights at all if not has_llm_priv(player_name, "llm_dev") then result.error = "Missing privilege: llm_dev (or llm_root)" return result end -- ============================================= -- 1. Compile -- ============================================= local func, compile_err = loadstring(code, "=(llm_ide)") if not func then result.error = "Compile error: " .. tostring(compile_err) core.log("warning", ("[code_executor] Compile failed for %s: %s"):format(player_name, result.error)) return result end -- ============================================= -- 2. Prepare environment & print redirection -- ============================================= local output_buffer = {} local env if use_sandbox then env, output_buffer = create_sandbox_env(player_name) setfenv(func, env) -- Lua 5.1 compatibility (Luanti mostly uses LuaJIT) else -- Unrestricted mode → Careful! if not is_root then result.error = "Unrestricted execution only allowed for llm_root" return result end -- Redirect print (without overwriting _G) local old_print = print print = function(...) local parts = {} for i = 1, select("#", ...) do parts[#parts+1] = tostring(select(i, ...)) end local line = table.concat(parts, "\t") table.insert(output_buffer, line) end end -- ============================================= -- 3. Execute (with instruction limit) -- ============================================= local ok, exec_res = pcall(function() -- Instruction limit could be added here later (currently dummy) return func() end) -- Reset print (if unrestricted) if not use_sandbox then print = old_print end -- ============================================= -- 4. Process result -- ============================================= result.output = table.concat(output_buffer, "\n") if ok then result.success = true result.return_value = exec_res core.log("action", ("[code_executor] Success by %s (sandbox=%s)"):format(player_name, tostring(use_sandbox))) else result.error = "Runtime error: " .. tostring(exec_res) core.log("warning", ("[code_executor] Execution failed for %s: %s"):format(player_name, result.error)) end -- ============================================= -- 5. Check for registrations → Persistence? -- ============================================= local has_registration = code:match("register_node%s*%(") or code:match("register_tool%s*%(") or code:match("register_craftitem%s*%(") or code:match("register_entity%s*%(") or code:match("register_craft%s*%(") if has_registration then if allow_persist and is_root then local saved, save_err = append_to_startup(code, player_name) if saved then local msg = "Code with registrations saved to llm_startup.lua.\nWill be active after server restart." core.chat_send_player(player_name, msg) result.output = (result.output or "") .. "\n\n" .. msg result.persisted = true else result.error = (result.error or "") .. "\nPersistence failed: " .. tostring(save_err) end else local msg = "Code contains registrations (node/tool/...). \nOnly llm_root can execute these persistently (restart required)." core.chat_send_player(player_name, msg) result.error = (result.error or "") .. "\n" .. msg result.success = false -- even if execution was ok end end -- Save history M.execution_history[player_name] = M.execution_history[player_name] or {} table.insert(M.execution_history[player_name], { timestamp = os.time(), code = code:sub(1, 200) .. (code:len() > 200 and "..." or ""), success = result.success, output = result.output, error = result.error, }) return result end -- ============================================================= -- History functions -- ============================================================= function M.get_history(player_name, limit) limit = limit or 10 local hist = M.execution_history[player_name] or {} local res = {} local start = math.max(1, #hist - limit + 1) for i = start, #hist do res[#res+1] = hist[i] end return res end function M.clear_history(player_name) M.execution_history[player_name] = nil end -- Cleanup core.register_on_leaveplayer(function(player) local name = player:get_player_name() M.execution_history[name] = nil end) return M