


























@@ -89,6 +89,24 @@ function buildCurrentCodexApprovalElicitation() {
8989};
9090}
919192+function buildComputerUseApprovalElicitation(overrides: Record<string, unknown> = {}) {
93+return {
94+threadId: "thread-1",
95+turnId: "turn-1",
96+serverName: "computer-use",
97+mode: "form",
98+message: "Allow Codex to use Notes?",
99+_meta: {
100+persist: ["always"],
101+},
102+requestedSchema: {
103+type: "object",
104+properties: {},
105+},
106+ ...overrides,
107+};
108+}
109+92110function buildPluginApprovalElicitation(overrides: Record<string, unknown> = {}) {
93111return {
94112threadId: "thread-1",
@@ -290,6 +308,201 @@ describe("Codex app-server elicitation bridge", () => {
290308expect(approvalRequest.description).toContain("Repository: openclaw/openclaw");
291309});
292310311+it("routes Computer Use app approvals through plugin approvals", async () => {
312+mockCallGatewayTool
313+.mockResolvedValueOnce({ id: "plugin:approval-computer-use", status: "accepted" })
314+.mockResolvedValueOnce({ id: "plugin:approval-computer-use", decision: "allow-once" });
315+316+const result = await handleCodexAppServerElicitationRequest({
317+requestParams: buildComputerUseApprovalElicitation(),
318+paramsForRun: createParams(),
319+threadId: "thread-1",
320+turnId: "turn-1",
321+pluginAppPolicyContext: createPluginAppPolicyContext({ apps: [] }),
322+computerUseMcpServerName: "computer-use",
323+});
324+325+expect(result).toEqual({
326+action: "accept",
327+content: null,
328+_meta: null,
329+});
330+expect(mockCallGatewayTool.mock.calls.map(([method]) => method)).toEqual([
331+"plugin.approval.request",
332+"plugin.approval.waitDecision",
333+]);
334+});
335+336+it("maps Computer Use allow-always decisions onto persistent metadata", async () => {
337+mockCallGatewayTool
338+.mockResolvedValueOnce({ id: "plugin:approval-computer-use-always", status: "accepted" })
339+.mockResolvedValueOnce({
340+id: "plugin:approval-computer-use-always",
341+decision: "allow-always",
342+});
343+344+const result = await handleCodexAppServerElicitationRequest({
345+requestParams: buildComputerUseApprovalElicitation(),
346+paramsForRun: createParams(),
347+threadId: "thread-1",
348+turnId: "turn-1",
349+pluginAppPolicyContext: createPluginAppPolicyContext({ apps: [] }),
350+computerUseMcpServerName: "computer-use",
351+});
352+353+expect(result).toEqual({
354+action: "accept",
355+content: null,
356+_meta: {
357+persist: "always",
358+},
359+});
360+});
361+362+it("does not handle non-Computer Use elicitations without approval metadata", async () => {
363+const result = await handleCodexAppServerElicitationRequest({
364+requestParams: buildComputerUseApprovalElicitation({ serverName: "desktop-control" }),
365+paramsForRun: createParams(),
366+threadId: "thread-1",
367+turnId: "turn-1",
368+pluginAppPolicyContext: createPluginAppPolicyContext({ apps: [] }),
369+computerUseMcpServerName: "computer-use",
370+});
371+372+expect(result).toBeUndefined();
373+expect(mockCallGatewayTool).not.toHaveBeenCalled();
374+});
375+376+it("routes configured custom Computer Use server names through plugin approvals", async () => {
377+mockCallGatewayTool
378+.mockResolvedValueOnce({ id: "plugin:approval-custom-computer-use", status: "accepted" })
379+.mockResolvedValueOnce({
380+id: "plugin:approval-custom-computer-use",
381+decision: "allow-once",
382+});
383+384+const result = await handleCodexAppServerElicitationRequest({
385+requestParams: buildComputerUseApprovalElicitation({ serverName: "desktop-control" }),
386+paramsForRun: createParams(),
387+threadId: "thread-1",
388+turnId: "turn-1",
389+pluginAppPolicyContext: createPluginAppPolicyContext({ apps: [] }),
390+computerUseMcpServerName: "desktop-control",
391+});
392+393+expect(result).toEqual({
394+action: "accept",
395+content: null,
396+_meta: null,
397+});
398+const approvalRequest = gatewayToolArg(0, 2) as { description: string };
399+expect(approvalRequest.description).toContain("MCP server: desktop-control");
400+});
401+402+it("declines approved Computer Use app approvals with unmappable non-empty schemas", async () => {
403+const warnSpy = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
404+mockCallGatewayTool
405+.mockResolvedValueOnce({ id: "plugin:approval-computer-use-fields", status: "accepted" })
406+.mockResolvedValueOnce({ id: "plugin:approval-computer-use-fields", decision: "allow-once" });
407+408+const result = await handleCodexAppServerElicitationRequest({
409+requestParams: buildComputerUseApprovalElicitation({
410+requestedSchema: {
411+type: "object",
412+properties: {
413+appName: {
414+type: "string",
415+title: "App name",
416+},
417+},
418+required: ["appName"],
419+},
420+}),
421+paramsForRun: createParams(),
422+threadId: "thread-1",
423+turnId: "turn-1",
424+pluginAppPolicyContext: createPluginAppPolicyContext({ apps: [] }),
425+computerUseMcpServerName: "computer-use",
426+});
427+428+expect(result).toEqual({ action: "decline", content: null, _meta: null });
429+expect(mockCallGatewayTool.mock.calls.map(([method]) => method)).toEqual([
430+"plugin.approval.request",
431+"plugin.approval.waitDecision",
432+]);
433+expect(warnSpy).toHaveBeenCalledWith(
434+"codex MCP approval elicitation approved without a mappable response",
435+expect.objectContaining({
436+fields: ["appName"],
437+outcome: "approved-once",
438+}),
439+);
440+});
441+442+it("does not bridge Computer Use elicitations without an approval form schema", async () => {
443+const result = await handleCodexAppServerElicitationRequest({
444+requestParams: buildComputerUseApprovalElicitation({
445+requestedSchema: "not-a-schema",
446+}),
447+paramsForRun: createParams(),
448+threadId: "thread-1",
449+turnId: "turn-1",
450+pluginAppPolicyContext: createPluginAppPolicyContext({ apps: [] }),
451+computerUseMcpServerName: "computer-use",
452+});
453+454+expect(result).toBeUndefined();
455+expect(mockCallGatewayTool).not.toHaveBeenCalled();
456+});
457+458+it("does not bridge Computer Use elicitations outside form mode", async () => {
459+const result = await handleCodexAppServerElicitationRequest({
460+requestParams: buildComputerUseApprovalElicitation({
461+mode: "notification",
462+}),
463+paramsForRun: createParams(),
464+threadId: "thread-1",
465+turnId: "turn-1",
466+pluginAppPolicyContext: createPluginAppPolicyContext({ apps: [] }),
467+computerUseMcpServerName: "computer-use",
468+});
469+470+expect(result).toBeUndefined();
471+expect(mockCallGatewayTool).not.toHaveBeenCalled();
472+});
473+474+it("falls back to a Computer Use approval title and sanitizes server names", async () => {
475+mockCallGatewayTool
476+.mockResolvedValueOnce({ id: "plugin:approval-computer-use-title", status: "accepted" })
477+.mockResolvedValueOnce({ id: "plugin:approval-computer-use-title", decision: "allow-once" });
478+479+const result = await handleCodexAppServerElicitationRequest({
480+requestParams: buildComputerUseApprovalElicitation({
481+message: "\u001b[31m",
482+serverName: "computer-use\u009b31m",
483+_meta: null,
484+}),
485+paramsForRun: createParams(),
486+threadId: "thread-1",
487+turnId: "turn-1",
488+pluginAppPolicyContext: createPluginAppPolicyContext({ apps: [] }),
489+computerUseMcpServerName: "computer-use\u009b31m",
490+});
491+492+expect(result).toEqual({
493+action: "accept",
494+content: null,
495+_meta: null,
496+});
497+const approvalRequest = gatewayToolArg(0, 2) as {
498+title: string;
499+description: string;
500+};
501+expect(approvalRequest.title).toBe("Computer Use approval");
502+expect(approvalRequest.description).toContain("MCP server: computer-use");
503+expect(approvalRequest.description).not.toContain("\u009b");
504+});
505+293506it("strips control and invisible formatting from approval display text", async () => {
294507mockCallGatewayTool
295508.mockResolvedValueOnce({ id: "plugin:approval-sanitized", status: "accepted" })
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。