

























@@ -119,6 +119,185 @@ describe("Codex app-server approval bridge", () => {
119119);
120120});
121121122+it("sanitizes command previews before forwarding approval text and events", async () => {
123+const params = createParams();
124+mockCallGatewayTool.mockResolvedValueOnce({
125+id: "plugin:approval-sanitized-command",
126+decision: "allow-once",
127+});
128+129+await handleCodexAppServerApprovalRequest({
130+method: "item/commandExecution/requestApproval",
131+requestParams: {
132+threadId: "thread-1",
133+turnId: "turn-1",
134+itemId: "cmd-sanitized",
135+command: ["pnpm", "test\n--watch", "\u001b[31mextensions/codex/src/app-server\u001b[0m"],
136+},
137+paramsForRun: params,
138+threadId: "thread-1",
139+turnId: "turn-1",
140+});
141+142+const [, , requestPayload] = mockCallGatewayTool.mock.calls[0] ?? [];
143+expect(requestPayload).toEqual(
144+expect.objectContaining({
145+description:
146+"Command: pnpm test --watch extensions/codex/src/app-server\nSession: agent:main:session-1",
147+}),
148+);
149+expect(params.onAgentEvent).toHaveBeenCalledWith(
150+expect.objectContaining({
151+stream: "approval",
152+data: expect.objectContaining({
153+status: "pending",
154+command: "pnpm test --watch extensions/codex/src/app-server",
155+}),
156+}),
157+);
158+});
159+160+it("preserves visible OSC-8 link labels in command previews", async () => {
161+const params = createParams();
162+mockCallGatewayTool.mockResolvedValueOnce({
163+id: "plugin:approval-osc",
164+decision: "allow-once",
165+});
166+const esc = "\u001b";
167+168+await handleCodexAppServerApprovalRequest({
169+method: "item/commandExecution/requestApproval",
170+requestParams: {
171+threadId: "thread-1",
172+turnId: "turn-1",
173+itemId: "cmd-osc",
174+command: `prefix ${esc}]8;;https://example.com${esc}\\VISIBLE${esc}]8;;${esc}\\ suffix`,
175+},
176+paramsForRun: params,
177+threadId: "thread-1",
178+turnId: "turn-1",
179+});
180+181+const [, , requestPayload] = mockCallGatewayTool.mock.calls[0] ?? [];
182+expect(requestPayload).toEqual(
183+expect.objectContaining({
184+description: "Command: prefix VISIBLE suffix\nSession: agent:main:session-1",
185+}),
186+);
187+expect(params.onAgentEvent).toHaveBeenCalledWith(
188+expect.objectContaining({
189+stream: "approval",
190+data: expect.objectContaining({ command: "prefix VISIBLE suffix" }),
191+}),
192+);
193+});
194+195+it("strips bidi and invisible formatting controls from command previews", async () => {
196+const params = createParams();
197+mockCallGatewayTool.mockResolvedValueOnce({
198+id: "plugin:approval-bidi",
199+decision: "allow-once",
200+});
201+202+await handleCodexAppServerApprovalRequest({
203+method: "item/commandExecution/requestApproval",
204+requestParams: {
205+threadId: "thread-1",
206+turnId: "turn-1",
207+itemId: "cmd-bidi",
208+command: "echo safe\u202e cod.exe\u2066 hidden\u2069 \ufeffdone\u{e0100}",
209+},
210+paramsForRun: params,
211+threadId: "thread-1",
212+turnId: "turn-1",
213+});
214+215+const [, , requestPayload] = mockCallGatewayTool.mock.calls[0] ?? [];
216+expect(requestPayload).toEqual(
217+expect.objectContaining({
218+description: "Command: echo safe cod.exe hidden done\nSession: agent:main:session-1",
219+}),
220+);
221+expect(params.onAgentEvent).toHaveBeenCalledWith(
222+expect.objectContaining({
223+stream: "approval",
224+data: expect.objectContaining({ command: "echo safe cod.exe hidden done" }),
225+}),
226+);
227+});
228+229+it("marks oversized unsafe command previews as omitted", async () => {
230+const params = createParams();
231+mockCallGatewayTool.mockResolvedValueOnce({
232+id: "plugin:approval-omitted-command",
233+decision: "allow-once",
234+});
235+const esc = "\u001b";
236+const oversizedPrefix = `${esc}]8;;https://example.com${esc}\\`.repeat(300);
237+238+await handleCodexAppServerApprovalRequest({
239+method: "item/commandExecution/requestApproval",
240+requestParams: {
241+threadId: "thread-1",
242+turnId: "turn-1",
243+itemId: "cmd-omitted",
244+command: [oversizedPrefix, "TAIL"],
245+},
246+paramsForRun: params,
247+threadId: "thread-1",
248+turnId: "turn-1",
249+});
250+251+const [, , requestPayload] = mockCallGatewayTool.mock.calls[0] ?? [];
252+expect(requestPayload).toEqual(
253+expect.objectContaining({
254+description:
255+"Command: [preview truncated or unsafe content omitted]\nSession: agent:main:session-1",
256+}),
257+);
258+expect(params.onAgentEvent).toHaveBeenCalledWith(
259+expect.objectContaining({
260+stream: "approval",
261+data: expect.objectContaining({
262+commandPreviewOmitted: true,
263+}),
264+}),
265+);
266+});
267+268+it("marks clipped command previews even when a safe prefix remains", async () => {
269+const params = createParams();
270+mockCallGatewayTool.mockResolvedValueOnce({
271+id: "plugin:approval-clipped-command",
272+decision: "allow-once",
273+});
274+275+await handleCodexAppServerApprovalRequest({
276+method: "item/commandExecution/requestApproval",
277+requestParams: {
278+threadId: "thread-1",
279+turnId: "turn-1",
280+itemId: "cmd-clipped",
281+command: `${"a".repeat(5000)} tail`,
282+},
283+paramsForRun: params,
284+threadId: "thread-1",
285+turnId: "turn-1",
286+});
287+288+const [, , requestPayload] = mockCallGatewayTool.mock.calls[0] ?? [];
289+const description = (requestPayload as { description: string }).description;
290+expect(description).toContain("[preview truncated or unsafe content omitted]");
291+expect(params.onAgentEvent).toHaveBeenCalledWith(
292+expect.objectContaining({
293+stream: "approval",
294+data: expect.objectContaining({
295+commandPreviewOmitted: true,
296+}),
297+}),
298+);
299+});
300+122301it("fails closed when no approval route is available", async () => {
123302const params = createParams();
124303mockCallGatewayTool.mockResolvedValueOnce({
@@ -149,6 +328,43 @@ describe("Codex app-server approval bridge", () => {
149328);
150329});
151330331+it("sanitizes reason previews before forwarding approval text and events", async () => {
332+const params = createParams();
333+mockCallGatewayTool.mockResolvedValueOnce({
334+id: "plugin:approval-sanitized-reason",
335+decision: null,
336+});
337+338+await handleCodexAppServerApprovalRequest({
339+method: "item/fileChange/requestApproval",
340+requestParams: {
341+threadId: "thread-1",
342+turnId: "turn-1",
343+itemId: "patch-sanitized",
344+reason: "needs write access\nfor \u001b[31m/tmp\u001b[0m\tplease",
345+},
346+paramsForRun: params,
347+threadId: "thread-1",
348+turnId: "turn-1",
349+});
350+351+const [, , requestPayload] = mockCallGatewayTool.mock.calls[0] ?? [];
352+expect(requestPayload).toEqual(
353+expect.objectContaining({
354+description: "Reason: needs write access for /tmp please\nSession: agent:main:session-1",
355+}),
356+);
357+expect(params.onAgentEvent).toHaveBeenCalledWith(
358+expect.objectContaining({
359+stream: "approval",
360+data: expect.objectContaining({
361+status: "unavailable",
362+reason: "needs write access for /tmp please",
363+}),
364+}),
365+);
366+});
367+152368it("fails closed for unsupported native approval methods without requesting plugin approval", async () => {
153369const params = createParams();
154370@@ -277,6 +493,39 @@ describe("Codex app-server approval bridge", () => {
277493expect(description).toContain("High-risk targets:");
278494});
279495496+it("strips terminal and invisible controls from permission descriptions", async () => {
497+const params = createParams();
498+mockCallGatewayTool.mockResolvedValueOnce({
499+id: "plugin:approval-permission-controls",
500+decision: "allow-once",
501+});
502+503+await handleCodexAppServerApprovalRequest({
504+method: "item/permissions/requestApproval",
505+requestParams: {
506+threadId: "thread-1",
507+turnId: "turn-1",
508+itemId: "perm-controls",
509+permissions: {
510+network: { allowHosts: ["exa\u009b31mmple.com", "safe\u202e.example.com"] },
511+fileSystem: { roots: ["/tmp/\u001b[31mproject\u001b[0m"] },
512+},
513+},
514+paramsForRun: params,
515+threadId: "thread-1",
516+turnId: "turn-1",
517+});
518+519+const [, , requestPayload] = mockCallGatewayTool.mock.calls[0] ?? [];
520+const description = (requestPayload as { description: string }).description;
521+expect(description).toContain("example.com");
522+expect(description).toContain("safe .example.com");
523+expect(description).toContain("/tmp/project");
524+expect(description).not.toContain("\u009b");
525+expect(description).not.toContain("\u202e");
526+expect(description).not.toContain("\u001b");
527+});
528+280529it("ignores approval requests that are missing explicit thread or turn ids", async () => {
281530const params = createParams();
282531此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。