fix: a sandboxed agent can request host node in an ex (#384) (#63880)

This commit is contained in:
Devin Robison
2026-04-10 10:40:27 -06:00
committed by GitHub
parent 777c6f7580
commit dffad08529
5 changed files with 91 additions and 16 deletions

View File

@@ -116,6 +116,7 @@ Docs: https://docs.openclaw.ai
- Matrix/migration: keep packaged warning-only crypto migrations from being misclassified as actionable when only helper chunks are present, so startup and doctor stay on the warning-only path instead of creating unnecessary migration snapshots. (#64373) Thanks @gumadeiras.
- Matrix/ACP thread bindings: preserve canonical room casing and parent conversation routing during ACP session spawn so mixed-case room ids bind correctly from top-level rooms and existing Matrix threads. (#64343) Thanks @gumadeiras.
- Agents/subagents: deduplicate delivered completion announces so retry or re-entry cleanup does not inject duplicate internal-context completion turns into the parent session. (#61525) Thanks @100yenadmin.
- Agents/exec: keep sandboxed `tools.exec.host=auto` sessions from honoring per-call `host=node` or `host=gateway` overrides while a sandbox runtime is active, and stop advertising node routing in that state so exec stays on the sandbox host. (#63880)
## 2026.4.9

View File

@@ -127,7 +127,20 @@ describe("resolveExecTarget", () => {
sandboxAvailable: true,
}),
).toThrow(
"exec host not allowed (requested gateway; configured host is auto; set tools.exec.host=gateway or auto to allow this override).",
"exec host not allowed (requested gateway; configured host is auto; set tools.exec.host=gateway to allow this override).",
);
});
it("rejects per-call host=node override from auto when sandbox is available", () => {
expect(() =>
resolveExecTarget({
configuredTarget: "auto",
requestedTarget: "node",
elevatedRequested: false,
sandboxAvailable: true,
}),
).toThrow(
"exec host not allowed (requested node; configured host is auto; set tools.exec.host=node to allow this override).",
);
});

View File

@@ -228,7 +228,10 @@ export function isRequestedExecTargetAllowed(params: {
return true;
}
if (params.configuredTarget === "auto") {
if (params.sandboxAvailable && params.requestedTarget === "gateway") {
if (
params.sandboxAvailable &&
(params.requestedTarget === "gateway" || params.requestedTarget === "node")
) {
return false;
}
return true;
@@ -254,9 +257,13 @@ export function resolveExecTarget(params: {
) {
const allowedConfig = Array.from(
new Set(
requestedTarget === "gateway" && !params.sandboxAvailable
? ["gateway", "auto"]
: [renderExecTargetLabel(requestedTarget), "auto"],
configuredTarget === "auto" &&
params.sandboxAvailable &&
(requestedTarget === "gateway" || requestedTarget === "node")
? [renderExecTargetLabel(requestedTarget)]
: requestedTarget === "gateway" && !params.sandboxAvailable
? ["gateway", "auto"]
: [renderExecTargetLabel(requestedTarget), "auto"],
),
).join(" or ");
throw new Error(

View File

@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { SessionEntry } from "../config/sessions.js";
import * as execApprovals from "../infra/exec-approvals.js";
import { resolveExecDefaults } from "./exec-defaults.js";
import { canExecRequestNode, resolveExecDefaults } from "./exec-defaults.js";
describe("resolveExecDefaults", () => {
beforeEach(() => {
@@ -27,7 +27,7 @@ describe("resolveExecDefaults", () => {
).toBe(false);
});
it("keeps node routing available when exec host is auto", () => {
it("does not advertise node routing when exec host is auto and sandbox is available", () => {
expect(
resolveExecDefaults({
cfg: {
@@ -42,6 +42,25 @@ describe("resolveExecDefaults", () => {
).toMatchObject({
host: "auto",
effectiveHost: "sandbox",
canRequestNode: false,
});
});
it("keeps node routing available when exec host is auto without sandbox", () => {
expect(
resolveExecDefaults({
cfg: {
tools: {
exec: {
host: "auto",
},
},
},
sandboxAvailable: false,
}),
).toMatchObject({
host: "auto",
effectiveHost: "gateway",
canRequestNode: true,
});
});
@@ -104,4 +123,19 @@ describe("resolveExecDefaults", () => {
ask: "off",
});
});
it("blocks node advertising in helper calls when sandbox is available", () => {
expect(
canExecRequestNode({
cfg: {
tools: {
exec: {
host: "auto",
},
},
},
sandboxAvailable: true,
}),
).toBe(false);
});
});

View File

@@ -53,16 +53,38 @@ function resolveExecConfigState(params: {
};
}
function resolveExecSandboxAvailability(params: {
cfg: OpenClawConfig;
sessionKey?: string;
sandboxAvailable?: boolean;
}) {
return (
params.sandboxAvailable ??
(params.sessionKey
? resolveSandboxRuntimeStatus({
cfg: params.cfg,
sessionKey: params.sessionKey,
}).sandboxed
: false)
);
}
export function canExecRequestNode(params: {
cfg?: OpenClawConfig;
sessionEntry?: SessionEntry;
agentId?: string;
sessionKey?: string;
sandboxAvailable?: boolean;
}): boolean {
const { host } = resolveExecConfigState(params);
const { cfg, host } = resolveExecConfigState(params);
return isRequestedExecTargetAllowed({
configuredTarget: host,
requestedTarget: "node",
sandboxAvailable: resolveExecSandboxAvailability({
cfg,
sessionKey: params.sessionKey,
sandboxAvailable: params.sandboxAvailable,
}),
});
}
@@ -81,14 +103,11 @@ export function resolveExecDefaults(params: {
canRequestNode: boolean;
} {
const { cfg, host, agentExec, globalExec } = resolveExecConfigState(params);
const sandboxAvailable =
params.sandboxAvailable ??
(params.sessionKey
? resolveSandboxRuntimeStatus({
cfg,
sessionKey: params.sessionKey,
}).sandboxed
: false);
const sandboxAvailable = resolveExecSandboxAvailability({
cfg,
sessionKey: params.sessionKey,
sandboxAvailable: params.sandboxAvailable,
});
const resolved = resolveExecTarget({
configuredTarget: host,
elevatedRequested: false,
@@ -115,6 +134,7 @@ export function resolveExecDefaults(params: {
canRequestNode: isRequestedExecTargetAllowed({
configuredTarget: host,
requestedTarget: "node",
sandboxAvailable,
}),
};
}