Files
LLM-Connect/code_executor.lua
2026-03-04 23:20:04 +01:00

299 lines
10 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 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