mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
Exec: fail closed when sandbox host is unavailable
This commit is contained in:
committed by
Peter Steinberger
parent
5a0032de3e
commit
c76a47cce2
@@ -29,7 +29,7 @@ Background sessions are scoped per agent; `process` only sees sessions from the
|
||||
|
||||
Notes:
|
||||
|
||||
- `host` defaults to `sandbox`.
|
||||
- `host` defaults to `sandbox` when sandbox runtime is active, and defaults to `gateway` otherwise.
|
||||
- `elevated` is ignored when sandboxing is off (exec already runs on the host).
|
||||
- `gateway`/`node` approvals are controlled by `~/.openclaw/exec-approvals.json`.
|
||||
- `node` requires a paired node (companion app or headless node host).
|
||||
@@ -38,9 +38,9 @@ Notes:
|
||||
from `PATH` to avoid fish-incompatible scripts, then falls back to `SHELL` if neither exists.
|
||||
- Host execution (`gateway`/`node`) rejects `env.PATH` and loader overrides (`LD_*`/`DYLD_*`) to
|
||||
prevent binary hijacking or injected code.
|
||||
- Important: sandboxing is **off by default**. If sandboxing is off, `host=sandbox` runs directly on
|
||||
the gateway host (no container) and **does not require approvals**. To require approvals, run with
|
||||
`host=gateway` and configure exec approvals (or enable sandboxing).
|
||||
- Important: sandboxing is **off by default**. If sandboxing is off and `host=sandbox` is explicitly
|
||||
configured/requested, exec now fails closed instead of silently running on the gateway host.
|
||||
Enable sandboxing or use `host=gateway` with approvals.
|
||||
- Script preflight checks (for common Python/Node shell-syntax mistakes) only inspect files inside the
|
||||
effective `workdir` boundary. If a script path resolves outside `workdir`, preflight is skipped for
|
||||
that 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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user