From 45c8207ef29c0ef4390aa58ac81089f878ab44b9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 2 Apr 2026 18:24:27 +0100 Subject: [PATCH] fix(exec): clarify auto routing semantics (#58897) (thanks @vincentkoc) --- CHANGELOG.md | 1 + docs/cli/approvals.md | 6 ++ docs/help/troubleshooting.md | 1 + docs/nodes/index.md | 2 + docs/tools/exec-approvals.md | 6 ++ docs/tools/exec.md | 3 + src/agents/bash-tools.exec-runtime.test.ts | 73 ++++++++++++++++++++++ src/agents/bash-tools.exec-runtime.ts | 6 ++ 8 files changed, 98 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eda560d3225..c23a8dcca40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ Docs: https://docs.openclaw.ai - Agents/subagents: pin admin-only subagent gateway calls to `operator.admin` while keeping `agent` at least privilege, so `sessions_spawn` no longer dies on loopback scope-upgrade pairing with `close(1008) "pairing required"`. (#59555) Thanks @openperf. - Exec approvals/config: strip invalid `security`, `ask`, and `askFallback` values from `~/.openclaw/exec-approvals.json` during normalization so malformed policy enums fall back cleanly to the documented defaults instead of corrupting runtime policy resolution. (#59112) Thanks @openperf. - Exec approvals/doctor: report host policy sources from the real approvals file path and ignore malformed host override values when attributing effective policy conflicts. (#59367) Thanks @gumadeiras. +- Exec/runtime: treat `tools.exec.host=auto` as routing-only, keep implicit no-config exec on sandbox when available or gateway otherwise, and reject per-call host overrides that would bypass the configured sandbox or host target. (#58897) Thanks @vincentkoc. - Slack/mrkdwn formatting: add built-in Slack mrkdwn guidance in inbound context so Slack replies stop falling back to generic Markdown patterns that render poorly in Slack. (#59100) Thanks @jadewon. - WhatsApp/presence: send `unavailable` presence on connect in self-chat mode so personal-phone users stop losing all push notifications while the gateway is running. (#59410) Thanks @mcaxtr. - WhatsApp/media: add HTML, XML, and CSS to the MIME map and fall back gracefully for unknown media types instead of dropping the attachment. (#51562) Thanks @bobbyt74. diff --git a/docs/cli/approvals.md b/docs/cli/approvals.md index d3b0d0278db..85243e557b2 100644 --- a/docs/cli/approvals.md +++ b/docs/cli/approvals.md @@ -85,6 +85,12 @@ openclaw config set tools.exec.security full openclaw config set tools.exec.ask off ``` +Why `tools.exec.host=gateway` in this example: + +- `host=auto` still means "sandbox when available, otherwise gateway". +- YOLO is about approvals, not routing. +- If you want host exec even when a sandbox is configured, make the host choice explicit with `gateway` or `/exec host=gateway`. + This matches the current host-default YOLO behavior. Tighten it if you want approvals. ## Allowlist helpers diff --git a/docs/help/troubleshooting.md b/docs/help/troubleshooting.md index a345c0bbcd0..eec340839b1 100644 --- a/docs/help/troubleshooting.md +++ b/docs/help/troubleshooting.md @@ -278,6 +278,7 @@ flowchart TD - If `tools.exec.host` is unset, the default is `auto`. - `host=auto` resolves to `sandbox` when a sandbox runtime is active, `gateway` otherwise. + - `host=auto` is routing only; the no-prompt "YOLO" behavior comes from `security=full` plus `ask=off` on gateway/node. - On `gateway` and `node`, unset `tools.exec.security` defaults to `full`. - Unset `tools.exec.ask` defaults to `off`. - Result: if you are seeing approvals, some host-local or per-session policy tightened exec away from the current defaults. diff --git a/docs/nodes/index.md b/docs/nodes/index.md index 96485488e42..c2d1e5c13dd 100644 --- a/docs/nodes/index.md +++ b/docs/nodes/index.md @@ -157,6 +157,8 @@ Or per session: Once set, any `exec` call with `host=node` runs on the node host (subject to the node allowlist/approvals). +`host=auto` will not silently hop to the node just because a tool call requests it. If you want node exec, set `tools.exec.host=node` or `/exec host=node ...` explicitly. + Related: - [Node host CLI](/cli/node) diff --git a/docs/tools/exec-approvals.md b/docs/tools/exec-approvals.md index 6420e9b7365..6e7b306640d 100644 --- a/docs/tools/exec-approvals.md +++ b/docs/tools/exec-approvals.md @@ -104,6 +104,12 @@ This is now the default host behavior unless you tighten it explicitly: - `tools.exec.ask`: `off` - host `askFallback`: `full` +Important distinction: + +- `tools.exec.host=auto` chooses where exec runs: sandbox when available, otherwise gateway. +- YOLO chooses how host exec is approved: `security=full` plus `ask=off`. +- `auto` does not let a tool call override a sandboxed session to `gateway` or `node`. If you want a different host, set `tools.exec.host` or use `/exec host=...` explicitly. + If you want a more conservative setup, tighten either layer back to `allowlist` / `on-miss` or `deny`. diff --git a/docs/tools/exec.md b/docs/tools/exec.md index 155df0527f5..192b0b272a5 100644 --- a/docs/tools/exec.md +++ b/docs/tools/exec.md @@ -30,6 +30,8 @@ Background sessions are scoped per agent; `process` only sees sessions from the Notes: - `host` defaults to `auto`: sandbox when sandbox runtime is active for the session, otherwise gateway. +- `auto` is only the default routing strategy. It is not a wildcard override that lets a tool call jump from sandbox to gateway or node. +- With no extra config, `host=auto` still "just works": no sandbox means it resolves to `gateway`; a live sandbox means it stays in the sandbox. - `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). @@ -57,6 +59,7 @@ Notes: - `tools.exec.security` (default: `deny` for sandbox, `full` for gateway + node when unset) - `tools.exec.ask` (default: `off`) - No-approval host exec is the default for gateway + node. If you want approvals/allowlist behavior, tighten both `tools.exec.*` and the host `~/.openclaw/exec-approvals.json`; see [Exec approvals](/tools/exec-approvals#no-approval-yolo-mode). +- YOLO comes from the host-policy defaults (`security=full`, `ask=off`), not from `host=auto`. If you want to force gateway or node routing, set `tools.exec.host` or use `/exec host=...`. - `tools.exec.node` (default: unset) - `tools.exec.strictInlineEval` (default: false): when true, inline interpreter eval forms such as `python -c`, `node -e`, `ruby -e`, `perl -e`, `php -r`, `lua -e`, and `osascript -e` always require explicit approval. `allow-always` can still persist benign interpreter/script invocations, but inline-eval forms still prompt each time. - `tools.exec.pathPrepend`: list of directories to prepend to `PATH` for exec runs (gateway + sandbox only). diff --git a/src/agents/bash-tools.exec-runtime.test.ts b/src/agents/bash-tools.exec-runtime.test.ts index a9dc655dec3..19e2378e578 100644 --- a/src/agents/bash-tools.exec-runtime.test.ts +++ b/src/agents/bash-tools.exec-runtime.test.ts @@ -47,6 +47,36 @@ describe("resolveExecTarget", () => { ({ resolveExecTarget } = await import("./bash-tools.exec-runtime.js")); }); + it("keeps implicit auto on sandbox when a sandbox runtime is available", () => { + expect( + resolveExecTarget({ + configuredTarget: "auto", + elevatedRequested: false, + sandboxAvailable: true, + }), + ).toMatchObject({ + configuredTarget: "auto", + requestedTarget: null, + selectedTarget: "auto", + effectiveHost: "sandbox", + }); + }); + + it("keeps implicit auto on gateway when no sandbox runtime is available", () => { + expect( + resolveExecTarget({ + configuredTarget: "auto", + elevatedRequested: false, + sandboxAvailable: false, + }), + ).toMatchObject({ + configuredTarget: "auto", + requestedTarget: null, + selectedTarget: "auto", + effectiveHost: "gateway", + }); + }); + it("rejects host overrides when configured host is auto", () => { expect(() => resolveExecTarget({ @@ -84,6 +114,49 @@ describe("resolveExecTarget", () => { effectiveHost: "sandbox", }); }); + + it("requires an exact match for non-auto configured targets", () => { + expect(() => + resolveExecTarget({ + configuredTarget: "gateway", + requestedTarget: "auto", + elevatedRequested: false, + sandboxAvailable: true, + }), + ).toThrow("exec host not allowed"); + }); + + it("allows exact node matches", () => { + expect( + resolveExecTarget({ + configuredTarget: "node", + requestedTarget: "node", + elevatedRequested: false, + sandboxAvailable: true, + }), + ).toMatchObject({ + configuredTarget: "node", + requestedTarget: "node", + selectedTarget: "node", + effectiveHost: "node", + }); + }); + + it("still forces elevated requests onto the gateway host", () => { + expect( + resolveExecTarget({ + configuredTarget: "auto", + requestedTarget: "sandbox", + elevatedRequested: true, + sandboxAvailable: true, + }), + ).toMatchObject({ + configuredTarget: "auto", + requestedTarget: "sandbox", + selectedTarget: "gateway", + effectiveHost: "gateway", + }); + }); }); describe("emitExecSystemEvent", () => { diff --git a/src/agents/bash-tools.exec-runtime.ts b/src/agents/bash-tools.exec-runtime.ts index 8849f0605f6..bde8085b501 100644 --- a/src/agents/bash-tools.exec-runtime.ts +++ b/src/agents/bash-tools.exec-runtime.ts @@ -221,6 +221,9 @@ export function isRequestedExecTargetAllowed(params: { configuredTarget: ExecTarget; requestedTarget: ExecTarget; }) { + // `auto` is a routing strategy, not a wildcard allowlist. Keep per-call host + // selection pinned to the configured/session-selected target so a sandboxed + // session cannot silently hop to gateway or node. return params.requestedTarget === params.configuredTarget; } @@ -253,6 +256,9 @@ export function resolveExecTarget(params: { ); } const selectedTarget = requestedTarget ?? configuredTarget; + // `auto` preserves the no-config "just work" default: sandbox when available, + // otherwise gateway. The YOLO part comes from security/ask defaults, not from + // `auto` itself. const effectiveHost = selectedTarget === "auto" ? (params.sandboxAvailable ? "sandbox" : "gateway") : selectedTarget; return {