@cartanova/qgrid-cli 2.5.3 → 2.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.ko.md +7 -2
  2. package/README.md +7 -2
  3. package/bundle/dist/application/monit/monit.frame.js +3 -3
  4. package/bundle/dist/application/qgrid/oauth.js +1 -1
  5. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +36 -14
  6. package/bundle/dist/application/qgrid/qgrid.frame.js +19 -5
  7. package/bundle/dist/application/qgrid/tool-emulation-schema.js +93 -0
  8. package/bundle/dist/application/qgrid/tool-emulation.js +116 -96
  9. package/bundle/dist/application/request-log/request-log.model.js +3 -3
  10. package/bundle/dist/application/request-log-step/request-log-step.model.js +2 -2
  11. package/bundle/dist/application/token/token.model.js +2 -2
  12. package/bundle/dist/server-error-handler.js +15 -0
  13. package/bundle/dist/sonamu.config.js +4 -7
  14. package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +4 -3
  15. package/bundle/dist/utils/providers/anthropic/claude-session.js +7 -5
  16. package/bundle/dist/utils/providers/common/json-schema-keywords.js +77 -0
  17. package/bundle/dist/utils/providers/common/schema-validation.js +178 -0
  18. package/bundle/dist/utils/providers/common/strictifier.js +332 -42
  19. package/bundle/dist/utils/providers/openai/codex-worker.js +16 -8
  20. package/bundle/dist/utils/providers/openai/openai-dispatcher.js +5 -37
  21. package/bundle/dist/utils/providers/openai/openai-worker-pool-config.js +37 -0
  22. package/bundle/src/application/monit/monit.frame.ts +1 -1
  23. package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +103 -0
  24. package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +337 -0
  25. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +45 -25
  26. package/bundle/src/application/qgrid/qgrid.frame.test.ts +580 -8
  27. package/bundle/src/application/qgrid/qgrid.frame.ts +28 -1
  28. package/bundle/src/application/qgrid/tool-emulation-schema.test.ts +188 -0
  29. package/bundle/src/application/qgrid/tool-emulation-schema.ts +177 -0
  30. package/bundle/src/application/qgrid/tool-emulation.test.ts +285 -67
  31. package/bundle/src/application/qgrid/tool-emulation.ts +199 -102
  32. package/bundle/src/server-error-handler.ts +22 -0
  33. package/bundle/src/sonamu.config.test.ts +37 -0
  34. package/bundle/src/sonamu.config.ts +2 -5
  35. package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +44 -0
  36. package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +2 -2
  37. package/bundle/src/utils/providers/anthropic/claude-session.test.ts +10 -0
  38. package/bundle/src/utils/providers/anthropic/claude-session.ts +6 -1
  39. package/bundle/src/utils/providers/common/json-schema-keywords.ts +72 -0
  40. package/bundle/src/utils/providers/common/schema-validation.test.ts +284 -0
  41. package/bundle/src/utils/providers/common/schema-validation.ts +308 -0
  42. package/bundle/src/utils/providers/common/strictifier.test.ts +746 -0
  43. package/bundle/src/utils/providers/common/strictifier.ts +520 -52
  44. package/bundle/src/utils/providers/openai/codex-worker.test.ts +31 -0
  45. package/bundle/src/utils/providers/openai/codex-worker.ts +12 -4
  46. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +6 -4
  47. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +6 -79
  48. package/bundle/src/utils/providers/openai/openai-worker-pool-config.ts +80 -0
  49. package/bundle/web-dist/client/assets/{index-DXdGBfB8.js → index-CN_kMswh.js} +3 -3
  50. package/bundle/web-dist/client/assets/{logs-D_j1yxC4.js → logs-6nYj9OKL.js} +1 -1
  51. package/bundle/web-dist/client/assets/{monit-CIToyoxW.js → monit-CaPH9Uwc.js} +1 -1
  52. package/bundle/web-dist/client/assets/{routes-Dd21A-wi.js → routes-Dh5HPi-b.js} +1 -1
  53. package/bundle/web-dist/client/assets/{show-DfePWRbc.js → show-Be_LVd9d.js} +1 -1
  54. package/bundle/web-dist/client/assets/{tokens-B4MV2fdk.js → tokens-CAi3zu_0.js} +1 -1
  55. package/bundle/web-dist/client/index.html +1 -1
  56. package/bundle/web-dist/server/entry-server.generated.js +1 -1
  57. package/package.json +1 -1
  58. package/skills/qgrid/references/ai-sdk-provider-contract.md +34 -2
  59. package/skills/qgrid/references/anthropic-claude-code-runtime.md +3 -0
  60. package/skills/qgrid/references/cli-env-and-server-boot.md +1 -1
  61. package/skills/qgrid/references/decision-rationale.md +10 -1
  62. package/skills/qgrid/references/docs-workflow.md +9 -0
  63. package/skills/qgrid/references/repo-map.md +2 -1
  64. package/skills/qgrid/references/tool-calling-and-multiturn.md +145 -6
  65. package/skills/qgrid/references/verification-and-debugging.md +16 -1
