






















@@ -51,6 +51,38 @@ async function expectNoForwardedInvoke(hasInvoke: () => boolean): Promise<void>
5151expect(hasInvoke()).toBe(false);
5252}
535354+function parseInvokeParamsJSON(payload: unknown): Record<string, unknown> | null {
55+const obj = payload as { paramsJSON?: unknown };
56+const raw = typeof obj?.paramsJSON === "string" ? obj.paramsJSON : "";
57+return raw ? (JSON.parse(raw) as Record<string, unknown>) : null;
58+}
59+60+function createInvokeParamCapture() {
61+let invokeCount = 0;
62+let lastInvokeParams: Record<string, unknown> | null = null;
63+return {
64+count: () => invokeCount,
65+onInvoke: (payload: unknown) => {
66+invokeCount += 1;
67+lastInvokeParams = parseInvokeParamsJSON(payload);
68+},
69+waitForParams: async () => {
70+await vi.waitFor(
71+() => {
72+if (!lastInvokeParams) {
73+throw new Error("expected forwarded invoke params");
74+}
75+},
76+{
77+timeout: 5_000,
78+interval: 50,
79+},
80+);
81+return requireRecord(lastInvokeParams, "forwarded invoke params");
82+},
83+};
84+}
85+5486function requireNonEmptyString(value: string | null | undefined, label: string): string {
5587if (!value) {
5688throw new Error(`expected ${label}`);
@@ -126,6 +158,38 @@ async function requestAllowOnceApproval(
126158return approvalId;
127159}
128160161+function approvedSystemRunParams(
162+command: string[],
163+rawCommand: string,
164+runId: string,
165+extra: Record<string, unknown> = {},
166+): Record<string, unknown> {
167+return {
168+ command,
169+ rawCommand,
170+ runId,
171+approved: true,
172+approvalDecision: "allow-once",
173+ ...extra,
174+};
175+}
176+177+function approvedChatSystemRunParams(
178+context: ChatApprovalContext,
179+runId: string,
180+extra: Record<string, unknown> = {},
181+): Record<string, unknown> {
182+return approvedSystemRunParams(["echo", "chat"], "echo chat", runId, {
183+agentId: context.agentId,
184+sessionKey: context.sessionKey,
185+turnSourceChannel: context.turnSourceChannel,
186+turnSourceTo: context.turnSourceTo,
187+turnSourceAccountId: context.turnSourceAccountId,
188+turnSourceThreadId: context.turnSourceThreadId,
189+ ...extra,
190+});
191+}
192+129193type ChatApprovalContext = {
130194agentId: string;
131195sessionKey: string;
@@ -509,18 +573,8 @@ describe("node.invoke approval bypass", () => {
509573});
510574511575test("binds approvals to decision/device and blocks cross-device replay", async () => {
512-let invokeCount = 0;
513-let lastInvokeParams: Record<string, unknown> | null = null;
514-const node = await connectLinuxNode((payload) => {
515-invokeCount += 1;
516-const obj = payload as { paramsJSON?: unknown };
517-const raw = typeof obj?.paramsJSON === "string" ? obj.paramsJSON : "";
518-if (!raw) {
519-lastInvokeParams = null;
520-return;
521-}
522-lastInvokeParams = JSON.parse(raw) as Record<string, unknown>;
523-});
576+const invokeCapture = createInvokeParamCapture();
577+const node = await connectLinuxNode(invokeCapture.onInvoke);
524578525579const wsApprover = await connectOperator(["operator.write", "operator.approvals"]);
526580const wsCaller = await connectOperator(["operator.write"]);
@@ -534,50 +588,29 @@ describe("node.invoke approval bypass", () => {
534588const invoke = await rpcReq(wsCaller, "node.invoke", {
535589 nodeId,
536590command: "system.run",
537-params: {
538-command: ["echo", "hi"],
539-rawCommand: "echo hi",
540-runId: approvalId,
541-approved: true,
591+params: approvedSystemRunParams(["echo", "hi"], "echo hi", approvalId, {
542592approvalDecision: "allow-always",
543593injected: "nope",
544-},
594+}),
545595idempotencyKey: crypto.randomUUID(),
546596});
547597expect(invoke.ok).toBe(true);
548-await vi.waitFor(
549-() => {
550-if (!lastInvokeParams) {
551-throw new Error("expected forwarded invoke params");
552-}
553-},
554-{
555-timeout: 5_000,
556-interval: 50,
557-},
558-);
559-const forwardedParams = requireRecord(lastInvokeParams, "forwarded invoke params");
598+const forwardedParams = await invokeCapture.waitForParams();
560599expect(forwardedParams["approved"]).toBe(true);
561600expect(forwardedParams["approvalDecision"]).toBe("allow-once");
562601expect(forwardedParams["injected"]).toBeUndefined();
563602564603const replayApprovalId = await requestAllowOnceApproval(wsApprover, "echo hi", nodeId);
565-const invokeCountBeforeReplay = invokeCount;
604+const invokeCountBeforeReplay = invokeCapture.count();
566605const replay = await rpcReq(wsOtherDevice, "node.invoke", {
567606 nodeId,
568607command: "system.run",
569-params: {
570-command: ["echo", "hi"],
571-rawCommand: "echo hi",
572-runId: replayApprovalId,
573-approved: true,
574-approvalDecision: "allow-once",
575-},
608+params: approvedSystemRunParams(["echo", "hi"], "echo hi", replayApprovalId),
576609idempotencyKey: crypto.randomUUID(),
577610});
578611expect(replay.ok).toBe(false);
579612expect(replay.error?.message ?? "").toContain("not valid for this device");
580-await expectNoForwardedInvoke(() => invokeCount > invokeCountBeforeReplay);
613+await expectNoForwardedInvoke(() => invokeCapture.count() > invokeCountBeforeReplay);
581614} finally {
582615wsApprover.close();
583616wsCaller.close();
@@ -587,14 +620,8 @@ describe("node.invoke approval bypass", () => {
587620});
588621589622test("bridges no-device chat approvals across backend reconnects only for the same turn source", async () => {
590-let invokeCount = 0;
591-let lastInvokeParams: Record<string, unknown> | null = null;
592-const node = await connectLinuxNode((payload) => {
593-invokeCount += 1;
594-const obj = payload as { paramsJSON?: unknown };
595-const raw = typeof obj?.paramsJSON === "string" ? obj.paramsJSON : "";
596-lastInvokeParams = raw ? (JSON.parse(raw) as Record<string, unknown>) : null;
597-});
623+const invokeCapture = createInvokeParamCapture();
624+const node = await connectLinuxNode(invokeCapture.onInvoke);
598625599626const wsRequest = await connectTrustedBackend(["operator.write", "operator.approvals"]);
600627const wsReplay = await connectTrustedBackend(["operator.write", "operator.approvals"]);
@@ -619,34 +646,11 @@ describe("node.invoke approval bypass", () => {
619646const invoke = await rpcReq(wsReplay, "node.invoke", {
620647 nodeId,
621648command: "system.run",
622-params: {
623-command: ["echo", "chat"],
624-rawCommand: "echo chat",
625-agentId: context.agentId,
626-sessionKey: context.sessionKey,
627-turnSourceChannel: context.turnSourceChannel,
628-turnSourceTo: context.turnSourceTo,
629-turnSourceAccountId: context.turnSourceAccountId,
630-turnSourceThreadId: context.turnSourceThreadId,
631-runId: approvalId,
632-approved: true,
633-approvalDecision: "allow-once",
634-},
649+params: approvedChatSystemRunParams(context, approvalId),
635650idempotencyKey: crypto.randomUUID(),
636651});
637652expect(invoke.ok).toBe(true);
638-await vi.waitFor(
639-() => {
640-if (!lastInvokeParams) {
641-throw new Error("expected forwarded invoke params");
642-}
643-},
644-{
645-timeout: 5_000,
646-interval: 50,
647-},
648-);
649-const forwardedParams = requireRecord(lastInvokeParams, "forwarded invoke params");
653+const forwardedParams = await invokeCapture.waitForParams();
650654expect(forwardedParams["approved"]).toBe(true);
651655expect(forwardedParams["approvalDecision"]).toBe("allow-once");
652656expect(forwardedParams["turnSourceTo"]).toBeUndefined();
@@ -657,28 +661,18 @@ describe("node.invoke approval bypass", () => {
657661 nodeId,
658662 context,
659663});
660-const invokeCountBeforeMismatch = invokeCount;
664+const invokeCountBeforeMismatch = invokeCapture.count();
661665const mismatch = await rpcReq(wsReplay, "node.invoke", {
662666 nodeId,
663667command: "system.run",
664-params: {
665-command: ["echo", "chat"],
666-rawCommand: "echo chat",
667-agentId: context.agentId,
668-sessionKey: context.sessionKey,
669-turnSourceChannel: context.turnSourceChannel,
668+params: approvedChatSystemRunParams(context, mismatchApprovalId, {
670669turnSourceTo: "telegram:67890",
671-turnSourceAccountId: context.turnSourceAccountId,
672-turnSourceThreadId: context.turnSourceThreadId,
673-runId: mismatchApprovalId,
674-approved: true,
675-approvalDecision: "allow-once",
676-},
670+}),
677671idempotencyKey: crypto.randomUUID(),
678672});
679673expect(mismatch.ok).toBe(false);
680674expect(mismatch.error?.message ?? "").toContain("not valid for this client");
681-await expectNoForwardedInvoke(() => invokeCount > invokeCountBeforeMismatch);
675+await expectNoForwardedInvoke(() => invokeCapture.count() > invokeCountBeforeMismatch);
682676} finally {
683677wsRequest.close();
684678wsReplay.close();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。