Import 0.9.0 development baseline
This commit is contained in:
298
code_executor.lua
Normal file
298
code_executor.lua
Normal file
@@ -0,0 +1,298 @@
|
||||
-- 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 ist Superrolle: impliziert llm_dev und alle anderen
|
||||
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
|
||||
|
||||
-- Prüfen ob der Player überhaupt Ausführungsrechte hat
|
||||
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
|
||||
Reference in New Issue
Block a user