






























@@ -38,6 +38,18 @@ const evaluateShellAllowlistMock = vi.hoisted(() =>
3838segmentAllowlistEntries: [],
3939})),
4040);
41+const resolveExecApprovalsFromFileMock = vi.hoisted(() =>
42+vi.fn(() => ({
43+allowlist: [],
44+file: { version: 1, agents: {} },
45+agent: {
46+security: "full",
47+ask: "off",
48+askFallback: "deny",
49+autoAllowSkills: false,
50+},
51+})),
52+);
4153const requiresExecApprovalMock = vi.hoisted(() => vi.fn(() => true));
4254const resolveExecHostApprovalContextMock = vi.hoisted(() =>
4355vi.fn(() => ({
@@ -93,10 +105,7 @@ vi.mock("../infra/exec-approvals.js", () => ({
93105hasDurableExecApproval: vi.fn(() => false),
94106requiresExecApproval: requiresExecApprovalMock,
95107resolveExecApprovalAllowedDecisions: vi.fn(() => ["allow-once", "allow-always", "deny"]),
96-resolveExecApprovalsFromFile: vi.fn(() => ({
97-allowlist: [],
98-file: { version: 1, agents: {} },
99-})),
108+resolveExecApprovalsFromFile: resolveExecApprovalsFromFileMock,
100109}));
101110102111vi.mock("../infra/command-analysis/inline-eval.js", () => ({
@@ -275,6 +284,17 @@ describe("executeNodeHostCommand", () => {
275284segments: [{ resolution: null, argv: ["bun", "./script.ts"] }],
276285segmentAllowlistEntries: [],
277286});
287+resolveExecApprovalsFromFileMock.mockReset();
288+resolveExecApprovalsFromFileMock.mockReturnValue({
289+allowlist: [],
290+file: { version: 1, agents: {} },
291+agent: {
292+security: "full",
293+ask: "off",
294+askFallback: "deny",
295+autoAllowSkills: false,
296+},
297+});
278298requiresExecApprovalMock.mockReset();
279299requiresExecApprovalMock.mockReturnValue(true);
280300resolveExecHostApprovalContextMock.mockReset();
@@ -460,6 +480,94 @@ describe("executeNodeHostCommand", () => {
460480expect(warnings.join("\n")).toContain("needs a person");
461481});
462482483+it.each([
484+{
485+name: "ask always",
486+nodeSecurity: "full",
487+nodeAsk: "always",
488+},
489+{
490+name: "deny security",
491+nodeSecurity: "deny",
492+nodeAsk: "off",
493+},
494+] as const)(
495+"requests human approval when node policy has $name floor",
496+async ({ nodeSecurity, nodeAsk }) => {
497+const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
498+decision: "allow-once",
499+risk: "low",
500+rationale: "test reviewer would allow it",
501+}));
502+resolveExecHostApprovalContextMock.mockReturnValue({
503+approvals: { allowlist: [], file: { version: 1, agents: {} } },
504+hostSecurity: "allowlist",
505+hostAsk: "on-miss",
506+askFallback: "deny",
507+});
508+resolveExecApprovalsFromFileMock.mockReturnValue({
509+allowlist: [],
510+file: { version: 1, agents: {} },
511+agent: {
512+security: nodeSecurity,
513+ask: nodeAsk,
514+askFallback: "deny",
515+autoAllowSkills: false,
516+},
517+});
518+callGatewayToolMock.mockImplementation(
519+async (method: string, _options: unknown, params: MockNodeInvokeParams | undefined) => {
520+if (method === "exec.approvals.node.get") {
521+return { file: { version: 1, agents: {} } };
522+}
523+if (method === "exec.approval.resolve") {
524+return { payload: {} };
525+}
526+if (method !== "node.invoke") {
527+throw new Error(`unexpected gateway method: ${method}`);
528+}
529+if (params?.command === "system.run.prepare") {
530+return { payload: { plan: preparedPlan } };
531+}
532+if (params?.command === "system.run") {
533+return {
534+payload: {
535+success: true,
536+stdout: "ok",
537+stderr: "",
538+exitCode: 0,
539+timedOut: false,
540+},
541+};
542+}
543+throw new Error(`unexpected node invoke command: ${String(params?.command)}`);
544+},
545+);
546+547+const result = await executeNodeHostCommand({
548+command: "bun ./script.ts",
549+workdir: "/tmp/work",
550+env: {},
551+security: "allowlist",
552+ask: "on-miss",
553+autoReview: true,
554+ autoReviewer,
555+defaultTimeoutSec: 30,
556+approvalRunningNoticeMs: 0,
557+warnings: [],
558+agentId: "requested-agent",
559+sessionKey: "requested-session",
560+});
561+562+expect(result.details?.status).toBe("approval-pending");
563+expect(autoReviewer).not.toHaveBeenCalled();
564+expect(createAndRegisterDefaultExecApprovalRequestMock).toHaveBeenCalledTimes(1);
565+expect(
566+callGatewayToolMock.mock.calls.some(([method]) => method === "exec.approval.resolve"),
567+).toBe(false);
568+},
569+);
570+463571it("auto-reviews strict inline-eval commands before asking a human", async () => {
464572const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
465573decision: "allow-once",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。