Skip to content

Commit 250677b

Browse files
committed
fix(tool-payload): reject oversized XML payload bytes
1 parent a780541 commit 250677b

2 files changed

Lines changed: 26 additions & 2 deletions

File tree

packages/tool-call-repair/src/payload.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export type PlainTextToolCallParseOptions = {
3434
};
3535

3636
const DEFAULT_MAX_PLAIN_TEXT_TOOL_PAYLOAD_BYTES = 256_000;
37+
const utf8Encoder = new TextEncoder();
3738

3839
type PlainTextToolCallOpening = {
3940
allowsOptionalXmlishClose?: boolean;
@@ -221,6 +222,15 @@ type XmlishParameterBlockBounds = {
221222
start: number;
222223
};
223224

225+
function utf8ByteLengthExceeds(
226+
text: string,
227+
start: number,
228+
end: number,
229+
maxBytes: number,
230+
): boolean {
231+
return end - start > maxBytes || utf8Encoder.encode(text.slice(start, end)).byteLength > maxBytes;
232+
}
233+
224234
function findXmlishParameterBlock(text: string, start: number): XmlishParameterBlockBounds | null {
225235
const cursor = skipWhitespace(text, start);
226236
const openMatch = /^<parameter=([A-Za-z0-9_.:-]{1,120})>/i.exec(text.slice(cursor));
@@ -252,7 +262,7 @@ function consumeXmlishParameterBlock(
252262
if (!bounds) {
253263
return null;
254264
}
255-
if (bounds.end - bounds.start > maxPayloadBytes) {
265+
if (utf8ByteLengthExceeds(text, bounds.start, bounds.end, maxPayloadBytes)) {
256266
return null;
257267
}
258268
return {
@@ -344,7 +354,7 @@ function parseXmlishPlainTextToolCallBlockAt(
344354
if (!parameter) {
345355
break;
346356
}
347-
if (parameter.end - opening.end > maxPayloadBytes) {
357+
if (utf8ByteLengthExceeds(text, opening.end, parameter.end, maxPayloadBytes)) {
348358
return null;
349359
}
350360
args[parameter.name] = parameter.value;

src/plugin-sdk/tool-payload.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,20 @@ describe("parseStandalonePlainTextToolCallBlocks", () => {
242242
).toBeNull();
243243
});
244244

245+
it("counts serialized XML parameter payload limits in UTF-8 bytes", () => {
246+
const parameter = ["<parameter=content>", "é".repeat(20), "</parameter>"].join("\n");
247+
const raw = ["<function=write>", parameter, "</function>"].join("\n");
248+
expect(parameter.length).toBeLessThan(64);
249+
expect(new TextEncoder().encode(parameter).byteLength).toBeGreaterThan(64);
250+
251+
expect(
252+
parseStandalonePlainTextToolCallBlocks(raw, {
253+
allowedToolNames: ["write"],
254+
maxPayloadBytes: 64,
255+
}),
256+
).toBeNull();
257+
});
258+
245259
it("respects allowed tool names for Harmony calls", () => {
246260
const blocks = parseStandalonePlainTextToolCallBlocks(
247261
'commentary to=write code {"path":"/tmp/file.txt","content":"x"}',

0 commit comments

Comments
 (0)