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) <noreply@anthropic.com>
main
Regela 1 day ago
parent 50ad1e805c
commit 0e29569770

6
.gitignore vendored

@ -1 +1,7 @@
more_modules
env.fish
rust_core/target/
build/
*.so
*.log
rgl_data.db

@ -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.

@ -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

@ -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
)

Loading…
Cancel
Save