From 0ac4e01b7bcaddef060d1f74ea557633f2e49419 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 19 Aug 2026 01:18:50 +0200 Subject: [PATCH] fix(ai): length-prefix the unknown interaction-query approval payload Unknown LEN fields in protobuf-es carry raw wire bytes including the length varint (BinaryReader.skip captures it; BinaryWriter.raw replays verbatim after the tag). The fallback for unnamed permission-query variants wrote 'approved {}' as bare 0a 00, producing a frame the server cannot decode (the 0a is read as a length of 16). Prefix the payload with its length and lock the wire shape with a round-trip test. --- packages/ai/src/providers/cursor.ts | 6 +++++- .../cursor-webfetch-interaction-query.test.ts | 21 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) 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);