




















@@ -241,6 +241,9 @@ describe("executeNodeHostCommand", () => {
241241callGatewayToolMock.mockReset();
242242callGatewayToolMock.mockImplementation(
243243async (method: string, _options: unknown, params: MockNodeInvokeParams | undefined) => {
244+if (method === "exec.approvals.node.get") {
245+return { file: { version: 1, agents: {} } };
246+}
244247if (method === "exec.approval.resolve") {
245248return { payload: {} };
246249}
@@ -568,6 +571,151 @@ describe("executeNodeHostCommand", () => {
568571},
569572);
570573574+it("requests human approval when node approval policy is unavailable", async () => {
575+const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
576+decision: "allow-once",
577+risk: "low",
578+rationale: "test reviewer would allow it",
579+}));
580+resolveExecHostApprovalContextMock.mockReturnValue({
581+approvals: { allowlist: [], file: { version: 1, agents: {} } },
582+hostSecurity: "allowlist",
583+hostAsk: "on-miss",
584+askFallback: "deny",
585+});
586+callGatewayToolMock.mockImplementation(
587+async (method: string, _options: unknown, params: MockNodeInvokeParams | undefined) => {
588+if (method === "exec.approvals.node.get") {
589+throw new Error("node approvals unavailable");
590+}
591+if (method === "exec.approval.resolve") {
592+return { payload: {} };
593+}
594+if (method !== "node.invoke") {
595+throw new Error(`unexpected gateway method: ${method}`);
596+}
597+if (params?.command === "system.run.prepare") {
598+return { payload: { plan: preparedPlan } };
599+}
600+if (params?.command === "system.run") {
601+return {
602+payload: {
603+success: true,
604+stdout: "ok",
605+stderr: "",
606+exitCode: 0,
607+timedOut: false,
608+},
609+};
610+}
611+throw new Error(`unexpected node invoke command: ${String(params?.command)}`);
612+},
613+);
614+615+const result = await executeNodeHostCommand({
616+command: "bun ./script.ts",
617+workdir: "/tmp/work",
618+env: {},
619+security: "allowlist",
620+ask: "on-miss",
621+autoReview: true,
622+ autoReviewer,
623+defaultTimeoutSec: 30,
624+approvalRunningNoticeMs: 0,
625+warnings: [],
626+agentId: "requested-agent",
627+sessionKey: "requested-session",
628+});
629+630+expect(result.details?.status).toBe("approval-pending");
631+expect(autoReviewer).not.toHaveBeenCalled();
632+expect(createAndRegisterDefaultExecApprovalRequestMock).toHaveBeenCalledTimes(1);
633+expect(
634+callGatewayToolMock.mock.calls.some(([method]) => method === "exec.approval.resolve"),
635+).toBe(false);
636+});
637+638+it("does not use fallback-full when node approval policy is unavailable", async () => {
639+const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
640+decision: "allow-once",
641+risk: "low",
642+rationale: "test reviewer would allow it",
643+}));
644+resolveExecHostApprovalContextMock.mockReturnValue({
645+approvals: { allowlist: [], file: { version: 1, agents: {} } },
646+hostSecurity: "allowlist",
647+hostAsk: "on-miss",
648+askFallback: "full",
649+});
650+callGatewayToolMock.mockImplementation(
651+async (method: string, _options: unknown, params: MockNodeInvokeParams | undefined) => {
652+if (method === "exec.approvals.node.get") {
653+throw new Error("node approvals unavailable");
654+}
655+if (method !== "node.invoke") {
656+throw new Error(`unexpected gateway method: ${method}`);
657+}
658+if (params?.command === "system.run.prepare") {
659+return { payload: { plan: preparedPlan } };
660+}
661+if (params?.command === "system.run") {
662+return {
663+payload: {
664+success: true,
665+stdout: "should-not-run",
666+stderr: "",
667+exitCode: 0,
668+timedOut: false,
669+},
670+};
671+}
672+throw new Error(`unexpected node invoke command: ${String(params?.command)}`);
673+},
674+);
675+resolveApprovalDecisionOrUndefinedMock.mockResolvedValue(null);
676+createExecApprovalDecisionStateMock.mockReturnValue({
677+baseDecision: { timedOut: true },
678+approvedByAsk: true,
679+deniedReason: null,
680+});
681+enforceStrictInlineEvalApprovalBoundaryMock.mockImplementation((value) =>
682+value.requiresAutoReviewHumanApproval === true && value.baseDecision.timedOut
683+ ? { approvedByAsk: false, deniedReason: "approval-timeout" }
684+ : { approvedByAsk: value.approvedByAsk, deniedReason: value.deniedReason },
685+);
686+687+const result = await executeNodeHostCommand({
688+command: "bun ./script.ts",
689+workdir: "/tmp/work",
690+env: {},
691+security: "allowlist",
692+ask: "on-miss",
693+autoReview: true,
694+ autoReviewer,
695+defaultTimeoutSec: 30,
696+approvalRunningNoticeMs: 0,
697+warnings: [],
698+agentId: "requested-agent",
699+sessionKey: "requested-session",
700+});
701+702+expect(result.details?.status).toBe("approval-pending");
703+expect(autoReviewer).not.toHaveBeenCalled();
704+await vi.waitFor(() => {
705+expect(sendExecApprovalFollowupResultMock).toHaveBeenCalledWith(
706+{ approvalId: "approval-1" },
707+"Exec denied (node=node-1 id=approval-1, approval-timeout): bun ./script.ts",
708+);
709+});
710+expect(
711+callGatewayToolMock.mock.calls.some(
712+([method, , params]) =>
713+method === "node.invoke" &&
714+(params as MockNodeInvokeParams | undefined)?.command === "system.run",
715+),
716+).toBe(false);
717+});
718+571719it("auto-reviews strict inline-eval commands before asking a human", async () => {
572720const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
573721decision: "allow-once",
@@ -905,6 +1053,65 @@ describe("executeNodeHostCommand", () => {
9051053expectSystemRunInvoke({ invokeTimeoutMs: 35_000, runTimeoutMs: 0 });
9061054});
90710551056+it("auto-reviews strict inline-eval commands with full/off host policy when node policy is available", async () => {
1057+const autoReviewer = vi.fn<ExecAutoReviewer>(async () => ({
1058+decision: "allow-once",
1059+risk: "low",
1060+rationale: "safe inline eval",
1061+}));
1062+detectInterpreterInlineEvalArgvMock.mockReturnValue(INLINE_EVAL_HIT);
1063+evaluateShellAllowlistMock.mockReturnValue({
1064+allowlistMatches: [],
1065+analysisOk: true,
1066+allowlistSatisfied: false,
1067+segments: [
1068+{
1069+resolution: null,
1070+argv: ["python3", "-c", "print(1)"],
1071+raw: "python3 -c 'print(1)'",
1072+},
1073+],
1074+segmentAllowlistEntries: [],
1075+});
1076+resolveExecHostApprovalContextMock.mockReturnValue({
1077+approvals: { allowlist: [], file: { version: 1, agents: {} } },
1078+hostSecurity: "full",
1079+hostAsk: "off",
1080+askFallback: "deny",
1081+});
1082+1083+const result = await executeNodeHostCommand({
1084+command: "python3 -c 'print(1)'",
1085+workdir: "/tmp/work",
1086+env: {},
1087+security: "full",
1088+ask: "off",
1089+autoReview: true,
1090+ autoReviewer,
1091+strictInlineEval: true,
1092+defaultTimeoutSec: 30,
1093+approvalRunningNoticeMs: 0,
1094+warnings: [],
1095+agentId: "requested-agent",
1096+sessionKey: "requested-session",
1097+});
1098+1099+expect(result.details?.status).toBe("completed");
1100+expect(autoReviewer).toHaveBeenCalledWith(
1101+expect.objectContaining({
1102+command: "python3 -c 'print(1)'",
1103+argv: ["python3", "-c", "print(1)"],
1104+host: "node",
1105+reason: "strict-inline-eval",
1106+}),
1107+);
1108+expect(callGatewayToolMock).toHaveBeenCalledWith(
1109+"exec.approvals.node.get",
1110+{ timeoutMs: 10_000 },
1111+{ nodeId: "node-1" },
1112+);
1113+});
1114+9081115it("denies timed-out inline-eval requests instead of invoking the node", async () => {
9091116detectInterpreterInlineEvalArgvMock.mockReturnValue(INLINE_EVAL_HIT);
9101117resolveApprovalDecisionOrUndefinedMock.mockResolvedValue(null);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。