From 7e2e7e1c35087b586d0bfbfdc336a636bbaa3cb6 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 28 Sep 2026 11:23:22 -0500 Subject: [PATCH] Fence permission requests to the admitted ACP session Co-Authored-By: Paperclip --- .../acpx/codex-runtime-adapter.test.ts | 24 ++++++++++++++++++- .../src/drivers/acpx/codex-runtime-adapter.ts | 5 ++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts index f1c44314788..d66084329dc 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.test.ts @@ -28,6 +28,28 @@ const HANDLE: AcpRuntimeHandle = { }; describe("Codex ACPX runtime adapter", () => { + it("rejects forged permission session identifiers before delegating or applying full-auto policy", async () => { + const pending = pendingExtensionTurn("turn-1"); + const runtime = fakeRuntime(); vi.mocked(runtime.startTurn).mockReturnValue(pending.turn); + let created!: AcpRuntimeOptions; + const options = openOptions(fakeCommand()); + const port = await openCodexAcpxRuntime(options, { + createRegistry: () => registry(), createStore: () => store(), createRuntime: (value) => { created = value; return runtime; }, + }); + const handler = vi.fn(async () => ({ outcome: "allow_once" as const })); + const turn = port.startTurn({ text: "Write", requestId: "turn-1", onPermissionRequest: handler }); + const signal = new AbortController().signal; + for (const [sessionId, rawSessionId] of [["forged", "backend-1"], ["backend-1", "forged"]]) { + const request = { sessionId, raw: { sessionId: rawSessionId }, inferredKind: "edit" as const }; + await expect(created.onPermissionRequest!(request as never, { signal })).resolves.toEqual({ outcome: "reject_once" }); + } + expect(handler).not.toHaveBeenCalled(); + await expect(created.onPermissionRequest!({ sessionId: "backend-1", raw: { sessionId: "backend-1" }, inferredKind: "edit" } as never, { signal })).resolves.toEqual({ outcome: "allow_once" }); + expect(handler).toHaveBeenCalledOnce(); + options.permissionMode = "approve-all"; + await expect(created.onPermissionRequest!({ sessionId: "forged", raw: {}, inferredKind: "edit" } as never, { signal })).resolves.toEqual({ outcome: "reject_once" }); + pending.settle(); await turn.result; await port.close({ reason: "session checks complete" }); + }); it("routes only profile-allowed extensions to the owning turn and expires late responses", async () => { const runtime = fakeRuntime(); const first = pendingExtensionTurn("turn-1"); @@ -1745,7 +1767,7 @@ describe("Codex ACPX runtime adapter", () => { await expect( runtimeOptions?.onPermissionRequest?.( { - sessionId: "session-1", + sessionId: "backend-1", inferredKind: "write", raw: {}, }, diff --git a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts index f4fc95a27c1..77d540520e0 100644 --- a/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts +++ b/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts @@ -349,6 +349,11 @@ export async function openQualifiedAcpxRuntime( ], })), onPermissionRequest: async (request, context) => { + const rawSessionId = objectRecord(request.raw).sessionId; + if (!extensionBoundary.sessionIds.has(request.sessionId) + || (rawSessionId !== undefined && rawSessionId !== request.sessionId)) { + return { outcome: "reject_once" }; + } const disposition = decideAcpxPermission( options.profile.agent, options.permissionMode,