-- subagents/luatool.lua: the restricted environment, the instruction budget, -- and one real fan-out through the fake host. -- -- The sandbox cases check the environment the guest actually gets rather than -- only the errors an escape attempt produces, because a missing global is the -- whole mechanism. One documented gap is asserted as a gap: the real string -- metatable is reachable from any literal, so `("").dump` exists. Without -- `load` there is no way to run bytecode, so it stays noise rather than an -- escape — the assertion is here so a future change to that reasoning is -- deliberate. local fake = require("spec.fake_ext") local luatool = require("subagents.luatool") local progress = require("subagents.progress") local function has(text, needle) assert(type(text) == "string", "expected a string, got " .. type(text)) assert(text:find(needle, 1, true), "expected to find " .. needle .. " in:\n" .. tostring(text)) end local function profile_set() local set = { list = {}, by_name = {}, warnings = {} } for _, name in ipairs({ "alpha", "beta" }) do local profile = { name = name, description = name, body = "You are " .. name .. ".\n" } set.list[#set.list + 1] = profile set.by_name[name] = profile end return set end local function with_host(fn) local handle = fake.install() local ok, err = pcall(fn, handle, profile_set()) handle.restore() if not ok then error(err, 0) end end return { { "the guest environment has no filesystem, process, or module access", function() local env = luatool.build_env() for _, name in ipairs({ "os", "io", "debug", "package", "require", "load", "loadstring", "dofile", "loadfile", "coroutine", "setmetatable", "getmetatable", "rawset", "rawget", "collectgarbage", "arg", "pcall", "xpcall", }) do assert(env[name] == nil, "the guest can reach " .. name) end assert(env._G == env, "_G must point at the restricted table") assert(type(env.subagents.workflow) == "function", "the workflow constructor is the whole API") assert(env.string.dump == nil, "string.dump is removed from the guest copy") assert(env.string ~= string, "the guest gets a copy it may safely mutate") assert(env.print() == nil, "print is a no-op") end }, { "the string metatable stays reachable, and stays harmless", function() local env = luatool.build_env() -- Documented gap: ("").dump resolves through the real string metatable. assert(type(("").dump) == "function", "the gap this note describes has moved") assert(env.load == nil and env.loadstring == nil, "bytecode is only dangerous with a loader, and there is none") end }, { "an escape attempt inside the guest fails at the call", function() with_host(function(handle, profiles) local source = [[ return subagents.workflow(function(ctx, input) return { status = "completed", output = require("os").time() } end) ]] local text = luatool.handle({ prompt = "x", source = source }, profiles) has(text, "Error:") has(text, "nil value") assert(#handle.spawns == 0, "the guest started no children") end) end }, { "source that does not return a workflow is refused", function() with_host(function(handle, profiles) has(luatool.handle({ prompt = "x", source = "return 42" }, profiles), "Error: source must return subagents.workflow(function(ctx, input) ... end)") has(luatool.handle({ prompt = "x", source = "return (" }, profiles), "Error: source did not compile") has(luatool.handle({ prompt = "x", source = "error('nope')" }, profiles), "Error: source failed to run") end) end }, { "prompt and source are both required", function() with_host(function(handle, profiles) has(luatool.handle({ source = "return 1" }, profiles), "Error: prompt is required") has(luatool.handle({ prompt = "x" }, profiles), "Error: source is required") has(luatool.handle({ prompt = "", source = "return 1" }, profiles), "Error: prompt is required") end) end }, { "a runaway guest is stopped by the instruction budget", function() with_host(function(handle, profiles) local source = [[ return subagents.workflow(function(ctx, input) local n = 0 while true do n = n + 1 end end) ]] local text = luatool.handle({ prompt = "x", source = source }, profiles) has(text, "Error:") has(text, "instruction budget exceeded") end) end }, { "the guest cannot catch the budget error and spin again", function() with_host(function(handle, profiles) -- Bounded so a regression fails this case instead of hanging it: with -- pcall back in the environment the guest would burn three budgets and -- then report success. local source = [[ return subagents.workflow(function(ctx, input) for _ = 1, 3 do pcall(function() while true do end end) end return { status = "completed", output = "outlived the budget" } end) ]] local text = luatool.handle({ prompt = "x", source = source }, profiles) has(text, "Error:") has(text, "pcall") end) end }, { "inline agent profiles are scoped to one workflow invocation", function() with_host(function(handle, profiles) handle.queue_for("local-reviewer", { id = "0198-local", output = "reviewed" }) local source = [[ return subagents.workflow(function(ctx, input) return ctx:agent({ agent = "local-reviewer", prompt = input }):await() end) ]] progress.reset() local component progress.claim({ id = "lua-call", tool_name = "subagents.lua", collapsed = true, set_component = function(_, value) component = value return { invalidate = function() end, alive = function() return true end, set_pinned = function() end, } end, }) progress.bind({ tool_call_id = "lua-call" }) local text = luatool.handle({ prompt = "inspect this", source = source, agents = { { name = "local-reviewer", description = "One-off reviewer", system_prompt = "Review only the requested change.", }, }, }, profiles) has(text, "reviewed") local compact = table.concat(component:render(100), "\n") assert(not compact:find("inspect this", 1, true), compact) assert(not compact:find("Review only the requested change.", 1, true), compact) progress.collapse({ collapsed = false }) local expanded = table.concat(component:render(100), "\n") has(expanded, "system prompt: Review only the requested change.") has(expanded, "prompt: inspect this") assert(#handle.spawns == 1, "the inline profile started one child") assert(handle.spawns[1].label == "local-reviewer") local seeded = handle.spawns[1].system_messages assert(seeded[#seeded].text == "Review only the requested change.") local missing = luatool.handle({ prompt = "again", source = source }, profiles) has(missing, "unknown agent 'local-reviewer'") assert(#handle.spawns == 1, "the inline profile did not leak into the next workflow") progress.reset() end) end }, { "invalid inline profiles fail before running guest source", function() with_host(function(handle, profiles) local text = luatool.handle({ prompt = "x", source = "error('guest source should not run')", agents = { { name = "local", system_prompt = "" } }, }, profiles) has(text, "agents[1].system_prompt must be a non-empty string") assert(#handle.spawns == 0) end) end }, { "a fan-out runs end to end and renders one block per child", function() with_host(function(handle, profiles) handle.queue_for("alpha", { id = "0198-a", output = "alpha says hi" }) handle.queue_for("beta", { id = "0198-b", output = "beta says hi" }) local source = [[ return subagents.workflow(function(ctx, input) local jobs = {} for _, name in ipairs({ "alpha", "beta" }) do jobs[#jobs + 1] = ctx:agent({ agent = name, prompt = "handle " .. input }) end return ctx:await(jobs, "all") end) ]] local text = luatool.handle({ prompt = "the task", source = source }, profiles) assert(#handle.spawns == 2, "one spawn per ctx:agent") assert(handle.spawns[1].prompt == "handle the task", tostring(handle.spawns[1].prompt)) assert(handle.spawns[1].label == "alpha") has(text, "id: 0198-a") has(text, "alpha says hi") has(text, "id: 0198-b") has(text, "beta says hi") assert(select(2, text:gsub("status: completed", "")) == 2, "expected two rendered blocks") end) end }, { "the job budget applies to a generated workflow", function() with_host(function(handle, profiles) local source = string.format([[ return subagents.workflow(function(ctx, input) for index = 1, %d do ctx:agent({ agent = "alpha", prompt = "spam " .. index }) end end) ]], luatool.max_jobs + 1) local text = luatool.handle({ prompt = "x", source = source }, profiles) has(text, "job limit exceeded") assert(#handle.runs == luatool.max_jobs, "the cap is enforced at the host boundary") end) end }, { "the guest cannot raise its own job cap through ctx", function() with_host(function(handle, profiles) local source = string.format([[ return subagents.workflow(function(ctx, input) ctx.max_jobs = nil ctx.job_count = 0 for index = 1, %d do ctx:agent({ agent = "alpha", prompt = "spam " .. index }) end end) ]], luatool.max_jobs + 1) local text = luatool.handle({ prompt = "x", source = source }, profiles) has(text, "job limit exceeded") assert(#handle.runs == luatool.max_jobs, "the cap is private state, not a ctx field") end) end }, }