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.
This commit is contained in:
can1357
2026-08-19 01:38:25 +02:00
parent 82257d3ab1
commit 0ac4e01b7b
2 changed files with 25 additions and 2 deletions
+5 -1
View File
@@ -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 },
@@ -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);