@@ -0,0 +1,284 @@
1
+ import { Buffer } from "node:buffer";
2
+
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import {
6
+ ANTHROPIC_SCHEMA_ARGV_MAX_UTF8_BYTES,
7
+ CALLER_SCHEMA_LIMITS,
8
+ CallerSchemaValidationError,
9
+ parseAndValidateCallerSchemas,
10
+ serializeAndValidateDispatchSchema,
11
+ } from "./schema-validation";
12
+
13
+ // jsonSchema 단독 경로용 테스트 헬퍼 — 프로덕션 진입점은 parseAndValidateCallerSchemas 하나다.
14
+ const parseCallerOutputSchema = (jsonSchema: string) =>
15
+ parseAndValidateCallerSchemas({ jsonSchema });
16
+
17
+ function schemaWithArrayDepth(arrayDepth: number): string {
18
+ return `{"type":"object","x":${"[".repeat(arrayDepth)}0${"]".repeat(arrayDepth)}}`;
19
+ }
20
+
21
+ function schemaWithNodeCount(nodeCount: number): string {
22
+ // root object + "object" string + enum array account for the other three nodes.
23
+ const enumLength = nodeCount - 3;
24
+ return `{"type":"object","enum":[${Array.from({ length: enumLength }, () => "0").join(",")}]}`;
25
+ }
26
+
27
+ function schemaWithUtf8Bytes(byteLength: number): string {
28
+ const prefix = '{"type":"object","description":"';
29
+ const suffix = '"}';
30
+ const paddingLength =
31
+ byteLength - Buffer.byteLength(prefix, "utf8") - Buffer.byteLength(suffix, "utf8");
32
+ if (paddingLength < 0) throw new Error("requested schema byte length is too small");
33
+ return `${prefix}${"x".repeat(paddingLength)}${suffix}`;
34
+ }
35
+
36
+ describe("caller schema validation", () => {
37
+ it("accepts schemas exactly at each complexity boundary", () => {
38
+ expect(() =>
39
+ parseCallerOutputSchema(
40
+ schemaWithUtf8Bytes(CALLER_SCHEMA_LIMITS.maxUtf8Bytes),
41
+ ),
42
+ ).not.toThrow();
43
+ expect(() =>
44
+ parseCallerOutputSchema(
45
+ schemaWithNodeCount(CALLER_SCHEMA_LIMITS.maxNodes),
46
+ ),
47
+ ).not.toThrow();
48
+ // Root is depth 0 and the primitive inside N nested arrays is depth N + 1.
49
+ expect(() =>
50
+ parseCallerOutputSchema(
51
+ schemaWithArrayDepth(CALLER_SCHEMA_LIMITS.maxDepth - 1),
52
+ ),
53
+ ).not.toThrow();
54
+ });
55
+
56
+ it.each([
57
+ [
58
+ "UTF-8 bytes",
59
+ () => schemaWithUtf8Bytes(CALLER_SCHEMA_LIMITS.maxUtf8Bytes + 1),
60
+ "UTF-8 byte limit",
61
+ ],
62
+ [
63
+ "nodes",
64
+ () => schemaWithNodeCount(CALLER_SCHEMA_LIMITS.maxNodes + 1),
65
+ "node limit",
66
+ ],
67
+ [
68
+ "depth",
69
+ () => schemaWithArrayDepth(CALLER_SCHEMA_LIMITS.maxDepth),
70
+ "depth limit",
71
+ ],
72
+ ])("rejects schemas above the %s budget", (_label, makeSchema, message) => {
73
+ expect(() => parseCallerOutputSchema(makeSchema())).toThrow(
74
+ CallerSchemaValidationError,
75
+ );
76
+ expect(() => parseCallerOutputSchema(makeSchema())).toThrow(message);
77
+ });
78
+
79
+ it("measures the raw schema in UTF-8 bytes, not JavaScript code units", () => {
80
+ const prefix = '{"type":"object","description":"';
81
+ const suffix = '"}';
82
+ const remainingBytes =
83
+ CALLER_SCHEMA_LIMITS.maxUtf8Bytes -
84
+ Buffer.byteLength(prefix, "utf8") -
85
+ Buffer.byteLength(suffix, "utf8");
86
+ const schema = `${prefix}${"가".repeat(Math.floor(remainingBytes / 3) + 1)}${suffix}`;
87
+
88
+ expect(schema.length).toBeLessThan(CALLER_SCHEMA_LIMITS.maxUtf8Bytes);
89
+ expect(() => parseCallerOutputSchema(schema)).toThrow(/UTF-8 byte limit/);
90
+ });
91
+
92
+ it("enforces the final Anthropic argv limit without constraining OpenAI to it", () => {
93
+ const atLimit = JSON.parse(
94
+ schemaWithUtf8Bytes(ANTHROPIC_SCHEMA_ARGV_MAX_UTF8_BYTES),
95
+ );
96
+ const aboveLimit = JSON.parse(
97
+ schemaWithUtf8Bytes(ANTHROPIC_SCHEMA_ARGV_MAX_UTF8_BYTES + 1),
98
+ );
99
+
100
+ expect(() => serializeAndValidateDispatchSchema(atLimit, "anthropic")).not.toThrow();
101
+ expect(() => serializeAndValidateDispatchSchema(aboveLimit, "anthropic")).toThrow(
102
+ "Anthropic dispatch schema exceeds argv UTF-8 byte limit",
103
+ );
104
+ expect(() => serializeAndValidateDispatchSchema(aboveLimit, "openai")).not.toThrow();
105
+ });
106
+
107
+ it("enforces the aggregate limit again after schema composition", () => {
108
+ const overLimit = {
109
+ type: "object",
110
+ description: "x".repeat(CALLER_SCHEMA_LIMITS.maxUtf8Bytes),
111
+ } as const;
112
+
113
+ expect(() => serializeAndValidateDispatchSchema(overLimit, "openai")).toThrow(
114
+ "dispatch schema exceeds UTF-8 byte limit",
115
+ );
116
+ });
117
+
118
+ it("rejects non-serializable direct dispatch schemas", () => {
119
+ expect(() => serializeAndValidateDispatchSchema(() => {}, "anthropic")).toThrow(
120
+ "dispatch schema must be JSON-serializable",
121
+ );
122
+ });
123
+
124
+ it("applies the same depth guard to tools-only input schemas", () => {
125
+ const inputSchema = JSON.parse(
126
+ schemaWithArrayDepth(CALLER_SCHEMA_LIMITS.maxDepth),
127
+ ) as unknown;
128
+
129
+ expect(() =>
130
+ parseAndValidateCallerSchemas({
131
+ tools: [{ name: "deepTool", inputSchema }],
132
+ }),
133
+ ).toThrow(CallerSchemaValidationError);
134
+ expect(() =>
135
+ parseAndValidateCallerSchemas({
136
+ tools: [{ name: "deepTool", inputSchema }],
137
+ }),
138
+ ).toThrow("tools[0].inputSchema exceeds depth limit");
139
+ });
140
+
141
+ it("aggregates node and byte budgets across output and tool schemas", () => {
142
+ const halfNodeSchema = JSON.parse(
143
+ schemaWithNodeCount(Math.floor(CALLER_SCHEMA_LIMITS.maxNodes / 2) + 1),
144
+ ) as unknown;
145
+ expect(() =>
146
+ parseAndValidateCallerSchemas({
147
+ tools: [
148
+ { name: "first", inputSchema: halfNodeSchema },
149
+ { name: "second", inputSchema: halfNodeSchema },
150
+ ],
151
+ }),
152
+ ).toThrow("caller schemas exceed aggregate node limit");
153
+
154
+ const halfByteSchema = JSON.parse(
155
+ schemaWithUtf8Bytes(Math.floor(CALLER_SCHEMA_LIMITS.maxUtf8Bytes / 2) + 1),
156
+ ) as unknown;
157
+ expect(() =>
158
+ parseAndValidateCallerSchemas({
159
+ tools: [
160
+ { name: "first", inputSchema: halfByteSchema },
161
+ { name: "second", inputSchema: halfByteSchema },
162
+ ],
163
+ }),
164
+ ).toThrow("caller schemas exceed aggregate UTF-8 byte limit");
165
+ });
166
+
167
+ it("counts a tool name twice before materializing the composed schema", () => {
168
+ const name = "x".repeat(Math.floor(CALLER_SCHEMA_LIMITS.maxUtf8Bytes / 2));
169
+ const stringify = vi.spyOn(JSON, "stringify");
170
+
171
+ expect(Buffer.byteLength(name, "utf8")).toBeLessThan(CALLER_SCHEMA_LIMITS.maxUtf8Bytes);
172
+ expect(() =>
173
+ parseAndValidateCallerSchemas({
174
+ tools: [{ name, inputSchema: { type: "object" } }],
175
+ }),
176
+ ).toThrow("tools[0].name exceeds UTF-8 byte limit");
177
+ expect(stringify).not.toHaveBeenCalled();
178
+ });
179
+
180
+ it("counts JSON escaping in tool descriptions before schema composition", () => {
181
+ const description = '"'.repeat(
182
+ Math.floor(CALLER_SCHEMA_LIMITS.maxUtf8Bytes / 2),
183
+ );
184
+ const stringify = vi.spyOn(JSON, "stringify");
185
+
186
+ expect(Buffer.byteLength(description, "utf8")).toBeLessThan(
187
+ CALLER_SCHEMA_LIMITS.maxUtf8Bytes,
188
+ );
189
+ expect(() =>
190
+ parseAndValidateCallerSchemas({
191
+ tools: [{ name: "escaped", description, inputSchema: { type: "object" } }],
192
+ }),
193
+ ).toThrow("tools[0].description exceeds UTF-8 byte limit");
194
+ expect(stringify).not.toHaveBeenCalled();
195
+ });
196
+
197
+ it("counts nested escaping when an input schema is embedded in the tool description", () => {
198
+ const escapedValue = '"'.repeat(
199
+ Math.floor(CALLER_SCHEMA_LIMITS.maxUtf8Bytes / 4),
200
+ );
201
+ const inputSchema = { type: "object", description: escapedValue };
202
+ const serializedInputSchema = JSON.stringify(inputSchema);
203
+
204
+ expect(Buffer.byteLength(serializedInputSchema, "utf8")).toBeLessThan(
205
+ CALLER_SCHEMA_LIMITS.maxUtf8Bytes,
206
+ );
207
+ expect(() =>
208
+ parseAndValidateCallerSchemas({
209
+ tools: [{ name: "escapedInput", inputSchema }],
210
+ }),
211
+ ).toThrow("tools[0].inputSchema exceeds UTF-8 byte limit");
212
+ });
213
+
214
+ it("counts per-tool composition framing even when tool metadata is empty", () => {
215
+ const tools = Array.from({ length: CALLER_SCHEMA_LIMITS.maxNodes }, () => ({
216
+ name: "",
217
+ inputSchema: {},
218
+ }));
219
+
220
+ expect(() => parseAndValidateCallerSchemas({ tools })).toThrow(
221
+ "caller schemas exceed aggregate UTF-8 byte limit",
222
+ );
223
+ });
224
+
225
+ it("rejects an oversized tool schema before materializing its full JSON serialization", () => {
226
+ const stringify = vi.spyOn(JSON, "stringify");
227
+ let thrown: unknown;
228
+
229
+ try {
230
+ parseAndValidateCallerSchemas({
231
+ tools: [
232
+ {
233
+ name: "oversized",
234
+ inputSchema: {
235
+ type: "object",
236
+ description: "x".repeat(CALLER_SCHEMA_LIMITS.maxUtf8Bytes + 1),
237
+ },
238
+ },
239
+ ],
240
+ });
241
+ } catch (error) {
242
+ thrown = error;
243
+ }
244
+
245
+ expect(thrown).toBeInstanceOf(CallerSchemaValidationError);
246
+ expect((thrown as Error).message).toContain("UTF-8 byte limit");
247
+ expect(stringify).not.toHaveBeenCalled();
248
+ });
249
+
250
+ it("stops a wide object at the node limit before reading later property values", () => {
251
+ const properties: Record<string, unknown> = {};
252
+ // inputSchema root, its type value, and this properties object consume three nodes.
253
+ for (let index = 0; index < CALLER_SCHEMA_LIMITS.maxNodes - 3; index += 1) {
254
+ properties[`field${index}`] = 0;
255
+ }
256
+ const readPastLimit = vi.fn(() => {
257
+ throw new Error("value past node limit was read");
258
+ });
259
+ Object.defineProperty(properties, "pastLimit", {
260
+ enumerable: true,
261
+ get: readPastLimit,
262
+ });
263
+
264
+ expect(() =>
265
+ parseAndValidateCallerSchemas({
266
+ tools: [{ name: "wide", inputSchema: { type: "object", properties } }],
267
+ }),
268
+ ).toThrow("tools[0].inputSchema exceeds node limit");
269
+ expect(readPastLimit).not.toHaveBeenCalled();
270
+ });
271
+
272
+ it.each([
273
+ ["malformed JSON", '{"type":"object"', "must contain valid JSON"],
274
+ ["array JSON root", "[]", 'top-level type must be "object"'],
275
+ ["primitive JSON root", "true", 'top-level type must be "object"'],
276
+ ["array output schema", '{"type":"array","items":{"type":"string"}}', 'top-level type must be "object"'],
277
+ ["missing output type", '{"properties":{}}', 'top-level type must be "object"'],
278
+ ])("rejects %s with a stable validation error", (_label, schema, message) => {
279
+ expect(() => parseCallerOutputSchema(schema)).toThrow(
280
+ CallerSchemaValidationError,
281
+ );
282
+ expect(() => parseCallerOutputSchema(schema)).toThrow(message);
283
+ });
284
+ });
@@ -0,0 +1,308 @@
1
+ import { Buffer } from "node:buffer";
2
+
3
+ import { type JsonValue } from "../../../codex-protocol/serde_json/JsonValue";
4
+
5
+ /**
6
+ * Caller-provided schemas are serialized, copied, and recursively normalized before dispatch.
7
+ * Keep these limits comfortably above normal generated schemas while bounding that preprocessing
8
+ * well below the API's 10 MiB request-body ceiling.
9
+ */
10
+ export const CALLER_SCHEMA_LIMITS = {
11
+ maxUtf8Bytes: 512 * 1024,
12
+ maxNodes: 20_000,
13
+ maxDepth: 128,
14
+ } as const;
15
+
16
+ // Linux permits at most 128 KiB (MAX_ARG_STRLEN) for one argv value. qgrid uses a 50% safety
17
+ // margin for every argv-delivered payload — Claude Code --json-schema here and the system-prompt
18
+ // transport (claude-session) both derive from this single constant.
19
+ export const ARGV_SAFE_MAX_UTF8_BYTES = 64 * 1024;
20
+ export const ANTHROPIC_SCHEMA_ARGV_MAX_UTF8_BYTES = ARGV_SAFE_MAX_UTF8_BYTES;
21
+
22
+ export class CallerSchemaValidationError extends Error {
23
+ constructor(message: string) {
24
+ super(`qgrid: ${message}`);
25
+ this.name = "CallerSchemaValidationError";
26
+ }
27
+ }
28
+
29
+ export interface CallerSchemasInput {
30
+ jsonSchema?: string;
31
+ tools?: Array<{ name: string; description?: string; inputSchema: unknown }>;
32
+ }
33
+
34
+ interface ComplexityState {
35
+ utf8Bytes: number;
36
+ nodes: number;
37
+ schemas: number;
38
+ }
39
+
40
+ export function parseAndValidateCallerSchemas(input: CallerSchemasInput): JsonValue | undefined {
41
+ const state: ComplexityState = {
42
+ utf8Bytes: 0,
43
+ nodes: 0,
44
+ schemas: 0,
45
+ };
46
+ let outputSchema: JsonValue | undefined;
47
+
48
+ if (input.jsonSchema !== undefined) {
49
+ addUtf8Bytes(Buffer.byteLength(input.jsonSchema, "utf8"), "jsonSchema", state);
50
+
51
+ let parsed: unknown;
52
+ try {
53
+ parsed = JSON.parse(input.jsonSchema);
54
+ } catch {
55
+ throw new CallerSchemaValidationError("jsonSchema must contain valid JSON");
56
+ }
57
+
58
+ if (!isRecord(parsed) || parsed.type !== "object") {
59
+ throw new CallerSchemaValidationError('jsonSchema top-level type must be "object"');
60
+ }
61
+
62
+ addComplexity(parsed, "jsonSchema", state);
63
+ state.schemas += 1;
64
+ outputSchema = parsed as JsonValue;
65
+ }
66
+
67
+ for (const [index, tool] of (input.tools ?? []).entries()) {
68
+ addToolMetadataUtf8Bytes(tool, index, state);
69
+
70
+ const path = `tools[${index}].inputSchema`;
71
+ addComplexity(tool.inputSchema, path, state, true);
72
+
73
+ const serialized = stringifyJsonOrThrow(tool.inputSchema, path);
74
+
75
+ // buildToolCallSchema embeds this serialization inside another JSON string. Charge the
76
+ // escaped outer-string representation before that composed description is materialized.
77
+ addUtf8Bytes(jsonStringContentUtf8ByteUpperBound(serialized), path, state);
78
+ state.schemas += 1;
79
+ }
80
+
81
+ return outputSchema;
82
+ }
83
+
84
+ export function serializeAndValidateDispatchSchema(
85
+ schema: unknown,
86
+ provider?: string,
87
+ ): string | undefined {
88
+ if (schema === undefined) return undefined;
89
+
90
+ const serialized = stringifyJsonOrThrow(schema, "dispatch schema");
91
+
92
+ const byteLength = Buffer.byteLength(serialized, "utf8");
93
+ if (byteLength > CALLER_SCHEMA_LIMITS.maxUtf8Bytes) {
94
+ throw new CallerSchemaValidationError(
95
+ `dispatch schema exceeds UTF-8 byte limit of ${CALLER_SCHEMA_LIMITS.maxUtf8Bytes}`,
96
+ );
97
+ }
98
+ if (provider === "anthropic") {
99
+ assertAnthropicSchemaArgvSize(serialized);
100
+ }
101
+
102
+ return serialized;
103
+ }
104
+
105
+ export function assertAnthropicSchemaArgvSize(jsonSchema: string): void {
106
+ if (Buffer.byteLength(jsonSchema, "utf8") > ANTHROPIC_SCHEMA_ARGV_MAX_UTF8_BYTES) {
107
+ throw new CallerSchemaValidationError(
108
+ `Anthropic dispatch schema exceeds argv UTF-8 byte limit of ${ANTHROPIC_SCHEMA_ARGV_MAX_UTF8_BYTES}`,
109
+ );
110
+ }
111
+ }
112
+
113
+ function stringifyJsonOrThrow(value: unknown, subject: string): string {
114
+ let serialized: string | undefined;
115
+ try {
116
+ serialized = JSON.stringify(value);
117
+ } catch {
118
+ throw new CallerSchemaValidationError(`${subject} must be JSON-serializable`);
119
+ }
120
+ if (serialized === undefined) {
121
+ throw new CallerSchemaValidationError(`${subject} must be JSON-serializable`);
122
+ }
123
+ return serialized;
124
+ }
125
+
126
+ function addUtf8Bytes(byteLength: number, path: string, state: ComplexityState): void {
127
+ state.utf8Bytes += byteLength;
128
+ if (state.utf8Bytes > CALLER_SCHEMA_LIMITS.maxUtf8Bytes) {
129
+ throwLimitError(path, "UTF-8 byte", CALLER_SCHEMA_LIMITS.maxUtf8Bytes, state);
130
+ }
131
+ }
132
+
133
+ const TOOL_DESCRIPTION_ENTRY_PREFIX = "- ";
134
+ const TOOL_DESCRIPTION_NAME_SEPARATOR = ": ";
135
+ const TOOL_DESCRIPTION_INPUT_SCHEMA_PREFIX = "\n inputSchema: ";
136
+ const TOOL_DESCRIPTION_ENTRY_SEPARATOR = "\n";
137
+
138
+ function addToolMetadataUtf8Bytes(
139
+ tool: NonNullable<CallerSchemasInput["tools"]>[number],
140
+ index: number,
141
+ state: ComplexityState,
142
+ ): void {
143
+ const toolPath = `tools[${index}]`;
144
+
145
+ // buildToolCallSchema places every tool name in both the enum and the human-readable tool
146
+ // description. Count both escaped occurrences, plus the enum's JSON string delimiters.
147
+ const encodedNameBytes = jsonStringContentUtf8ByteUpperBound(tool.name);
148
+ addUtf8Bytes(encodedNameBytes * 2 + 2, `${toolPath}.name`, state);
149
+
150
+ if (tool.description !== undefined) {
151
+ addUtf8Bytes(
152
+ jsonStringContentUtf8ByteUpperBound(tool.description),
153
+ `${toolPath}.description`,
154
+ state,
155
+ );
156
+ }
157
+
158
+ // Per-tool framing also grows with the number of tools even when all caller strings are empty.
159
+ // Include both the toolDescriptions join separator and the enum comma after the first entry.
160
+ const framingBytes =
161
+ jsonStringContentUtf8ByteUpperBound(TOOL_DESCRIPTION_ENTRY_PREFIX) +
162
+ jsonStringContentUtf8ByteUpperBound(TOOL_DESCRIPTION_NAME_SEPARATOR) +
163
+ jsonStringContentUtf8ByteUpperBound(TOOL_DESCRIPTION_INPUT_SCHEMA_PREFIX) +
164
+ (index === 0 ? 0 : jsonStringContentUtf8ByteUpperBound(TOOL_DESCRIPTION_ENTRY_SEPARATOR) + 1);
165
+ addUtf8Bytes(framingBytes, toolPath, state);
166
+ }
167
+
168
+ /**
169
+ * Returns a conservative UTF-8 byte count for a string when it becomes JSON string content,
170
+ * without allocating the escaped representation. JSON.stringify uses short two-byte escapes for
171
+ * five control characters; counting every control character as a six-byte `\u00XX` escape keeps
172
+ * this an upper bound while avoiding a second large temporary string.
173
+ */
174
+ function jsonStringContentUtf8ByteUpperBound(value: string): number {
175
+ let bytes = 0;
176
+
177
+ for (let index = 0; index < value.length; index += 1) {
178
+ const codeUnit = value.charCodeAt(index);
179
+
180
+ if (codeUnit === 0x22 || codeUnit === 0x5c) {
181
+ bytes += 2;
182
+ continue;
183
+ }
184
+ if (codeUnit <= 0x1f) {
185
+ bytes += 6;
186
+ continue;
187
+ }
188
+ if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
189
+ const nextCodeUnit = value.charCodeAt(index + 1);
190
+ if (nextCodeUnit >= 0xdc00 && nextCodeUnit <= 0xdfff) {
191
+ bytes += 4;
192
+ index += 1;
193
+ } else {
194
+ // Well-formed JSON.stringify escapes lone surrogates as `\uXXXX`.
195
+ bytes += 6;
196
+ }
197
+ continue;
198
+ }
199
+ if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
200
+ bytes += 6;
201
+ continue;
202
+ }
203
+
204
+ bytes += codeUnit <= 0x7f ? 1 : codeUnit <= 0x7ff ? 2 : 3;
205
+ }
206
+
207
+ return bytes;
208
+ }
209
+
210
+ function addComplexity(
211
+ root: unknown,
212
+ path: string,
213
+ state: ComplexityState,
214
+ precheckUtf8Bytes = false,
215
+ ): void {
216
+ const rawUtf8Bytes = { value: 0 };
217
+ if (precheckUtf8Bytes && typeof root === "string") {
218
+ addRawUtf8LowerBound(root, path, state, rawUtf8Bytes);
219
+ }
220
+ addNode(path, state);
221
+ if (root === null || typeof root !== "object") return;
222
+
223
+ const stack: Array<{ value: Record<string, unknown> | unknown[]; depth: number }> = [
224
+ { value: root as Record<string, unknown> | unknown[], depth: 0 },
225
+ ];
226
+
227
+ while (stack.length > 0) {
228
+ const current = stack.pop()!;
229
+ if (Array.isArray(current.value)) {
230
+ for (let index = 0; index < current.value.length; index += 1) {
231
+ const depth = current.depth + 1;
232
+ assertDepthWithinLimit(depth, path);
233
+ addNode(path, state);
234
+
235
+ const child = current.value[index];
236
+ if (precheckUtf8Bytes && typeof child === "string") {
237
+ addRawUtf8LowerBound(child, path, state, rawUtf8Bytes);
238
+ }
239
+ if (child !== null && typeof child === "object") {
240
+ stack.push({ value: child as Record<string, unknown> | unknown[], depth });
241
+ }
242
+ }
243
+ continue;
244
+ }
245
+
246
+ for (const key in current.value) {
247
+ if (!Object.hasOwn(current.value, key)) continue;
248
+ if (precheckUtf8Bytes) {
249
+ addRawUtf8LowerBound(key, path, state, rawUtf8Bytes);
250
+ }
251
+ const depth = current.depth + 1;
252
+ assertDepthWithinLimit(depth, path);
253
+ addNode(path, state);
254
+
255
+ const child = current.value[key];
256
+ if (precheckUtf8Bytes && typeof child === "string") {
257
+ addRawUtf8LowerBound(child, path, state, rawUtf8Bytes);
258
+ }
259
+ if (child !== null && typeof child === "object") {
260
+ stack.push({ value: child as Record<string, unknown> | unknown[], depth });
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ function addRawUtf8LowerBound(
267
+ value: string,
268
+ path: string,
269
+ state: ComplexityState,
270
+ rawUtf8Bytes: { value: number },
271
+ ): void {
272
+ rawUtf8Bytes.value += Buffer.byteLength(value, "utf8");
273
+ if (state.utf8Bytes + rawUtf8Bytes.value > CALLER_SCHEMA_LIMITS.maxUtf8Bytes) {
274
+ throwLimitError(path, "UTF-8 byte", CALLER_SCHEMA_LIMITS.maxUtf8Bytes, state);
275
+ }
276
+ }
277
+
278
+ function assertDepthWithinLimit(depth: number, path: string): void {
279
+ if (depth > CALLER_SCHEMA_LIMITS.maxDepth) {
280
+ throw new CallerSchemaValidationError(
281
+ `${path} exceeds depth limit of ${CALLER_SCHEMA_LIMITS.maxDepth}`,
282
+ );
283
+ }
284
+ }
285
+
286
+ function addNode(path: string, state: ComplexityState): void {
287
+ state.nodes += 1;
288
+ if (state.nodes > CALLER_SCHEMA_LIMITS.maxNodes) {
289
+ throwLimitError(path, "node", CALLER_SCHEMA_LIMITS.maxNodes, state);
290
+ }
291
+ }
292
+
293
+ function throwLimitError(
294
+ path: string,
295
+ kind: "UTF-8 byte" | "node",
296
+ limit: number,
297
+ state: ComplexityState,
298
+ ): never {
299
+ throw new CallerSchemaValidationError(
300
+ state.schemas === 0
301
+ ? `${path} exceeds ${kind} limit of ${limit}`
302
+ : `caller schemas exceed aggregate ${kind} limit of ${limit}`,
303
+ );
304
+ }
305
+
306
+ function isRecord(value: unknown): value is Record<string, unknown> {
307
+ return value !== null && typeof value === "object" && !Array.isArray(value);
308
+ }