From 0e2956977070c1169919db59972d209bba5e3144 Mon Sep 17 00:00:00 2001 From: Regela Date: Sat, 28 Mar 2026 09:29:57 +0300 Subject: [PATCH] Add module sandbox isolation and dynamic imgui window registry - Sandbox each module via setfenv(): writes go to per-module environment, reads proxy to _G. Modules can no longer accidentally overwrite each other's globals or framework internals. Sandbox is GC'd on unload. - Add dynamic window registry (fw.register_window/toggle_window/ show_window/hide_window) so modules can create imgui windows without editing the framework. Replace hardcoded BTC OnFrame with generic handler. - Make admin_visible local (was global) - Fix console serialize: add local, move above M.init() for correct scoping - Expand .gitignore: target/, build/, *.so, *.log, rgl_data.db, env.fish Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 6 ++ docs/TASKS.md | 3 - http_framework/modules/console/init.lua | 96 ++++++++++++------------ http_framework/rgl_framework.lua | 98 ++++++++++++++++++------- 4 files changed, 127 insertions(+), 76 deletions(-) diff --git a/.gitignore b/.gitignore index dc063ad..7caa699 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ more_modules +env.fish +rust_core/target/ +build/ +*.so +*.log +rgl_data.db diff --git a/docs/TASKS.md b/docs/TASKS.md index 3ddebf2..96792aa 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -30,9 +30,6 @@ No Makefile or build automation exists. Need a script for: ## Medium -### Fix BTC module global state -`more_modules/btc/init.lua` uses `btc_visible = false` as a global variable (for mimgui OnFrame). Should be moved to a proper module state mechanism to avoid global namespace pollution. - ### Implement WebSocket backpressure `bridge.rs` broadcast channel has capacity 256. Events are silently dropped when full. Should either increase capacity, add warning logging, or implement backpressure. diff --git a/http_framework/modules/console/init.lua b/http_framework/modules/console/init.lua index 207df92..cf673c5 100644 --- a/http_framework/modules/console/init.lua +++ b/http_framework/modules/console/init.lua @@ -1,55 +1,8 @@ -- Lua Console module — execute Lua code in the game's main thread local M = {} -function M.init(fw) - local static_dir = fw.modules_dir .. "/console/static" - fw.register_module("console", static_dir) - - fw.on_api("console", "exec", function(body) - local ok, data = pcall(fw.json_decode, body) - if not ok or not data or not data.code then - return '{"ok":false,"error":"invalid body"}' - end - - local code = data.code - if #code == 0 then - return '{"ok":false,"error":"empty code"}' - end - - -- Try as expression first (return value), then as statement - local fn, err = loadstring("return " .. code) - if not fn then - fn, err = loadstring(code) - end - if not fn then - return fw.json_encode({ok = false, error = "Compile: " .. tostring(err)}) - end - - local results = {pcall(fn)} - local success = table.remove(results, 1) - - if not success then - return fw.json_encode({ok = false, error = tostring(results[1])}) - end - - -- Format results - local parts = {} - for i, v in ipairs(results) do - parts[i] = serialize(v, 0) - end - - local output = table.concat(parts, ", ") - if #parts == 0 then output = "nil" end - - fw.log("INFO", "CONSOLE", "Exec: " .. code:sub(1, 80) .. " => " .. output:sub(1, 80)) - return fw.json_encode({ok = true, result = output, count = #parts}) - end) - - fw.log("INFO", "CONSOLE", "Module loaded") -end - -- Serialize a Lua value into a readable string -function serialize(val, depth) +local function serialize(val, depth) depth = depth or 0 if depth > 4 then return "..." end @@ -96,4 +49,51 @@ function serialize(val, depth) end end +function M.init(fw) + local static_dir = fw.modules_dir .. "/console/static" + fw.register_module("console", static_dir) + + fw.on_api("console", "exec", function(body) + local ok, data = pcall(fw.json_decode, body) + if not ok or not data or not data.code then + return '{"ok":false,"error":"invalid body"}' + end + + local code = data.code + if #code == 0 then + return '{"ok":false,"error":"empty code"}' + end + + -- Try as expression first (return value), then as statement + local fn, err = loadstring("return " .. code) + if not fn then + fn, err = loadstring(code) + end + if not fn then + return fw.json_encode({ok = false, error = "Compile: " .. tostring(err)}) + end + + local results = {pcall(fn)} + local success = table.remove(results, 1) + + if not success then + return fw.json_encode({ok = false, error = tostring(results[1])}) + end + + -- Format results + local parts = {} + for i, v in ipairs(results) do + parts[i] = serialize(v, 0) + end + + local output = table.concat(parts, ", ") + if #parts == 0 then output = "nil" end + + fw.log("INFO", "CONSOLE", "Exec: " .. code:sub(1, 80) .. " => " .. output:sub(1, 80)) + return fw.json_encode({ok = true, result = output, count = #parts}) + end) + + fw.log("INFO", "CONSOLE", "Module loaded") +end + return M diff --git a/http_framework/rgl_framework.lua b/http_framework/rgl_framework.lua index da68fcf..030185e 100644 --- a/http_framework/rgl_framework.lua +++ b/http_framework/rgl_framework.lua @@ -35,11 +35,12 @@ local module_states = {} -- persistent state per module for render() local command_handlers = {} local framework = {} local _current_module = nil -admin_visible = false +local admin_visible = false local recent_logs = {} local MAX_LOGS = 100 local notifications = {} -- {text, level, time, start} local module_errors = {} -- ["module_name"] = "last error" +local module_windows = {} -- {name = {visible, title, width, height}} local function log(level, tag, ...) if not rust then return print("[" .. level .. "][" .. tag .. "]", ...) end @@ -503,6 +504,33 @@ function setup_framework() rust.rgl_register_command(name, owner) log("INFO", "CMD", "Registered /" .. name .. " (" .. owner .. ")") end + + -- Module imgui window registry + framework.register_window = function(opts) + opts = opts or {} + local mod_name = _current_module or "__unknown" + module_windows[mod_name] = { + visible = false, + title = opts.title or mod_name, + width = opts.width or 450, + height = opts.height or 400, + } + end + + framework.toggle_window = function(name) + local w = module_windows[name or _current_module] + if w then w.visible = not w.visible end + end + + framework.show_window = function(name) + local w = module_windows[name or _current_module] + if w then w.visible = true end + end + + framework.hide_window = function(name) + local w = module_windows[name or _current_module] + if w then w.visible = false end + end end ---------------------------------------------------------------- @@ -514,8 +542,15 @@ function load_single_module(name) local f = io.open(path); if not f then return false, "not found" end; f:close() _current_module = name - local ok, mod = pcall(dofile, path) - if not ok then _current_module = nil; return false, "load: " .. tostring(mod) end + local chunk, cerr = loadfile(path) + if not chunk then _current_module = nil; return false, "load: " .. tostring(cerr) end + + -- Sandbox: module gets its own environment, reads fall through to _G + local sandbox = setmetatable({}, {__index = _G}) + setfenv(chunk, sandbox) + + local ok, mod = pcall(chunk) + if not ok then _current_module = nil; return false, "exec: " .. tostring(mod) end if not mod or not mod.init then _current_module = nil; return false, "no init()" end -- Create per-module framework wrapper with bound db prefix @@ -565,7 +600,7 @@ function load_single_module(name) _current_module = nil if not iok then return false, "init: " .. tostring(ierr) end - loaded_modules[name] = {mod = mod, status = "loaded"} + loaded_modules[name] = {mod = mod, status = "loaded", sandbox = sandbox} module_states[name] = module_states[name] or {} register_module_render(name) log("INFO", "MODS", "Loaded: " .. name) @@ -585,6 +620,7 @@ function unload_single_module(name) event_interceptors[ev] = new end rust.rgl_register_module(name, "") + module_windows[name] = nil loaded_modules[name] = nil log("INFO", "MODS", "Unloaded: " .. name) return true @@ -964,32 +1000,44 @@ if imgui_loaded and imgui then end ) - -- BTC module window (and any other module with render()) - local btc_window = imgui.new.bool() + -- Dynamic module windows (registered via fw.register_window) + local mod_window_bool = imgui.new.bool() imgui.OnFrame( - function() return btc_visible end, function() - btc_window[0] = true - imgui.SetNextWindowSize(imgui.ImVec2(450 * dpi, 400 * dpi), imgui.Cond.FirstUseEver) - imgui.Begin("BTC Miner", btc_window, imgui.WindowFlags.NoCollapse) - - local mod = loaded_modules and loaded_modules["btc"] - if mod and mod.mod and mod.mod.render then - local ui = create_ui_imgui() - if ui then - local rok, rerr = pcall(mod.mod.render, ui, module_states["btc"] or {}) - if not rok then - imgui.TextColored(imgui.ImVec4(1, 0.3, 0.3, 1), "Render error:") - imgui.TextWrapped(tostring(rerr)) + for _, w in pairs(module_windows) do + if w.visible then return true end + end + return false + end, + function() + for name, w in pairs(module_windows) do + if w.visible then + mod_window_bool[0] = true + imgui.SetNextWindowSize( + imgui.ImVec2(w.width * dpi, w.height * dpi), + imgui.Cond.FirstUseEver + ) + imgui.Begin(w.title, mod_window_bool, imgui.WindowFlags.NoCollapse) + + local entry = loaded_modules and loaded_modules[name] + if entry and entry.mod and entry.mod.render then + local ui = create_ui_imgui() + if ui then + local rok, rerr = pcall(entry.mod.render, ui, module_states[name] or {}) + if not rok then + imgui.TextColored(imgui.ImVec4(1, 0.3, 0.3, 1), "Render error:") + imgui.TextWrapped(tostring(rerr)) + end + imgui.EndTabBar() + end + else + imgui.Text(name .. " module not loaded") end - imgui.EndTabBar() + + imgui.End() + if not mod_window_bool[0] then w.visible = false end end - else - imgui.Text("BTC module not loaded") end - - imgui.End() - if not btc_window[0] then btc_visible = false end end )