























@@ -118,6 +118,83 @@ describe("Codex app-server approval bridge", () => {
118118);
119119});
120120121+it("describes command approval permission and policy amendments", async () => {
122+const params = createParams();
123+mockCallGatewayTool
124+.mockResolvedValueOnce({ id: "plugin:approval-command-permissions", status: "accepted" })
125+.mockResolvedValueOnce({
126+id: "plugin:approval-command-permissions",
127+decision: "allow-always",
128+});
129+130+const result = await handleCodexAppServerApprovalRequest({
131+method: "item/commandExecution/requestApproval",
132+requestParams: {
133+threadId: "thread-1",
134+turnId: "turn-1",
135+itemId: "cmd-permissions",
136+command: "npm install",
137+additionalPermissions: {
138+network: { enabled: true },
139+fileSystem: {
140+write: ["/"],
141+},
142+},
143+proposedExecpolicyAmendment: ["npm install"],
144+proposedNetworkPolicyAmendments: [{ host: "registry.npmjs.org", action: "allow" }],
145+},
146+paramsForRun: params,
147+threadId: "thread-1",
148+turnId: "turn-1",
149+});
150+151+expect(result).toEqual({ decision: "acceptForSession" });
152+const [, , requestPayload] = mockCallGatewayTool.mock.calls[0] ?? [];
153+const description = (requestPayload as { description: string }).description;
154+expect(description).toContain("Command: npm install");
155+expect(description).toContain("Additional permissions: network, fileSystem");
156+expect(description).toContain("High-risk targets: network access, filesystem root");
157+expect(description).toContain("Network enabled: true");
158+expect(description).toContain("File system write: /");
159+expect(description).toContain("Proposed exec policy: npm install");
160+expect(description).toContain("Proposed network policy: allow registry.npmjs.org");
161+});
162+163+it("keeps command approval permission details visible after long command previews", async () => {
164+const params = createParams();
165+mockCallGatewayTool
166+.mockResolvedValueOnce({ id: "plugin:approval-long-command-permissions", status: "accepted" })
167+.mockResolvedValueOnce({
168+id: "plugin:approval-long-command-permissions",
169+decision: "allow-always",
170+});
171+172+await handleCodexAppServerApprovalRequest({
173+method: "item/commandExecution/requestApproval",
174+requestParams: {
175+threadId: "thread-1",
176+turnId: "turn-1",
177+itemId: "cmd-long-permissions",
178+command: `${"npm install ".repeat(500)} --unsafe-perm`,
179+additionalPermissions: {
180+network: { enabled: true },
181+fileSystem: {
182+write: ["/"],
183+},
184+},
185+},
186+paramsForRun: params,
187+threadId: "thread-1",
188+turnId: "turn-1",
189+});
190+191+const [, , requestPayload] = mockCallGatewayTool.mock.calls[0] ?? [];
192+const description = (requestPayload as { description: string }).description;
193+expect(description).toContain("[preview truncated or unsafe content omitted]");
194+expect(description).toContain("Additional permissions: network, fileSystem");
195+expect(description).toContain("High-risk targets: network access, filesystem root");
196+});
197+121198it("sanitizes command previews before forwarding approval text and events", async () => {
122199const params = createParams();
123200mockCallGatewayTool
@@ -155,6 +232,44 @@ describe("Codex app-server approval bridge", () => {
155232);
156233});
157234235+it("escapes command approval previews before forwarding approval text and events", async () => {
236+const params = createParams();
237+mockCallGatewayTool
238+.mockResolvedValueOnce({ id: "plugin:approval-escaped-command", status: "accepted" })
239+.mockResolvedValueOnce({ id: "plugin:approval-escaped-command", decision: "allow-once" });
240+241+await handleCodexAppServerApprovalRequest({
242+method: "item/commandExecution/requestApproval",
243+requestParams: {
244+threadId: "thread-1",
245+turnId: "turn-1",
246+itemId: "cmd-escaped",
247+command: "printf '<@U123> [trusted](https://evil) @here'",
248+},
249+paramsForRun: params,
250+threadId: "thread-1",
251+turnId: "turn-1",
252+});
253+254+const [, , requestPayload] = mockCallGatewayTool.mock.calls[0] ?? [];
255+const description = (requestPayload as { description: string }).description;
256+expect(description).toContain(
257+"printf '<\uff20U123> \uff3btrusted\uff3d\uff08https://evil\uff09 \uff20here'",
258+);
259+expect(description).not.toContain("<@U123>");
260+expect(description).not.toContain("[trusted](https://evil)");
261+expect(description).not.toContain("@here");
262+expect(params.onAgentEvent).toHaveBeenCalledWith(
263+expect.objectContaining({
264+stream: "approval",
265+data: expect.objectContaining({
266+command:
267+"printf '<\uff20U123> \uff3btrusted\uff3d\uff08https://evil\uff09 \uff20here'",
268+}),
269+}),
270+);
271+});
272+158273it("preserves visible OSC-8 link labels in command previews", async () => {
159274const params = createParams();
160275mockCallGatewayTool
@@ -615,6 +730,59 @@ describe("Codex app-server approval bridge", () => {
615730expect(description).toContain("readPaths: ~/.ssh/id_rsa, /etc/hosts");
616731});
617732733+it("describes current protocol network and filesystem permission grants", async () => {
734+const params = createParams();
735+mockCallGatewayTool
736+.mockResolvedValueOnce({ id: "plugin:approval-current-permissions", status: "accepted" })
737+.mockResolvedValueOnce({ id: "plugin:approval-current-permissions", decision: "allow-once" });
738+739+const result = await handleCodexAppServerApprovalRequest({
740+method: "item/permissions/requestApproval",
741+requestParams: {
742+threadId: "thread-1",
743+turnId: "turn-1",
744+itemId: "perm-current",
745+permissions: {
746+network: { enabled: true },
747+fileSystem: {
748+read: ["/Users/simone/.ssh/id_rsa"],
749+write: ["/"],
750+entries: [
751+{ path: "/workspace/project", access: "read" },
752+{ path: "/tmp/output", access: "write" },
753+{ path: "/ignored", access: "none" },
754+],
755+},
756+},
757+},
758+paramsForRun: params,
759+threadId: "thread-1",
760+turnId: "turn-1",
761+});
762+763+expect(result).toEqual({
764+permissions: {
765+network: { enabled: true },
766+fileSystem: {
767+read: ["/Users/simone/.ssh/id_rsa"],
768+write: ["/"],
769+entries: [
770+{ path: "/workspace/project", access: "read" },
771+{ path: "/tmp/output", access: "write" },
772+{ path: "/ignored", access: "none" },
773+],
774+},
775+},
776+scope: "turn",
777+});
778+const [, , requestPayload] = mockCallGatewayTool.mock.calls[0] ?? [];
779+const description = (requestPayload as { description: string }).description;
780+expect(description).toContain("Network enabled: true");
781+expect(description).toContain("File system read: ~/.ssh/id_rsa; write: /");
782+expect(description).toContain("entries: read /workspace/project, write /tmp/output (+1 more)");
783+expect(description).toContain("High-risk targets: network access, filesystem root");
784+});
785+618786it("compacts Windows home paths in permission descriptions", async () => {
619787const params = createParams();
620788mockCallGatewayTool
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。