fix(agents): resolve exec host=node routing regression and elevated gateway override

This commit is contained in:
openperf
2026-04-04 17:39:11 +08:00
committed by Ayaan Zaidi
parent 17f086c021
commit d98eaba4c3
2 changed files with 136 additions and 11 deletions

View File

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

View File

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