Exec: fail closed when sandbox host is unavailable

This commit is contained in:
Brian Mendonca
2026-02-22 01:49:10 -07:00
committed by Peter Steinberger
parent 5a0032de3e
commit c76a47cce2
4 changed files with 65 additions and 12 deletions

View File

@@ -280,6 +280,7 @@ export function createExecTool(
logInfo(`exec: elevated command ${truncateMiddle(params.command, 120)}`);
}
const configuredHost = defaults?.host ?? "sandbox";
const sandboxHostConfigured = defaults?.host === "sandbox";
const requestedHost = normalizeExecHost(params.host) ?? null;
let host: ExecHost = requestedHost ?? configuredHost;
if (!elevatedRequested && requestedHost && requestedHost !== configuredHost) {
@@ -307,6 +308,18 @@ export function createExecTool(
}
const sandbox = host === "sandbox" ? defaults?.sandbox : undefined;
if (
host === "sandbox" &&
!sandbox &&
(sandboxHostConfigured || requestedHost === "sandbox")
) {
throw new Error(
[
"exec host=sandbox is configured, but sandbox runtime is unavailable for this session.",
'Enable sandbox mode (`agents.defaults.sandbox.mode="non-main"` or `"all"`) or set tools.exec.host to "gateway"/"node".',
].join("\n"),
);
}
const rawWorkdir = params.workdir?.trim() || defaults?.cwd || process.cwd();
let workdir = rawWorkdir;
let containerWorkdir = sandbox?.containerWorkdir;

View File

@@ -601,6 +601,11 @@ describe("Agent-specific tool filtering", () => {
const cfg: OpenClawConfig = {
tools: {
deny: ["process"],
exec: {
host: "gateway",
security: "full",
ask: "off",
},
},
};
@@ -622,11 +627,30 @@ describe("Agent-specific tool filtering", () => {
expect(resultDetails?.status).toBe("completed");
});
it("fails closed when exec host=sandbox is requested without sandbox runtime", async () => {
const tools = createOpenClawCodingTools({
config: {},
sessionKey: "agent:main:main",
workspaceDir: "/tmp/test-main-fail-closed",
agentDir: "/tmp/agent-main-fail-closed",
});
const execTool = tools.find((tool) => tool.name === "exec");
expect(execTool).toBeDefined();
await expect(
execTool!.execute("call-fail-closed", {
command: "echo done",
host: "sandbox",
}),
).rejects.toThrow("exec host not allowed");
});
it("should apply agent-specific exec host defaults over global defaults", async () => {
const cfg: OpenClawConfig = {
tools: {
exec: {
host: "sandbox",
security: "full",
ask: "off",
},
},
agents: {
@@ -654,6 +678,12 @@ describe("Agent-specific tool filtering", () => {
});
const mainExecTool = mainTools.find((tool) => tool.name === "exec");
expect(mainExecTool).toBeDefined();
const mainResult = await mainExecTool!.execute("call-main-default", {
command: "echo done",
yieldMs: 1000,
});
const mainDetails = mainResult?.details as { status?: string } | undefined;
expect(mainDetails?.status).toBe("completed");
await expect(
mainExecTool!.execute("call-main", {
command: "echo done",
@@ -669,12 +699,18 @@ describe("Agent-specific tool filtering", () => {
});
const helperExecTool = helperTools.find((tool) => tool.name === "exec");
expect(helperExecTool).toBeDefined();
const helperResult = await helperExecTool!.execute("call-helper", {
command: "echo done",
host: "sandbox",
yieldMs: 1000,
});
const helperDetails = helperResult?.details as { status?: string } | undefined;
expect(helperDetails?.status).toBe("completed");
await expect(
helperExecTool!.execute("call-helper-default", {
command: "echo done",
yieldMs: 1000,
}),
).rejects.toThrow("exec host=sandbox is configured");
await expect(
helperExecTool!.execute("call-helper", {
command: "echo done",
host: "sandbox",
yieldMs: 1000,
}),
).rejects.toThrow("exec host=sandbox is configured");
});
});

View File

@@ -349,9 +349,13 @@ export function createOpenClawCodingTools(options?: {
return [tool];
});
const { cleanupMs: cleanupMsOverride, ...execDefaults } = options?.exec ?? {};
// Fail-closed baseline: when no sandbox context exists, default exec to gateway
// so we never silently treat "sandbox" as host execution.
const resolvedExecHost =
options?.exec?.host ?? execConfig.host ?? (sandbox ? "sandbox" : "gateway");
const execTool = createExecTool({
...execDefaults,
host: options?.exec?.host ?? execConfig.host,
host: resolvedExecHost,
security: options?.exec?.security ?? execConfig.security,
ask: options?.exec?.ask ?? execConfig.ask,
node: options?.exec?.node ?? execConfig.node,