diff --git a/src/agents/bash-tools.exec-runtime.test.ts b/src/agents/bash-tools.exec-runtime.test.ts index c1ddf68e397..c5e7784214a 100644 --- a/src/agents/bash-tools.exec-runtime.test.ts +++ b/src/agents/bash-tools.exec-runtime.test.ts @@ -221,6 +221,22 @@ describe("resolveExecTarget", () => { }); }); + it("keeps explicit node override under elevated requests when configured target is auto", () => { + expect( + resolveExecTarget({ + configuredTarget: "auto", + requestedTarget: "node", + elevatedRequested: true, + sandboxAvailable: false, + }), + ).toMatchObject({ + configuredTarget: "auto", + requestedTarget: "node", + selectedTarget: "node", + effectiveHost: "node", + }); + }); + it("honours node target for elevated requests when configured target is node", () => { expect( resolveExecTarget({ @@ -252,20 +268,17 @@ describe("resolveExecTarget", () => { }); }); - it("silently discards mismatched requestedTarget under elevated+node", () => { - expect( + it("rejects mismatched requestedTarget under elevated+node", () => { + expect(() => resolveExecTarget({ configuredTarget: "node", requestedTarget: "gateway", elevatedRequested: true, sandboxAvailable: false, }), - ).toMatchObject({ - configuredTarget: "node", - requestedTarget: "gateway", - selectedTarget: "node", - effectiveHost: "node", - }); + ).toThrow( + "exec host not allowed (requested gateway; configured host is node; set tools.exec.host=gateway or auto to allow this override).", + ); }); }); diff --git a/src/agents/bash-tools.exec-runtime.ts b/src/agents/bash-tools.exec-runtime.ts index 0c019d85077..ff7ddd67669 100644 --- a/src/agents/bash-tools.exec-runtime.ts +++ b/src/agents/bash-tools.exec-runtime.ts @@ -242,15 +242,6 @@ export function resolveExecTarget(params: { }) { const configuredTarget = params.configuredTarget ?? "auto"; const requestedTarget = params.requestedTarget ?? null; - if (params.elevatedRequested) { - const elevatedTarget = configuredTarget === "node" ? ("node" as const) : ("gateway" as const); - return { - configuredTarget, - requestedTarget, - selectedTarget: elevatedTarget, - effectiveHost: elevatedTarget, - }; - } if ( requestedTarget && !isRequestedExecTargetAllowed({ @@ -273,12 +264,17 @@ export function resolveExecTarget(params: { ); } const selectedTarget = requestedTarget ?? configuredTarget; + const resolvedTarget = params.elevatedRequested + ? selectedTarget === "node" + ? "node" + : "gateway" + : selectedTarget; const effectiveHost = - selectedTarget === "auto" ? (params.sandboxAvailable ? "sandbox" : "gateway") : selectedTarget; + resolvedTarget === "auto" ? (params.sandboxAvailable ? "sandbox" : "gateway") : resolvedTarget; return { configuredTarget, requestedTarget, - selectedTarget, + selectedTarget: resolvedTarget, effectiveHost, }; } diff --git a/src/agents/bash-tools.exec.approval-id.test.ts b/src/agents/bash-tools.exec.approval-id.test.ts index ccc5b336818..39f8592f40b 100644 --- a/src/agents/bash-tools.exec.approval-id.test.ts +++ b/src/agents/bash-tools.exec.approval-id.test.ts @@ -458,6 +458,41 @@ describe("exec approvals", () => { expect(prepareCwd).toBeUndefined(); }); + it("routes explicit host=node to node invoke when elevated default is on under auto host", async () => { + const calls: string[] = []; + + vi.mocked(callGatewayTool).mockImplementation(async (method, _opts, params) => { + calls.push(method); + if (method === "node.invoke") { + const invoke = params as { command?: string }; + if (invoke.command === "system.run.prepare") { + return buildPreparedSystemRunPayload(params); + } + if (invoke.command === "system.run") { + return { payload: { success: true, stdout: "node-ok" } }; + } + } + return { ok: true }; + }); + + const tool = createExecTool({ + host: "auto", + ask: "off", + security: "full", + approvalRunningNoticeMs: 0, + elevated: { enabled: true, allowed: true, defaultLevel: "on" }, + }); + + const result = await tool.execute("call-auto-node-elevated-default", { + command: "echo gateway-ok", + host: "node", + }); + + expect(result.details.status).toBe("completed"); + expect(getResultText(result)).toContain("node-ok"); + expect(calls).toContain("node.invoke"); + }); + it("honors ask=off for elevated gateway exec without prompting", async () => { const calls: string[] = []; vi.mocked(callGatewayTool).mockImplementation(async (method) => {