From 2c5fb039ce1da3b0ce7ba9e177499b16c14c4656 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Sun, 11 Oct 2026 09:33:43 -0400 Subject: [PATCH 1/3] fix(tools): enforce string schema lengths by Unicode code point --- CHANGELOG.md | 2 ++ docs/CURRENT-STATE.md | 5 +++++ src/tools/schema.js | 5 +++-- src/tools/tool-schema-codepoints.test.mjs | 20 ++++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/tools/tool-schema-codepoints.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7acdedf6b..f7607fd02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ the five borough cores load and `CCTV_NYC_MAX_SOURCES` changes the cap. The catalog-wide ceiling is unchanged; extra cameras share it round-robin with the other packs. +- Tool string length validation counts Unicode characters correctly, including characters outside the basic multilingual plane. + - Treat explicitly current, complete empty vessel coverage as a successful update, clearing obsolete contacts while retaining stale or incomplete snapshots. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index c77d1d8e3..7e1970768 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -5310,3 +5310,8 @@ datacenters, dams and the Nepal locator retain that shared credit while displaye Military fragments stitch by source identity. Different mapped parcels merge only when their footprints touch or contain one another, never across a seam tolerance gap. + + +### fix(tools): enforce string schema lengths by Unicode code point + +Count code points for minLength and maxLength, preserving ASCII behavior and regular-expression validation. diff --git a/src/tools/schema.js b/src/tools/schema.js index 7c40c3eb5..cdb63cfc1 100644 --- a/src/tools/schema.js +++ b/src/tools/schema.js @@ -106,9 +106,10 @@ export function validateValue(schema, value, path = 'arguments') { fail('must be a string'); break; } - if (schema.minLength != null && value.length < schema.minLength) + const length = [...value].length; + if (schema.minLength != null && length < schema.minLength) fail(`must be at least ${schema.minLength} characters`); - if (schema.maxLength != null && value.length > schema.maxLength) + if (schema.maxLength != null && length > schema.maxLength) fail(`must be at most ${schema.maxLength} characters`); if ( schema.pattern !== undefined && diff --git a/src/tools/tool-schema-codepoints.test.mjs b/src/tools/tool-schema-codepoints.test.mjs new file mode 100644 index 000000000..c379b77e9 --- /dev/null +++ b/src/tools/tool-schema-codepoints.test.mjs @@ -0,0 +1,20 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { validateValue } from './schema.js'; +test('tool string schemas count Unicode characters rather than UTF-16 units', () => { + const value = '𠮷野家😀'; + assert.deepEqual(validateValue({ type: 'string', maxLength: 4 }, value), []); + assert.equal( + validateValue({ type: 'string', minLength: 5 }, value).length, + 1, + ); + assert.equal( + validateValue({ type: 'string', maxLength: 3 }, value).length, + 1, + ); + assert.deepEqual( + validateValue({ type: 'string', minLength: 4, maxLength: 4 }, 'abcd'), + [], + ); + assert.equal(validateValue({ type: 'string', minLength: 2 }, '𠮷').length, 1); +}); From 615ba9a00826a50c2907184b9193c6e6d51bdc0b Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 11:12:53 -0400 Subject: [PATCH 2/3] fix: address review feedback for PR #1026 --- docs/CURRENT-STATE.md | 7 ++----- src/tools/schema.js | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 7e1970768..e66800fd5 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -73,6 +73,8 @@ Windows/Claude Desktop Cowork confirmation remains outstanding (#927). ## Tools and local MCP server — October 1, 2026 +Tool argument string lengths follow JSON Schema Unicode code-point counts, so supplementary characters count once rather than as two UTF-16 units. + `gods-eye-view/tools` defines queries that answer questions from the app's data, and `gods-eye-view/tools/mcp` exposes a composed catalog over the Model Context Protocol. `npm run mcp` serves Core's tools over stdio to a local MCP client, @@ -5310,8 +5312,3 @@ datacenters, dams and the Nepal locator retain that shared credit while displaye Military fragments stitch by source identity. Different mapped parcels merge only when their footprints touch or contain one another, never across a seam tolerance gap. - - -### fix(tools): enforce string schema lengths by Unicode code point - -Count code points for minLength and maxLength, preserving ASCII behavior and regular-expression validation. diff --git a/src/tools/schema.js b/src/tools/schema.js index cdb63cfc1..b6e8e2fd0 100644 --- a/src/tools/schema.js +++ b/src/tools/schema.js @@ -106,7 +106,8 @@ export function validateValue(schema, value, path = 'arguments') { fail('must be a string'); break; } - const length = [...value].length; + let length = 0; + for (const character of value) length += 1; if (schema.minLength != null && length < schema.minLength) fail(`must be at least ${schema.minLength} characters`); if (schema.maxLength != null && length > schema.maxLength) From c12938cd05b843ca00e8bf0ac4be5d5c5770acaa Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Wed, 14 Oct 2026 15:05:40 -0700 Subject: [PATCH 3/3] perf(tools): stop counting string code points once the bounds are decided Strings without length bounds are not counted, and counting stops at the larger bound, so a huge tool argument costs no more than its limit. --- src/tools/schema.js | 20 ++++++++++++++------ src/tools/tool-schema-codepoints.test.mjs | 13 +++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/tools/schema.js b/src/tools/schema.js index b6e8e2fd0..f857825a1 100644 --- a/src/tools/schema.js +++ b/src/tools/schema.js @@ -106,12 +106,20 @@ export function validateValue(schema, value, path = 'arguments') { fail('must be a string'); break; } - let length = 0; - for (const character of value) length += 1; - if (schema.minLength != null && length < schema.minLength) - fail(`must be at least ${schema.minLength} characters`); - if (schema.maxLength != null && length > schema.maxLength) - fail(`must be at most ${schema.maxLength} characters`); + if (schema.minLength != null || schema.maxLength != null) { + // Code points, as JSON Schema counts. Counting stops once both bounds + // are decided, so a huge argument costs no more than its limit. + const limit = Math.max( + schema.minLength ?? 0, + schema.maxLength != null ? schema.maxLength + 1 : 0, + ); + let length = 0; + for (const _ of value) if (++length >= limit) break; + if (schema.minLength != null && length < schema.minLength) + fail(`must be at least ${schema.minLength} characters`); + if (schema.maxLength != null && length > schema.maxLength) + fail(`must be at most ${schema.maxLength} characters`); + } if ( schema.pattern !== undefined && !new RegExp(schema.pattern, 'u').test(value) diff --git a/src/tools/tool-schema-codepoints.test.mjs b/src/tools/tool-schema-codepoints.test.mjs index c379b77e9..27af78fa3 100644 --- a/src/tools/tool-schema-codepoints.test.mjs +++ b/src/tools/tool-schema-codepoints.test.mjs @@ -18,3 +18,16 @@ test('tool string schemas count Unicode characters rather than UTF-16 units', () ); assert.equal(validateValue({ type: 'string', minLength: 2 }, '𠮷').length, 1); }); + +test('tool string bounds hold at their edges and on huge arguments', () => { + const string = (bounds) => ({ type: 'string', ...bounds }); + assert.deepEqual(validateValue(string({ maxLength: 3 }), '😀😀😀'), []); + assert.equal(validateValue(string({ maxLength: 3 }), '😀😀😀😀').length, 1); + assert.deepEqual(validateValue(string({ minLength: 3 }), '😀😀😀'), []); + assert.deepEqual(validateValue(string({}), 'x'.repeat(1e6)), []); + // Counting all of it takes roughly 0.7 s; stopping at the limit takes none. + const huge = 'x'.repeat(1e8); + const started = performance.now(); + assert.equal(validateValue(string({ maxLength: 60 }), huge).length, 1); + assert.ok(performance.now() - started < 200, 'stops counting past the limit'); +});