mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge pull request #1026 from rudycelekli/contrib/gods-005-tool-schema-codepoints-20261011-reviewed
fix(tools): enforce string schema lengths by Unicode code point
This commit is contained in:
4 files changed
+51
-4
No files matched your search
@@ -24,6 +24,8 @@
|
|||||||
the five borough cores load and `CCTV_NYC_MAX_SOURCES` changes the cap. The catalog-wide ceiling is
|
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.
|
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.
|
||||||
|
|
||||||
- Director timeline seeks cross the dateline along the short longitude arc.
|
- Director timeline seeks cross the dateline along the short longitude arc.
|
||||||
|
|
||||||
- Treat explicitly current, complete empty vessel coverage as a successful update,
|
- Treat explicitly current, complete empty vessel coverage as a successful update,
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ Windows/Claude Desktop Cowork confirmation remains outstanding (#927).
|
|||||||
|
|
||||||
## Tools and local MCP server — October 1, 2026
|
## 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,
|
`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
|
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,
|
Protocol. `npm run mcp` serves Core's tools over stdio to a local MCP client,
|
||||||
|
|||||||
+14
-4
@@ -106,10 +106,20 @@ export function validateValue(schema, value, path = 'arguments') {
|
|||||||
fail('must be a string');
|
fail('must be a string');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (schema.minLength != null && value.length < schema.minLength)
|
if (schema.minLength != null || schema.maxLength != null) {
|
||||||
fail(`must be at least ${schema.minLength} characters`);
|
// Code points, as JSON Schema counts. Counting stops once both bounds
|
||||||
if (schema.maxLength != null && value.length > schema.maxLength)
|
// are decided, so a huge argument costs no more than its limit.
|
||||||
fail(`must be at most ${schema.maxLength} characters`);
|
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 (
|
if (
|
||||||
schema.pattern !== undefined &&
|
schema.pattern !== undefined &&
|
||||||
!new RegExp(schema.pattern, 'u').test(value)
|
!new RegExp(schema.pattern, 'u').test(value)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
Reference in new issue
Block a user