fix(exec): clarify auto routing semantics (#58897) (thanks @vincentkoc)

This commit is contained in:
Peter Steinberger 2026-04-02 18:24:27 +01:00
parent 938541999e
commit 45c8207ef2
8 changed files with 98 additions and 0 deletions

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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`.

View File

@ -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).

View File

@ -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", () => {

View File

@ -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 {