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:
parent
a4854368d7
commit
fb7ee48527
@ -37,7 +37,33 @@ local function serializeReturn(value)
|
|||||||
return { type = valueType, repr = tostring(value) };
|
return { type = valueType, repr = tostring(value) };
|
||||||
end
|
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 });
|
local env = setmetatable({}, { __index = _G });
|
||||||
|
|
||||||
env.write = function(value)
|
env.write = function(value)
|
||||||
@ -55,13 +81,21 @@ local function createExecEnv(output)
|
|||||||
appendOutput(output, '\n');
|
appendOutput(output, '\n');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
if requireFactory then
|
||||||
|
local req, pkg = requireFactory(env, requireDir);
|
||||||
|
if req then
|
||||||
|
env.require = req;
|
||||||
|
env.package = pkg;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return env;
|
return env;
|
||||||
end
|
end
|
||||||
|
|
||||||
local function executeSource(code, chunkName)
|
local function executeSource(code, chunkName, deps)
|
||||||
local output = {};
|
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
|
if not fn then
|
||||||
return { ok = false, error = tostring(loadErr), output = table.concat(output) };
|
return { ok = false, error = tostring(loadErr), output = table.concat(output) };
|
||||||
end
|
end
|
||||||
@ -78,7 +112,20 @@ local function executeSource(code, chunkName)
|
|||||||
return { ok = true, returns = returns, output = table.concat(output) };
|
return { ok = true, returns = returns, output = table.concat(output) };
|
||||||
end
|
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 = {};
|
local api = {};
|
||||||
|
|
||||||
api.formatLabel = formatLabel;
|
api.formatLabel = formatLabel;
|
||||||
@ -94,7 +141,7 @@ local function createMcpComputer()
|
|||||||
return { ok = false, error = 'code must be a non-empty string', output = '' };
|
return { ok = false, error = 'code must be a non-empty string', output = '' };
|
||||||
end
|
end
|
||||||
|
|
||||||
return executeSource(code, 'mcp-exec');
|
return executeSource(code, 'mcp-exec', resolved);
|
||||||
end
|
end
|
||||||
|
|
||||||
function api.executeFile(path, fsLike)
|
function api.executeFile(path, fsLike)
|
||||||
@ -122,7 +169,7 @@ local function createMcpComputer()
|
|||||||
return { ok = false, error = tostring(closeErr), output = '' };
|
return { ok = false, error = tostring(closeErr), output = '' };
|
||||||
end
|
end
|
||||||
|
|
||||||
return executeSource(contentOrErr, '@' .. path);
|
return executeSource(contentOrErr, '@' .. path, resolved);
|
||||||
end
|
end
|
||||||
|
|
||||||
function api.writeFile(path, content, fsLike)
|
function api.writeFile(path, content, fsLike)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TrapOS",
|
"name": "TrapOS",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"branch": "master",
|
"branch": "master",
|
||||||
"packages": [
|
"packages": [
|
||||||
"trapos"
|
"trapos"
|
||||||
|
|||||||
@ -6,8 +6,8 @@
|
|||||||
"trapos-net": "0.3.0",
|
"trapos-net": "0.3.0",
|
||||||
"trapos-ui": "0.2.2",
|
"trapos-ui": "0.2.2",
|
||||||
"trapos-ai": "0.7.0",
|
"trapos-ai": "0.7.0",
|
||||||
"trapos-sandbox": "0.3.1",
|
"trapos-sandbox": "0.3.2",
|
||||||
"trapos-sandbox-legacy": "0.3.0",
|
"trapos-sandbox-legacy": "0.3.0",
|
||||||
"trapos": "0.11.1"
|
"trapos": "0.11.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trapos-sandbox",
|
"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",
|
"description": "TrapOS sandbox gateway client: WS daemon (servers/sandbox) + sandbox command + MCP exec/write server",
|
||||||
"dependencies": ["trapos-core"],
|
"dependencies": ["trapos-core"],
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trapos",
|
"name": "trapos",
|
||||||
"version": "0.11.1",
|
"version": "0.11.2",
|
||||||
"description": "TrapOS full install meta-package",
|
"description": "TrapOS full install meta-package",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"trapos-boot",
|
"trapos-boot",
|
||||||
|
|||||||
@ -122,6 +122,68 @@ testlib.test('executeLua leaves direct terminal writes out of captured output',
|
|||||||
testlib.assertEquals(result.returns[1].value, 'done');
|
testlib.assertEquals(result.returns[1].value, 'done');
|
||||||
end);
|
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()
|
testlib.test('executeFile captures output and return values from file', function()
|
||||||
local mcpComputer = createMcpComputer();
|
local mcpComputer = createMcpComputer();
|
||||||
local fsLike = fsWithFiles({
|
local fsLike = fsWithFiles({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user