diff --git a/packages/ai/src/providers/cursor.ts b/packages/ai/src/providers/cursor.ts index 53c5d1179..a047da6e8 100644 --- a/packages/ai/src/providers/cursor.ts +++ b/packages/ai/src/providers/cursor.ts @@ -1229,9 +1229,13 @@ function sendUnknownApprovedInteractionResponse( fieldNo: number, ): void { // `approved {}` on the matching response oneof: field 1, empty message. + // Unknown LEN fields carry their raw wire bytes INCLUDING the length + // varint (that is what BinaryReader.skip captures and what + // BinaryWriter.raw replays verbatim after the tag), so the payload is + // `02` (length) `0a 00` (field 1, empty submessage). const response = create(InteractionResponseSchema, { id: queryId }); (response as { $unknown?: ProtoUnknownField[] }).$unknown = [ - { no: fieldNo, wireType: 2, data: new Uint8Array([0x0a, 0x00]) }, + { no: fieldNo, wireType: 2, data: new Uint8Array([0x02, 0x0a, 0x00]) }, ]; const clientMessage = create(AgentClientMessageSchema, { message: { case: "interactionResponse", value: response }, diff --git a/packages/ai/test/cursor-webfetch-interaction-query.test.ts b/packages/ai/test/cursor-webfetch-interaction-query.test.ts index 540e52c2a..389bd548d 100644 --- a/packages/ai/test/cursor-webfetch-interaction-query.test.ts +++ b/packages/ai/test/cursor-webfetch-interaction-query.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "bun:test"; -import { create, fromBinary } from "@bufbuild/protobuf"; +import { create, fromBinary, toBinary } from "@bufbuild/protobuf"; import { type BlockState, handleServerMessage, type ToolCallState } from "@oh-my-pi/pi-ai/providers/cursor"; import type { AssistantMessage } from "@oh-my-pi/pi-ai/types"; import { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream"; @@ -255,6 +255,25 @@ describe("Cursor interaction queries", () => { expect(response.result.value.result.case).toBe("approved"); }); + it("approves an unknown permission-shaped query variant with a decodable frame", async () => { + // Simulate a Cursor build newer than this proto: query variant on + // field 12 (LEN), which the schema does not name. The reply must carry + // `approved {}` on the same field number — and stay decodable: unknown + // LEN fields store raw wire bytes INCLUDING the length varint, so a + // missing prefix corrupts every byte after it in the frame. + const idOnly = toBinary(InteractionQuerySchema, create(InteractionQuerySchema, { id: 21 })); + const unknownVariant = new Uint8Array([0x62, 0x02, 0x0a, 0x00]); // field 12, LEN 2: approved {} + const raw = new Uint8Array(idOnly.length + unknownVariant.length); + raw.set(idOnly, 0); + raw.set(unknownVariant, idOnly.length); + + const frames = await dispatchQuery(fromBinary(InteractionQuerySchema, raw)); + const response = expectInteractionResponse(frames); + expect(response.id).toBe(21); + expect(response.result.case).toBeUndefined(); + expect(response.$unknown).toEqual([{ no: 12, wireType: 2, data: new Uint8Array([0x02, 0x0a, 0x00]) }]); + }); + it("does not invent a reply for an unknown query variant", async () => { const frames = await dispatchQuery(create(InteractionQuerySchema, { id: 17 })); expect(frames).toHaveLength(0);