fix(exec): inherit ask from exec-approvals.json when tools.exec.ask unset

Landed from contributor PR #29187 by @Bartok9.

Co-authored-by: Bartok9 <259807879+Bartok9@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-03-08 00:34:55 +00:00
parent 240b143bde
commit c6575891c7
5 changed files with 105 additions and 12 deletions

View File

@@ -1,5 +1,6 @@
import { Command } from "commander";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { ExecApprovalsFile } from "../infra/exec-approvals.js";
import { buildSystemRunPreparePayload } from "../test-utils/system-run-prepare-payload.js";
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
@@ -15,6 +16,16 @@ type NodeInvokeCall = {
let lastNodeInvokeCall: NodeInvokeCall | null = null;
let lastApprovalRequestCall: { params?: Record<string, unknown> } | null = null;
let localExecApprovalsFile: ExecApprovalsFile = { version: 1, agents: {} };
let nodeExecApprovalsFile: ExecApprovalsFile = {
version: 1,
defaults: {
security: "allowlist",
ask: "on-miss",
askFallback: "deny",
},
agents: {},
};
const callGateway = vi.fn(async (opts: NodeInvokeCall) => {
if (opts.method === "node.list") {
@@ -58,15 +69,7 @@ const callGateway = vi.fn(async (opts: NodeInvokeCall) => {
path: "/tmp/exec-approvals.json",
exists: true,
hash: "hash",
file: {
version: 1,
defaults: {
security: "allowlist",
ask: "on-miss",
askFallback: "deny",
},
agents: {},
},
file: nodeExecApprovalsFile,
};
}
if (opts.method === "exec.approval.request") {
@@ -93,6 +96,16 @@ vi.mock("../config/config.js", () => ({
loadConfig: () => ({}),
}));
vi.mock("../infra/exec-approvals.js", async () => {
const actual = await vi.importActual<typeof import("../infra/exec-approvals.js")>(
"../infra/exec-approvals.js",
);
return {
...actual,
loadExecApprovals: () => localExecApprovalsFile,
};
});
describe("nodes-cli coverage", () => {
let registerNodesCli: (program: Command) => void;
let sharedProgram: Command;
@@ -125,6 +138,16 @@ describe("nodes-cli coverage", () => {
randomIdempotencyKey.mockClear();
lastNodeInvokeCall = null;
lastApprovalRequestCall = null;
localExecApprovalsFile = { version: 1, agents: {} };
nodeExecApprovalsFile = {
version: 1,
defaults: {
security: "allowlist",
ask: "on-miss",
askFallback: "deny",
},
agents: {},
};
});
it("invokes system.run with parsed params", async () => {
@@ -207,6 +230,37 @@ describe("nodes-cli coverage", () => {
});
});
it("inherits ask=off from local exec approvals when tools.exec.ask is unset", async () => {
localExecApprovalsFile = {
version: 1,
defaults: {
security: "allowlist",
ask: "off",
askFallback: "deny",
},
agents: {},
};
nodeExecApprovalsFile = {
version: 1,
defaults: {
security: "allowlist",
askFallback: "deny",
},
agents: {},
};
const invoke = await runNodesCommand(["nodes", "run", "--node", "mac-1", "echo", "hi"]);
expect(invoke).toBeTruthy();
expect(invoke?.params?.command).toBe("system.run");
expect(invoke?.params?.params).toMatchObject({
command: ["echo", "hi"],
approved: false,
});
expect(invoke?.params?.params).not.toHaveProperty("approvalDecision");
expect(getApprovalRequestCall()).toBeNull();
});
it("invokes system.notify with provided fields", async () => {
const invoke = await runNodesCommand([
"nodes",