From 5d4c4bb85061cc47177e6932aaecea8f377a04c5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 29 Mar 2026 21:18:14 +0100 Subject: [PATCH] fix(exec): restore runtime-aware implicit host default --- CHANGELOG.md | 1 + SECURITY.md | 2 +- docs/gateway/security/index.md | 4 ++-- docs/tools/exec.md | 4 ++-- src/agents/bash-tools.exec.path.test.ts | 12 ++++++------ src/agents/bash-tools.exec.ts | 9 ++++++--- src/agents/pi-tools-agent-config.test.ts | 16 ++++++++-------- 7 files changed, 26 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22ddba3d66ce..768d1ee4fcbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2574,6 +2574,7 @@ Docs: https://docs.openclaw.ai - Security/Exec: in non-default setups that manually add `sort` to `tools.exec.safeBins`, block `sort --compress-program` so allowlist-mode safe-bin checks cannot bypass approval. Thanks @tdjackey for reporting. - Security/Exec approvals: when users choose `allow-always` for shell-wrapper commands (for example `/bin/zsh -lc ...`), persist allowlist patterns for the inner executable(s) instead of the wrapper shell binary, preventing accidental broad shell allowlisting in moderate mode. (#23276) Thanks @xrom2863. - Security/Exec: fail closed when `tools.exec.host=sandbox` is configured/requested but sandbox runtime is unavailable. (#23398) Thanks @bmendonca3. +- Security/Exec: restore runtime-aware implicit exec host selection so sandbox-off sessions keep defaulting to the gateway host, while explicit `host=sandbox` still fails closed without a sandbox runtime. (#56800) - Security/macOS app beta: enforce path-only `system.run` allowlist matching (drop basename matches like `echo`), migrate legacy basename entries to last resolved paths when available, and harden shell-chain handling to fail closed on unsafe parse/control syntax (including quoted command substitution/backticks). This is an optional allowlist-mode feature; default installs remain deny-by-default. Thanks @tdjackey for reporting. - Security/Agents: auto-generate and persist a dedicated `commands.ownerDisplaySecret` when `commands.ownerDisplay=hash`, remove gateway token fallback from owner-ID prompt hashing across CLI and embedded agent runners, and centralize owner-display secret resolution in one shared helper. Thanks @aether-ai-agent for reporting. - Security/SSRF: expand IPv4 fetch guard blocking to include RFC special-use/non-global ranges (including benchmarking, TEST-NET, multicast, and reserved/broadcast blocks), centralize range checks into a single CIDR policy table, and reuse one shared host/IP classifier across literal + DNS checks to reduce classifier drift. Thanks @princeeismond-dot for reporting. diff --git a/SECURITY.md b/SECURITY.md index bef814525a59..1000bae9b731 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -101,7 +101,7 @@ OpenClaw does **not** model one gateway as a multi-tenant, adversarial user boun - If multiple users need OpenClaw, use one VPS (or host/OS user boundary) per user. - For advanced setups, multiple gateways on one machine are possible, but only with strict isolation and are not the recommended default. - Exec behavior is host-first by default: `agents.defaults.sandbox.mode` defaults to `off`. -- `tools.exec.host` defaults to `sandbox` as a routing preference, but if sandbox runtime is not active for the session, exec runs on the gateway host. +- `tools.exec.host` defaults to `sandbox` only when sandbox runtime is active for the session; otherwise implicit exec runs on the gateway host. - Implicit exec calls (no explicit host in the tool call) follow the same behavior. - This is expected in OpenClaw's one-user trusted-operator model. If you need isolation, enable sandbox mode (`non-main`/`all`) and keep strict tool policy. diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index ef26d3ab599f..97ffa3804cf2 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -191,7 +191,7 @@ If more than one person can DM your bot: - **Local disk hygiene** (permissions, symlinks, config includes, “synced folder” paths). - **Plugins** (extensions exist without an explicit allowlist). - **Policy drift/misconfig** (sandbox docker settings configured but sandbox mode off; ineffective `gateway.nodes.denyCommands` patterns because matching is exact command-name only (for example `system.run`) and does not inspect shell text; dangerous `gateway.nodes.allowCommands` entries; global `tools.profile="minimal"` overridden by per-agent profiles; extension plugin tools reachable under permissive tool policy). -- **Runtime expectation drift** (for example `tools.exec.host="sandbox"` while sandbox mode is off, which now fails closed because no sandbox runtime is available). +- **Runtime expectation drift** (for example `tools.exec.host="sandbox"` while sandbox mode is off, which fails closed because no sandbox runtime is available). - **Model hygiene** (warn when configured models look legacy; not a hard block). If you run `--deep`, OpenClaw also attempts a best-effort live Gateway probe. @@ -534,7 +534,7 @@ Even with strong system prompts, **prompt injection is not solved**. System prom - Prefer mention gating in groups; avoid “always-on” bots in public rooms. - Treat links, attachments, and pasted instructions as hostile by default. - Run sensitive tool execution in a sandbox; keep secrets out of the agent’s reachable filesystem. -- Note: sandboxing is opt-in. If sandbox mode is off, `host=sandbox` fails closed even though tools.exec.host defaults to sandbox. To run on the gateway host, set `host=gateway` and configure exec approvals. +- Note: sandboxing is opt-in. If sandbox mode is off, explicit `host=sandbox` fails closed because no sandbox runtime is available. Implicit exec still runs on the gateway host; set `host=gateway` if you want that behavior to be explicit in config. - Limit high-risk tools (`exec`, `browser`, `web_fetch`, `web_search`) to trusted agents or explicit allowlists. - If you allowlist interpreters (`python`, `node`, `ruby`, `perl`, `php`, `lua`, `osascript`), enable `tools.exec.strictInlineEval` so inline eval forms still need explicit approval. - **Model choice matters:** older/smaller/legacy models are significantly less robust against prompt injection and tool misuse. For tool-enabled agents, use the strongest latest-generation, instruction-hardened model available. diff --git a/docs/tools/exec.md b/docs/tools/exec.md index d983d82f4395..4498ef99535a 100644 --- a/docs/tools/exec.md +++ b/docs/tools/exec.md @@ -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 for the session; otherwise it defaults to `gateway`. - `elevated` forces `host=gateway`; it is only available when elevated access is enabled for the current session/provider. - `gateway`/`node` approvals are controlled by `~/.openclaw/exec-approvals.json`. - `node` requires a paired node (companion app or headless node host). @@ -52,7 +52,7 @@ Notes: - `tools.exec.notifyOnExit` (default: true): when true, backgrounded exec sessions enqueue a system event and request a heartbeat on exit. - `tools.exec.approvalRunningNoticeMs` (default: 10000): emit a single “running” notice when an approval-gated exec runs longer than this (0 disables). -- `tools.exec.host` (default: `sandbox`) +- `tools.exec.host` (default: runtime-aware: `sandbox` when sandbox runtime is active, `gateway` otherwise) - `tools.exec.security` (default: `deny` for sandbox, `allowlist` for gateway + node when unset) - `tools.exec.ask` (default: `on-miss`) - `tools.exec.node` (default: unset) diff --git a/src/agents/bash-tools.exec.path.test.ts b/src/agents/bash-tools.exec.path.test.ts index 204660ddc69d..4f3a7e39eee5 100644 --- a/src/agents/bash-tools.exec.path.test.ts +++ b/src/agents/bash-tools.exec.path.test.ts @@ -251,14 +251,14 @@ describe("exec host env validation", () => { } }); - it("fails closed when the implicit sandbox host has no sandbox runtime", async () => { + it("defaults implicit exec host to gateway when sandbox runtime is unavailable", async () => { const tool = createExecTool({ security: "full", ask: "off" }); - await expect( - tool.execute("call1", { - command: "echo ok", - }), - ).rejects.toThrow(/sandbox runtime is unavailable/); + const result = await tool.execute("call1", { + command: "echo ok", + }); + const output = normalizeText(result.content.find((c) => c.type === "text")?.text); + expect(output).toContain("ok"); }); it("fails closed when sandbox host is explicitly configured without sandbox runtime", async () => { diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index c46ae918fe4d..d556e9b724d8 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -329,7 +329,9 @@ export function createExecTool( if (elevatedRequested) { logInfo(`exec: elevated command ${truncateMiddle(params.command, 120)}`); } - const configuredHost = defaults?.host ?? "sandbox"; + // Keep the implicit host aligned with the active runtime: host-first unless a sandbox + // runtime is actually available for this session, while still honoring explicit overrides. + const configuredHost = defaults?.host ?? (defaults?.sandbox ? "sandbox" : "gateway"); const requestedHost = normalizeExecHost(params.host) ?? null; let host: ExecHost = requestedHost ?? configuredHost; if (!elevatedRequested && requestedHost && requestedHost !== configuredHost) { @@ -358,8 +360,9 @@ export function createExecTool( } const sandbox = host === "sandbox" ? defaults?.sandbox : undefined; - // Never fall through to direct host exec when the selected host was sandbox. - if (host === "sandbox" && !sandbox) { + const sandboxHostConfigured = defaults?.host === "sandbox" || requestedHost === "sandbox"; + // Never fall through to direct host exec when sandbox was selected explicitly. + if (host === "sandbox" && !sandbox && sandboxHostConfigured) { throw new Error( [ "exec host resolved to sandbox, but sandbox runtime is unavailable for this session.", diff --git a/src/agents/pi-tools-agent-config.test.ts b/src/agents/pi-tools-agent-config.test.ts index d4b8aff7ea43..258ded2466ca 100644 --- a/src/agents/pi-tools-agent-config.test.ts +++ b/src/agents/pi-tools-agent-config.test.ts @@ -658,21 +658,21 @@ describe("Agent-specific tool filtering", () => { expect(resultDetails?.status).toBe("completed"); }); - it("fails closed when the implicit exec host resolves to sandbox without a runtime", async () => { + it("defaults implicit exec host to gateway when sandbox runtime is unavailable", async () => { const tools = createOpenClawCodingTools({ config: {}, sessionKey: "agent:main:main", - workspaceDir: "/tmp/test-main-implicit-sandbox", - agentDir: "/tmp/agent-main-implicit-sandbox", + workspaceDir: "/tmp/test-main-implicit-gateway", + agentDir: "/tmp/agent-main-implicit-gateway", }); const execTool = tools.find((tool) => tool.name === "exec"); expect(execTool).toBeDefined(); - await expect( - execTool!.execute("call-implicit-sandbox-default", { - command: "echo done", - }), - ).rejects.toThrow("sandbox runtime is unavailable"); + const result = await execTool!.execute("call-implicit-gateway-default", { + command: "echo done", + }); + const resultDetails = result?.details as { status?: string } | undefined; + expect(resultDetails?.status).toBe("completed"); }); it("fails closed when exec host=sandbox is requested without sandbox runtime", async () => {