mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-10 00:31:22 +00:00
fix(agents): resolve exec host=node routing regression and elevated gateway override
This commit is contained in:
@@ -86,18 +86,39 @@ describe("resolveExecTarget", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects host overrides when configured host is auto", () => {
|
||||
expect(() =>
|
||||
it("allows per-call host=node override when configured host is auto", () => {
|
||||
expect(
|
||||
resolveExecTarget({
|
||||
configuredTarget: "auto",
|
||||
requestedTarget: "node",
|
||||
elevatedRequested: false,
|
||||
sandboxAvailable: false,
|
||||
}),
|
||||
).toThrow("exec host not allowed");
|
||||
).toMatchObject({
|
||||
configuredTarget: "auto",
|
||||
requestedTarget: "node",
|
||||
selectedTarget: "node",
|
||||
effectiveHost: "node",
|
||||
});
|
||||
});
|
||||
|
||||
it("also rejects gateway override when configured host is auto", () => {
|
||||
it("allows per-call host=gateway override when configured host is auto and no sandbox", () => {
|
||||
expect(
|
||||
resolveExecTarget({
|
||||
configuredTarget: "auto",
|
||||
requestedTarget: "gateway",
|
||||
elevatedRequested: false,
|
||||
sandboxAvailable: false,
|
||||
}),
|
||||
).toMatchObject({
|
||||
configuredTarget: "auto",
|
||||
requestedTarget: "gateway",
|
||||
selectedTarget: "gateway",
|
||||
effectiveHost: "gateway",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects per-call host=gateway override from auto when sandbox is available", () => {
|
||||
expect(() =>
|
||||
resolveExecTarget({
|
||||
configuredTarget: "auto",
|
||||
@@ -108,6 +129,33 @@ describe("resolveExecTarget", () => {
|
||||
).toThrow("exec host not allowed");
|
||||
});
|
||||
|
||||
it("allows per-call host=sandbox override when configured host is auto", () => {
|
||||
expect(
|
||||
resolveExecTarget({
|
||||
configuredTarget: "auto",
|
||||
requestedTarget: "sandbox",
|
||||
elevatedRequested: false,
|
||||
sandboxAvailable: true,
|
||||
}),
|
||||
).toMatchObject({
|
||||
configuredTarget: "auto",
|
||||
requestedTarget: "sandbox",
|
||||
selectedTarget: "sandbox",
|
||||
effectiveHost: "sandbox",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects cross-host override when configured target is a concrete host", () => {
|
||||
expect(() =>
|
||||
resolveExecTarget({
|
||||
configuredTarget: "node",
|
||||
requestedTarget: "gateway",
|
||||
elevatedRequested: false,
|
||||
sandboxAvailable: false,
|
||||
}),
|
||||
).toThrow("exec host not allowed");
|
||||
});
|
||||
|
||||
it("allows explicit auto request when configured host is auto", () => {
|
||||
expect(
|
||||
resolveExecTarget({
|
||||
@@ -151,7 +199,7 @@ describe("resolveExecTarget", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("still forces elevated requests onto the gateway host", () => {
|
||||
it("forces elevated requests onto the gateway host when configured target is auto", () => {
|
||||
expect(
|
||||
resolveExecTarget({
|
||||
configuredTarget: "auto",
|
||||
@@ -166,6 +214,56 @@ describe("resolveExecTarget", () => {
|
||||
effectiveHost: "gateway",
|
||||
});
|
||||
});
|
||||
|
||||
it("honours node target for elevated requests when configured target is node", () => {
|
||||
expect(
|
||||
resolveExecTarget({
|
||||
configuredTarget: "node",
|
||||
requestedTarget: "node",
|
||||
elevatedRequested: true,
|
||||
sandboxAvailable: false,
|
||||
}),
|
||||
).toMatchObject({
|
||||
configuredTarget: "node",
|
||||
requestedTarget: "node",
|
||||
selectedTarget: "node",
|
||||
effectiveHost: "node",
|
||||
});
|
||||
});
|
||||
|
||||
it("routes to node for elevated when configured=node and no per-call override", () => {
|
||||
expect(
|
||||
resolveExecTarget({
|
||||
configuredTarget: "node",
|
||||
elevatedRequested: true,
|
||||
sandboxAvailable: false,
|
||||
}),
|
||||
).toMatchObject({
|
||||
configuredTarget: "node",
|
||||
requestedTarget: null,
|
||||
selectedTarget: "node",
|
||||
effectiveHost: "node",
|
||||
});
|
||||
});
|
||||
|
||||
it("silently discards mismatched requestedTarget under elevated+node", () => {
|
||||
// When elevated is requested and configuredTarget is "node", the elevated
|
||||
// path always returns "node" regardless of requestedTarget. This is
|
||||
// intentional — elevated overrides per-call host selection.
|
||||
expect(
|
||||
resolveExecTarget({
|
||||
configuredTarget: "node",
|
||||
requestedTarget: "gateway",
|
||||
elevatedRequested: true,
|
||||
sandboxAvailable: false,
|
||||
}),
|
||||
).toMatchObject({
|
||||
configuredTarget: "node",
|
||||
requestedTarget: "gateway",
|
||||
selectedTarget: "node",
|
||||
effectiveHost: "node",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("emitExecSystemEvent", () => {
|
||||
|
||||
@@ -220,11 +220,31 @@ export function renderExecTargetLabel(target: ExecTarget) {
|
||||
export function isRequestedExecTargetAllowed(params: {
|
||||
configuredTarget: ExecTarget;
|
||||
requestedTarget: ExecTarget;
|
||||
sandboxAvailable?: boolean;
|
||||
}) {
|
||||
// `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;
|
||||
// Exact match is always allowed (e.g. configured=node, requested=node).
|
||||
if (params.requestedTarget === params.configuredTarget) {
|
||||
return true;
|
||||
}
|
||||
// `auto` is a routing strategy that selects a host at runtime. Per-call
|
||||
// overrides to a concrete host are allowed so agents and directives can
|
||||
// pin individual commands to a specific target without requiring a global
|
||||
// config change.
|
||||
//
|
||||
// However, when a sandbox runtime is available the session is implicitly
|
||||
// sandboxed — allowing a per-call jump to gateway would bypass sandbox
|
||||
// confinement. Only overrides *to* sandbox (or node, which has its own
|
||||
// approval layer) are safe in that scenario. The ask/approval flow
|
||||
// remains the primary security gate for all non-sandbox hosts.
|
||||
if (params.configuredTarget === "auto") {
|
||||
if (params.sandboxAvailable && params.requestedTarget === "gateway") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Non-auto configured targets require an exact match to prevent silent
|
||||
// host-hopping (e.g. a node-pinned session should not route to gateway).
|
||||
return false;
|
||||
}
|
||||
|
||||
export function resolveExecTarget(params: {
|
||||
@@ -236,11 +256,17 @@ export function resolveExecTarget(params: {
|
||||
const configuredTarget = params.configuredTarget ?? "auto";
|
||||
const requestedTarget = params.requestedTarget ?? null;
|
||||
if (params.elevatedRequested) {
|
||||
// Elevated execution runs with host-level permissions. When the target is
|
||||
// explicitly pinned to "node", honour that binding — the node's own
|
||||
// approval/security layer handles elevated semantics on the remote host.
|
||||
// Only redirect to gateway when the configured target is auto/sandbox
|
||||
// (i.e. the intent is local elevated execution on the gateway machine).
|
||||
const elevatedTarget = configuredTarget === "node" ? ("node" as const) : ("gateway" as const);
|
||||
return {
|
||||
configuredTarget,
|
||||
requestedTarget,
|
||||
selectedTarget: "gateway" as const,
|
||||
effectiveHost: "gateway" as const,
|
||||
selectedTarget: elevatedTarget,
|
||||
effectiveHost: elevatedTarget,
|
||||
};
|
||||
}
|
||||
if (
|
||||
@@ -248,6 +274,7 @@ export function resolveExecTarget(params: {
|
||||
!isRequestedExecTargetAllowed({
|
||||
configuredTarget,
|
||||
requestedTarget,
|
||||
sandboxAvailable: params.sandboxAvailable,
|
||||
})
|
||||
) {
|
||||
throw new Error(
|
||||
|
||||
Reference in New Issue
Block a user