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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user