feat(mcp): inject require into exec-lua environment

- build real require/package via rom cc.require module
- expose to exec-lua and run-file executed code
- add stubbable requireFactory/requireDir deps for tests
- bump trapos 0.11.2 and trapos-sandbox 0.3.2
This commit is contained in:
Guillaume ARM 2026-06-16 02:01:18 +02:00
parent a4854368d7
commit fb7ee48527
6 changed files with 120 additions and 11 deletions

View File

@ -37,7 +37,33 @@ local function serializeReturn(value)
return { type = valueType, repr = tostring(value) };
end
local function createExecEnv(output)
-- Builds a CraftOS-style require/package pair the same way /rom/programs/shell.lua does
-- (dofile of cc.require, then .make(env, dir)). Returned factory caches the make function so
-- the rom module is only loaded once. Stays synchronous (require never yields), so it is safe
-- inside the synchronous sandbox handler -- unlike spawning a shell, which yields and deadlocks.
local function defaultRequireFactory()
local make;
local loaded = false;
return function(env, dir)
if not loaded then
loaded = true;
local ok, mod = pcall(dofile, '/rom/modules/main/cc/require.lua');
if ok and type(mod) == 'table' then
make = mod.make;
end
end
if type(make) ~= 'function' then
return nil, nil;
end
local madeOk, req, pkg = pcall(make, env, dir or '');
if not madeOk then
return nil, nil;
end
return req, pkg;
end
end
local function createExecEnv(output, requireFactory, requireDir)
local env = setmetatable({}, { __index = _G });
env.write = function(value)
@ -55,13 +81,21 @@ local function createExecEnv(output)
appendOutput(output, '\n');
end;
if requireFactory then
local req, pkg = requireFactory(env, requireDir);
if req then
env.require = req;
env.package = pkg;
end
end
return env;
end
local function executeSource(code, chunkName)
local function executeSource(code, chunkName, deps)
local output = {};
local fn, loadErr = load(code, chunkName, 't', createExecEnv(output));
local fn, loadErr = load(code, chunkName, 't', createExecEnv(output, deps.requireFactory, deps.requireDir));
if not fn then
return { ok = false, error = tostring(loadErr), output = table.concat(output) };
end
@ -78,7 +112,20 @@ local function executeSource(code, chunkName)
return { ok = true, returns = returns, output = table.concat(output) };
end
local function createMcpComputer()
local function createMcpComputer(deps)
deps = deps or {};
-- requireFactory: function(env, dir) -> require, package. Defaults to the rom-backed factory;
-- pass `false` to disable require injection, or a function to stub it in tests.
local requireFactory = deps.requireFactory;
if requireFactory == nil then
requireFactory = defaultRequireFactory();
end
local resolved = {
requireFactory = requireFactory or nil,
requireDir = deps.requireDir or '',
};
local api = {};
api.formatLabel = formatLabel;
@ -94,7 +141,7 @@ local function createMcpComputer()
return { ok = false, error = 'code must be a non-empty string', output = '' };
end
return executeSource(code, 'mcp-exec');
return executeSource(code, 'mcp-exec', resolved);
end
function api.executeFile(path, fsLike)
@ -122,7 +169,7 @@ local function createMcpComputer()
return { ok = false, error = tostring(closeErr), output = '' };
end
return executeSource(contentOrErr, '@' .. path);
return executeSource(contentOrErr, '@' .. path, resolved);
end
function api.writeFile(path, content, fsLike)

View File

@ -1,6 +1,6 @@
{
"name": "TrapOS",
"version": "0.11.1",
"version": "0.11.2",
"branch": "master",
"packages": [
"trapos"

View File

@ -6,8 +6,8 @@
"trapos-net": "0.3.0",
"trapos-ui": "0.2.2",
"trapos-ai": "0.7.0",
"trapos-sandbox": "0.3.1",
"trapos-sandbox": "0.3.2",
"trapos-sandbox-legacy": "0.3.0",
"trapos": "0.11.1"
"trapos": "0.11.2"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "trapos-sandbox",
"version": "0.3.1",
"version": "0.3.2",
"description": "TrapOS sandbox gateway client: WS daemon (servers/sandbox) + sandbox command + MCP exec/write server",
"dependencies": ["trapos-core"],
"files": [

View File

@ -1,6 +1,6 @@
{
"name": "trapos",
"version": "0.11.1",
"version": "0.11.2",
"description": "TrapOS full install meta-package",
"dependencies": [
"trapos-boot",

View File

@ -122,6 +122,68 @@ testlib.test('executeLua leaves direct terminal writes out of captured output',
testlib.assertEquals(result.returns[1].value, 'done');
end);
testlib.test('executeLua injects require and package into the environment', function()
local seenEnv, seenDir;
local mcpComputer = createMcpComputer({
requireFactory = function(env, dir)
seenEnv, seenDir = env, dir;
return function() return 'REQUIRED'; end, { loaded = {} };
end,
});
local result = mcpComputer.executeLua("return require('whatever'), type(package)");
testlib.assertEquals(result.ok, true);
testlib.assertEquals(result.returns[1].type, 'string');
testlib.assertEquals(result.returns[1].value, 'REQUIRED');
testlib.assertEquals(result.returns[2].type, 'string');
testlib.assertEquals(result.returns[2].value, 'table');
testlib.assertEquals(type(seenEnv), 'table');
testlib.assertEquals(seenDir, '');
end);
testlib.test('executeLua passes the configured requireDir to the factory', function()
local seenDir;
local mcpComputer = createMcpComputer({
requireDir = '/apis',
requireFactory = function(_, dir)
seenDir = dir;
return function() end, {};
end,
});
mcpComputer.executeLua('return 1');
testlib.assertEquals(seenDir, '/apis');
end);
testlib.test('executeLua leaves require unset when injection is disabled', function()
local mcpComputer = createMcpComputer({ requireFactory = false });
local result = mcpComputer.executeLua('return require == nil, package == nil');
testlib.assertEquals(result.ok, true);
testlib.assertEquals(result.returns[1].type, 'boolean');
testlib.assertEquals(result.returns[1].value, true);
testlib.assertEquals(result.returns[2].type, 'boolean');
testlib.assertEquals(result.returns[2].value, true);
end);
testlib.test('executeFile injects require into the file environment', function()
local mcpComputer = createMcpComputer({
requireFactory = function() return function() return 'FROM_FILE'; end, {}; end,
});
local fsLike = fsWithFiles({
['/needs-require.lua'] = "return require('x')",
});
local result = mcpComputer.executeFile('/needs-require.lua', fsLike);
testlib.assertEquals(result.ok, true);
testlib.assertEquals(result.returns[1].type, 'string');
testlib.assertEquals(result.returns[1].value, 'FROM_FILE');
end);
testlib.test('executeFile captures output and return values from file', function()
local mcpComputer = createMcpComputer();
local fsLike = fsWithFiles({