diff --git a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.i18n.yaml
index ae5592ef01..2f7a4b1adb 100644
--- a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-2026-07-20-unified-json-value-schema-dsl.md: 6a3dc21a7ae76ba8f84f5b3edf8306285a02a683
-2026-07-20-unified-json-value-schema-dsl.zh.md: 77ac599e6713563528d0382dfecff83071488930
+2026-07-20-unified-json-value-schema-dsl.md: e735472ee0ac6a696462aa7598d57fce04aa9c3e
+2026-07-20-unified-json-value-schema-dsl.zh.md: dd34817a6d047b14346b35ba6c4bce0290ad7feb
diff --git a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.md b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.md
index 6a3dc21a7a..e735472ee0 100644
--- a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.md
+++ b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.md
@@ -12,7 +12,7 @@ Tool parameters used a small author DSL while subagent/workflow structured outpu
`dsh-tools` owns one JSON-value schema vocabulary with two representations. `ValueSchemaSpec` is the author form for any JSON root; `ParameterSchemaSpec` is its implicit object-property-map form with per-property `required: true`. `JsonSchemaNode` is the raw wire form. Both support string, finite number, integer, boolean, null, array, object, type-correct scalar `enum`/`const`, and exact-one `oneOf`; `{ type: 'json' }` is author-only sugar for an annotation-only unconstrained raw node.
-An explicit author object must declare `additionalProperties: true | false`. The implicit parameter root and raw JSON Schema preserve the standard open default. `InferValue` and `InferArgs
` derive TypeScript values from the same declarations that `valueSchemaSpecToJsonSchema()` and `parameterSchemaSpecToJsonSchema()` compile. `assertSupportedJsonSchema()` rejects unsupported or misplaced keywords, and `validateJsonSchemaValue()` enforces the accepted subset against the lossless `JsonValue` boundary: no `undefined`, negative zero, non-finite numbers, sparse arrays, cycles, exotic objects, functions, symbols, or other coercive values. Intrinsic plain Object and Array containers remain plain across JavaScript realms; subclasses remain exotic. Validation and snapshot traversal are iterative, so valid nesting is limited by available memory rather than the JavaScript call stack.
+An explicit author object must declare `additionalProperties: true | false`. The implicit parameter root and raw JSON Schema preserve the standard open default. `InferValue` and `InferArgs
` derive TypeScript values from the same declarations that `valueSchemaSpecToJsonSchema()` and `parameterSchemaSpecToJsonSchema()` compile. `assertSupportedJsonSchema()` rejects unsupported or misplaced keywords, and `validateJsonSchemaValue()` enforces the accepted subset against the lossless `JsonValue` boundary: no `undefined`, negative zero, non-finite numbers, sparse arrays, cycles, exotic objects, functions, symbols, or other coercive values. Intrinsic plain Object and Array containers remain plain across JavaScript realms; subclasses remain exotic. Author compilation, raw-schema assertion, value validation, schema-to-TypeScript rendering, registry detachment, and dynamic Cordis cross-realm normalization and cloning use explicit work stacks, so valid nesting is limited by available memory rather than the JavaScript call stack.
Object-rooting is a consumer rule rather than a vocabulary restriction. Subagent and workflow caller-defined structured outputs use `assertObjectJsonSchema()` and `ObjectJsonSchema`; tool outputs may use any root. Dynamic Cordis registrations rebuild realm-foreign schemas into host-owned JSON, preserve raw-wrapper openness, and require direct-DSL object openness before calling the same compiler.
@@ -29,4 +29,4 @@ Object-rooting is a consumer rule rather than a vocabulary restriction. Subagent
- Output declarations can infer object, array, scalar, or null roots; subagent/workflow structured outputs remain object-rooted at their existing seams.
- Explicit object openness and type-correct literal constraints make malformed declarations fail during authoring or registration rather than during a later model call.
- Raw tools may still register broader JSON Schema directly, but unified code generation treats unsupported schemas as unknown instead of pretending to enforce them.
-- Runtime and compile-time tests cover every root, exact-one overlap/no-match behavior, raw open defaults, explicit openness, lossy JSON values, and inference.
+- Runtime and compile-time tests cover every root, exact-one overlap/no-match behavior, raw open defaults, explicit openness, lossy JSON values, inference, and deep nesting across core and dynamic projections.
diff --git a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.zh.md b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.zh.md
index 77ac599e67..dd34817a6d 100644
--- a/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.zh.md
@@ -12,7 +12,7 @@ Status: implemented
`dsh-tools` 以两种表示形式统一管理一套 JSON 值 schema 词汇。`ValueSchemaSpec` 是可描述任意 JSON 根类型的作者侧形式;`ParameterSchemaSpec` 是其隐式对象属性映射形式,每个属性可标记 `required: true`。`JsonSchemaNode` 是原始协议表示。两种形式都支持字符串、有限数值、整数、布尔值、null、数组、对象、类型正确的标量 `enum`/`const`,以及要求恰好匹配一个分支的 `oneOf`;`{ type: 'json' }` 仅是作者侧语法糖,会编译为仅含注解、不施加约束的原始节点。
-显式的作者侧对象必须声明 `additionalProperties: true | false`。隐式参数根对象和原始 JSON Schema 保留标准的默认开放语义。`InferValue` 和 `InferArgs
` 根据同一份声明推导 TypeScript 值,`valueSchemaSpecToJsonSchema()` 和 `parameterSchemaSpecToJsonSchema()` 也将这些声明编译为 JSON Schema。`assertSupportedJsonSchema()` 会拒绝不受支持或位置错误的关键字;`validateJsonSchemaValue()` 则以无损 `JsonValue` 边界校验受支持的子集,不允许 `undefined`、负零、非有限数、稀疏数组、循环引用、非普通对象、函数、symbol 及其他需要强制转换的值。内建的普通 Object 和 Array 容器跨 JavaScript 运行域后仍视为普通容器;其子类仍视为非普通对象。校验和快照遍历均以迭代方式执行,因此合法嵌套的深度上限由可用内存决定,而非 JavaScript 调用栈。
+显式的作者侧对象必须声明 `additionalProperties: true | false`。隐式参数根对象和原始 JSON Schema 保留标准的默认开放语义。`InferValue` 和 `InferArgs
` 根据同一份声明推导 TypeScript 值,`valueSchemaSpecToJsonSchema()` 和 `parameterSchemaSpecToJsonSchema()` 也将这些声明编译为 JSON Schema。`assertSupportedJsonSchema()` 会拒绝不受支持或位置错误的关键字;`validateJsonSchemaValue()` 则以无损 `JsonValue` 边界校验受支持的子集,不允许 `undefined`、负零、非有限数、稀疏数组、循环引用、非普通对象、函数、symbol 及其他需要强制转换的值。内建的普通 Object 和 Array 容器跨 JavaScript 运行域后仍视为普通容器;其子类仍视为非普通对象。作者侧 schema 编译、原始 schema 断言、值校验、schema 到 TypeScript 的渲染、注册表数据分离,以及动态 Cordis 的跨运行域规范化与克隆均使用显式工作栈,因此合法嵌套的深度上限由可用内存决定,而非 JavaScript 调用栈。
对象根限制属于消费方规则,不属于 schema 词汇本身。subagent 和工作流中由调用方定义的结构化输出通过 `assertObjectJsonSchema()` 和 `ObjectJsonSchema` 保持对象根限制;工具输出可以使用任意根类型。动态 Cordis 注册会把跨 JavaScript 运行域传入的 schema 重建为当前运行时持有的 JSON,保留原始包装层的默认开放语义,并要求直接使用 DSL 声明的对象明确选择开放方式,然后再调用同一编译器。
@@ -29,4 +29,4 @@ Status: implemented
- 输出声明可以推导对象、数组、标量或 null 根类型;subagent/工作流的结构化输出仍在其现有服务边界保持对象根限制。
- 显式的对象开放方式和类型正确的字面量约束会让格式错误的声明在编写或注册阶段快速失败,而不是拖到后续模型调用时才失败。
- 原始工具仍可直接注册范围更广的 JSON Schema,但统一代码生成会把不受支持的 schema 视为未知类型,不会假装自己能够强制执行。
-- 运行时和编译期测试覆盖所有根类型、恰好匹配一个分支时的重叠/无匹配行为、原始 schema 的默认开放语义、显式开放方式、有损 JSON 值和类型推导。
+- 运行时和编译期测试覆盖所有根类型、恰好匹配一个分支时的重叠/无匹配行为、原始 schema 的默认开放语义、显式开放方式、有损 JSON 值、类型推导,以及核心投影和动态投影中的深层嵌套。
diff --git a/packages/cordis/tool-cordis/README.md b/packages/cordis/tool-cordis/README.md
index 7819cb8c2c..babf259761 100644
--- a/packages/cordis/tool-cordis/README.md
+++ b/packages/cordis/tool-cordis/README.md
@@ -12,7 +12,7 @@ Exact model-facing schemas: [the generated tool catalog](../../../docs/tool-cata
## Trust stance
-The sandbox isolates globals but is not a security boundary. Node globals are absent or redirect to Cordis services such as `ctx.fs`, `ctx.web`, and `ctx.bash`, and writes to `globalThis` stay local, but host-realm helpers make escape possible. Mounted plugins receive a façade without framework internals, yet its allowed services affect the live runtime. Treat this toolset like bash access; see the [design and trust stance](../../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md).
+The sandbox isolates globals but is not a security boundary. Node globals are absent or redirect to Cordis services such as `ctx.fs`, `ctx.web`, and `ctx.bash`, and writes to `globalThis` stay local, but host-realm helpers make escape possible. Mounted plugins receive a façade without framework internals, yet its allowed services affect the live runtime. Dynamic tool schemas and annotations cross the realm through iterative JSON cloning and schema normalization, so valid deep declarations are memory-bounded rather than call-stack-bounded. Treat this toolset like bash access; see the [design and trust stance](../../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md).
## Config
diff --git a/packages/cordis/tool-cordis/src/guard.ts b/packages/cordis/tool-cordis/src/guard.ts
index 2ebba0fbd8..bf225948be 100644
--- a/packages/cordis/tool-cordis/src/guard.ts
+++ b/packages/cordis/tool-cordis/src/guard.ts
@@ -57,45 +57,105 @@ function hasPlainArrayPrototype(value: unknown[]): boolean {
}
/* jscpd:ignore-end */
+/** Where one cloned JSON value is installed. */
+type CloneDestination =
+ | { kind: 'root' }
+ | { kind: 'array'; target: unknown[]; index: number }
+ | { kind: 'object'; target: Record; key: string }
+
+/** Deferred work for stack-safe cross-realm JSON cloning. */
+type CloneTask =
+ | { kind: 'visit'; value: unknown; path: string; destination: CloneDestination }
+ | { kind: 'array-item'; source: unknown[]; index: number; path: string; target: unknown[] }
+ | { kind: 'leave'; source: object }
+
/** Materialize realm-foreign lossless JSON without allowing JSON.stringify coercions. */
-function cloneJson(value: unknown, path: string, seen = new Set()): unknown {
- if (value === null || typeof value === 'string' || typeof value === 'boolean') return value
- if (typeof value === 'number') {
- if (Number.isFinite(value) && !Object.is(value, -0)) return value
- throw new Error(`harness.defineTool ${path} must be lossless JSON data`)
- }
- if (typeof value !== 'object') throw new Error(`harness.defineTool ${path} must be lossless JSON data`)
- if (seen.has(value)) throw new Error(`harness.defineTool ${path} must be lossless JSON data`)
- seen.add(value)
- try {
- if (Array.isArray(value)) {
- if (!hasPlainArrayPrototype(value) || Reflect.ownKeys(value).length !== value.length + 1) {
- throw new Error(`harness.defineTool ${path} must be lossless JSON data`)
- }
- const output: unknown[] = []
- for (let index = 0; index < value.length; index++) {
- if (!Object.hasOwn(value, index)) throw new Error(`harness.defineTool ${path} must be lossless JSON data`)
- output.push(cloneJson(value[index], `${path}[${index}]`, seen))
- }
- return output
+function cloneJson(value: unknown, path: string): unknown {
+ const ancestors = new Set()
+ let root: unknown
+ const assign = (destination: CloneDestination, item: unknown): void => {
+ if (destination.kind === 'root') {
+ root = item
+ return
}
- if (!isPlainRecord(value)) throw new Error(`harness.defineTool ${path} must be lossless JSON data`)
- if (Reflect.ownKeys(value).some(key => typeof key !== 'string' || !Object.prototype.propertyIsEnumerable.call(value, key))) {
- throw new Error(`harness.defineTool ${path} must be lossless JSON data`)
+ if (destination.kind === 'array') {
+ destination.target[destination.index] = item
+ return
+ }
+ Object.defineProperty(destination.target, destination.key, {
+ value: item,
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ })
+ }
+ const reject = (at: string): never => {
+ throw new Error(`harness.defineTool ${at} must be lossless JSON data`)
+ }
+
+ const tasks: CloneTask[] = [{ kind: 'visit', value, path, destination: { kind: 'root' } }]
+ for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
+ if (task.kind === 'leave') {
+ ancestors.delete(task.source)
+ continue
+ }
+ if (task.kind === 'array-item') {
+ if (!Object.hasOwn(task.source, task.index)) reject(task.path)
+ tasks.push({
+ kind: 'visit',
+ value: task.source[task.index],
+ path: `${task.path}[${task.index}]`,
+ destination: { kind: 'array', target: task.target, index: task.index },
+ })
+ continue
+ }
+
+ const current = task.value
+ if (current === null || typeof current === 'string' || typeof current === 'boolean') {
+ assign(task.destination, current)
+ continue
+ }
+ if (typeof current === 'number') {
+ if (!Number.isFinite(current) || Object.is(current, -0)) reject(task.path)
+ assign(task.destination, current)
+ continue
+ }
+ if (typeof current !== 'object' || ancestors.has(current)) reject(task.path)
+
+ if (Array.isArray(current)) {
+ if (!hasPlainArrayPrototype(current) || Reflect.ownKeys(current).length !== current.length + 1) reject(task.path)
+ const output: unknown[] = []
+ assign(task.destination, output)
+ ancestors.add(current)
+ tasks.push({ kind: 'leave', source: current })
+ for (let index = current.length - 1; index >= 0; index--) {
+ tasks.push({ kind: 'array-item', source: current, index, path: task.path, target: output })
+ }
+ continue
+ }
+ if (!isPlainRecord(current)) reject(task.path)
+ const record = current as Record
+ if (Reflect.ownKeys(record).some(key => typeof key !== 'string' || !Object.prototype.propertyIsEnumerable.call(record, key))) {
+ reject(task.path)
}
const output: Record = {}
- for (const [key, entry] of Object.entries(value)) {
- Object.defineProperty(output, key, {
- value: cloneJson(entry, `${path}.${key}`, seen),
- enumerable: true,
- configurable: true,
- writable: true,
+ assign(task.destination, output)
+ ancestors.add(record)
+ tasks.push({ kind: 'leave', source: record })
+ const entries = Object.entries(record)
+ for (let index = entries.length - 1; index >= 0; index--) {
+ const entry = entries[index]
+ /* v8 ignore next -- the loop is bounded by the captured entry count. */
+ if (entry === undefined) continue
+ tasks.push({
+ kind: 'visit',
+ value: entry[1],
+ path: `${task.path}.${entry[0]}`,
+ destination: { kind: 'object', target: output, key: entry[0] },
})
}
- return output
- } finally {
- seen.delete(value)
}
+ return root
}
/** Copy and realm-materialize the shared annotation vocabulary. */
@@ -160,111 +220,229 @@ function normalizeRequiredNames(value: unknown, properties: Record
+}
+
+/** Where a normalized value node is installed. */
+type NormalizeValueDestination =
+ | { kind: 'property'; target: Record; key: string }
+ | { kind: 'item'; target: Record }
+ | { kind: 'one-of'; target: Record[]; index: number }
+
+/** Where a normalized property map is installed. */
+type NormalizeMapDestination =
+ | { kind: 'root'; holder: NormalizeRoot }
+ | { kind: 'properties'; target: Record }
+
+/** Deferred work for stack-safe sandbox schema normalization. */
+type NormalizeTask =
+ | {
+ kind: 'map'
+ entries: Record
+ path: string
+ requiredNames: ReadonlySet
+ raw: boolean
+ destination: NormalizeMapDestination
+ }
+ | {
+ kind: 'value'
+ value: unknown
+ path: string
+ forceRequired: boolean
+ raw: boolean
+ parameterProperty: boolean
+ destination: NormalizeValueDestination
+ }
+ | { kind: 'leave'; value: object }
+
+/** Install one normalized node without `__proto__` assignment semantics. */
+function assignNormalizedValue(destination: NormalizeValueDestination, value: Record): void {
+ if (destination.kind === 'property') {
+ Object.defineProperty(destination.target, destination.key, {
+ value,
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ })
+ } else if (destination.kind === 'item') {
+ destination.target.items = value
+ } else {
+ destination.target[destination.index] = value
+ }
+}
+
+/** Install one normalized property map at its root or containing object. */
+function assignNormalizedMap(destination: NormalizeMapDestination, value: Record): void {
+ if (destination.kind === 'root') destination.holder.value = value
+ else destination.target.properties = value
+}
+
+/** Normalize one implicit property map and all descendants with explicit work frames. */
function normalizePropertyMap(
entries: Record,
path: string,
requiredNames: ReadonlySet,
raw: boolean,
): Record {
- const spec: Record = {}
- for (const [key, prop] of Object.entries(entries)) {
- Object.defineProperty(spec, key, {
- value: normalizeValueSchema(prop, `${path}.${key}`, requiredNames.has(key), raw, true),
- enumerable: true,
- configurable: true,
- writable: true,
- })
- }
- return spec
-}
-
-/** Normalize one property or nested value schema into the host realm. */
-function normalizeValueSchema(
- value: unknown,
- path: string,
- forceRequired = false,
- raw = false,
- parameterProperty = false,
-): Record {
- if (!isPlainRecord(value)) {
- throw new Error(`harness.defineTool ${path} must be a ParameterSchemaSpec property object`)
- }
- const requiredKey = parameterProperty && !raw ? ['required'] : []
- if (parameterProperty && raw && Object.hasOwn(value, 'required') && value.type !== 'object') {
- throw new Error(`harness.defineTool ${path}.required belongs to the containing raw object schema`)
- }
- if (parameterProperty && !raw && Object.hasOwn(value, 'required') && value.required !== true) {
- throw new Error(`harness.defineTool ${path}.required must be true when present`)
- }
- const prop: Record = {}
- if (forceRequired || value.required === true) prop.required = true
- copyAnnotations(value, prop, path)
-
- if (Object.hasOwn(value, 'oneOf')) {
- assertSchemaKeys(value, path, ['oneOf', ...requiredKey, ...ANNOTATION_KEYS])
- if (!Array.isArray(value.oneOf)) throw new Error(`harness.defineTool ${path}.oneOf must contain at least two schemas`)
- prop.oneOf = value.oneOf.map((branch, index) => normalizeValueSchema(branch, `${path}.oneOf[${index}]`, false, raw))
- return prop
- }
-
- if (raw && !Object.hasOwn(value, 'type')) {
- assertSchemaKeys(value, path, ANNOTATION_KEYS)
- prop.type = 'json'
- return prop
- }
- if (!SCHEMA_TYPES.has(value.type) || raw && value.type === 'json') {
- throw new Error(`harness.defineTool ${path} must declare a valid type: ${VALID_TYPES} (got ${JSON.stringify(value.type)})`)
- }
- const type = value.type
- prop.type = type
-
- switch (type) {
- case 'object': {
- assertSchemaKeys(value, path, ['type', 'properties', 'additionalProperties', ...requiredKey, ...(raw ? ['required'] : []), ...ANNOTATION_KEYS])
- if (!raw && (!Object.hasOwn(value, 'additionalProperties') || typeof value.additionalProperties !== 'boolean')) {
- throw new Error(`harness.defineTool ${path}.additionalProperties must be explicitly true or false`)
- }
- if (raw && Object.hasOwn(value, 'additionalProperties') && typeof value.additionalProperties !== 'boolean') {
- throw new Error(`harness.defineTool ${path}.additionalProperties must be a boolean`)
- }
- if (raw && Object.hasOwn(value, 'required') && value.required === undefined) {
- throw new Error(`harness.defineTool ${path}.required must be an array of declared property names`)
- }
- prop.additionalProperties = raw ? value.additionalProperties ?? true : value.additionalProperties
- if (Object.hasOwn(value, 'properties')) {
- if (!isPlainRecord(value.properties)) throw new Error(`harness.defineTool ${path}.properties must be an object of schemas`)
- const nestedRequired = raw ? normalizeRequiredNames(value.required, value.properties, `${path}.required`) : new Set()
- prop.properties = normalizePropertyMap(value.properties, `${path}.properties`, nestedRequired, raw)
- } else if (raw && value.required !== undefined) {
- normalizeRequiredNames(value.required, {}, `${path}.required`)
- }
- return prop
+ const holder: NormalizeRoot = {}
+ const ancestors = new Set()
+ const tasks: NormalizeTask[] = [{
+ kind: 'map',
+ entries,
+ path,
+ requiredNames,
+ raw,
+ destination: { kind: 'root', holder },
+ }]
+ for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
+ if (task.kind === 'leave') {
+ ancestors.delete(task.value)
+ continue
}
- case 'array':
- assertSchemaKeys(value, path, ['type', 'items', ...requiredKey, ...ANNOTATION_KEYS])
- if (Object.hasOwn(value, 'items')) prop.items = normalizeValueSchema(value.items, `${path}.items`, false, raw)
- return prop
- case 'string':
- case 'number':
- case 'integer':
- case 'boolean':
- case 'null':
- assertSchemaKeys(value, path, ['type', 'enum', 'const', ...requiredKey, ...ANNOTATION_KEYS])
- if (Object.hasOwn(value, 'enum')) {
- prop.enum = Array.isArray(value.enum)
- ? value.enum.map((entry, index) => cloneJson(entry, `${path}.enum[${index}]`))
- : value.enum
+ if (task.kind === 'map') {
+ if (ancestors.has(task.entries)) throw new Error(`harness.defineTool ${task.path} is circular`)
+ ancestors.add(task.entries)
+ const spec: Record = {}
+ assignNormalizedMap(task.destination, spec)
+ tasks.push({ kind: 'leave', value: task.entries })
+ const mapEntries = Object.entries(task.entries)
+ for (let index = mapEntries.length - 1; index >= 0; index--) {
+ const entry = mapEntries[index]
+ /* v8 ignore next -- the loop is bounded by the captured entry count. */
+ if (entry === undefined) continue
+ tasks.push({
+ kind: 'value',
+ value: entry[1],
+ path: `${task.path}.${entry[0]}`,
+ forceRequired: task.requiredNames.has(entry[0]),
+ raw: task.raw,
+ parameterProperty: true,
+ destination: { kind: 'property', target: spec, key: entry[0] },
+ })
}
- if (Object.hasOwn(value, 'const')) prop.const = cloneJson(value.const, `${path}.const`)
- return prop
- case 'json':
- assertSchemaKeys(value, path, ['type', ...requiredKey, ...ANNOTATION_KEYS])
- return prop
- /* v8 ignore next 2 -- SCHEMA_TYPES narrows this closed switch before dispatch. */
- default:
- throw new Error(`harness.defineTool ${path} must declare a valid type: ${VALID_TYPES}`)
+ continue
+ }
+
+ const { value, path } = task
+ if (!isPlainRecord(value)) {
+ throw new Error(`harness.defineTool ${path} must be a ParameterSchemaSpec property object`)
+ }
+ if (ancestors.has(value)) throw new Error(`harness.defineTool ${path} is circular`)
+ ancestors.add(value)
+ const requiredKey = task.parameterProperty && !task.raw ? ['required'] : []
+ if (task.parameterProperty && task.raw && Object.hasOwn(value, 'required') && value.type !== 'object') {
+ throw new Error(`harness.defineTool ${path}.required belongs to the containing raw object schema`)
+ }
+ if (task.parameterProperty && !task.raw && Object.hasOwn(value, 'required') && value.required !== true) {
+ throw new Error(`harness.defineTool ${path}.required must be true when present`)
+ }
+ const prop: Record = {}
+ assignNormalizedValue(task.destination, prop)
+ tasks.push({ kind: 'leave', value })
+ if (task.forceRequired || value.required === true) prop.required = true
+ copyAnnotations(value, prop, path)
+
+ if (Object.hasOwn(value, 'oneOf')) {
+ assertSchemaKeys(value, path, ['oneOf', ...requiredKey, ...ANNOTATION_KEYS])
+ if (!Array.isArray(value.oneOf)) throw new Error(`harness.defineTool ${path}.oneOf must contain at least two schemas`)
+ const oneOf: Record[] = []
+ prop.oneOf = oneOf
+ for (let index = value.oneOf.length - 1; index >= 0; index--) {
+ tasks.push({
+ kind: 'value',
+ value: value.oneOf[index],
+ path: `${path}.oneOf[${index}]`,
+ forceRequired: false,
+ raw: task.raw,
+ parameterProperty: false,
+ destination: { kind: 'one-of', target: oneOf, index },
+ })
+ }
+ continue
+ }
+
+ if (task.raw && !Object.hasOwn(value, 'type')) {
+ assertSchemaKeys(value, path, ANNOTATION_KEYS)
+ prop.type = 'json'
+ continue
+ }
+ if (!SCHEMA_TYPES.has(value.type) || task.raw && value.type === 'json') {
+ throw new Error(`harness.defineTool ${path} must declare a valid type: ${VALID_TYPES} (got ${JSON.stringify(value.type)})`)
+ }
+ const type = value.type
+ prop.type = type
+
+ switch (type) {
+ case 'object': {
+ assertSchemaKeys(value, path, ['type', 'properties', 'additionalProperties', ...requiredKey, ...(task.raw ? ['required'] : []), ...ANNOTATION_KEYS])
+ if (!task.raw && (!Object.hasOwn(value, 'additionalProperties') || typeof value.additionalProperties !== 'boolean')) {
+ throw new Error(`harness.defineTool ${path}.additionalProperties must be explicitly true or false`)
+ }
+ if (task.raw && Object.hasOwn(value, 'additionalProperties') && typeof value.additionalProperties !== 'boolean') {
+ throw new Error(`harness.defineTool ${path}.additionalProperties must be a boolean`)
+ }
+ if (task.raw && Object.hasOwn(value, 'required') && value.required === undefined) {
+ throw new Error(`harness.defineTool ${path}.required must be an array of declared property names`)
+ }
+ prop.additionalProperties = task.raw ? value.additionalProperties ?? true : value.additionalProperties
+ if (Object.hasOwn(value, 'properties')) {
+ const properties = value.properties
+ if (!isPlainRecord(properties)) throw new Error(`harness.defineTool ${path}.properties must be an object of schemas`)
+ const nestedRequired = task.raw
+ ? normalizeRequiredNames(value.required, properties, `${path}.required`)
+ : new Set()
+ tasks.push({
+ kind: 'map',
+ entries: properties,
+ path: `${path}.properties`,
+ requiredNames: nestedRequired,
+ raw: task.raw,
+ destination: { kind: 'properties', target: prop },
+ })
+ } else if (task.raw && value.required !== undefined) {
+ normalizeRequiredNames(value.required, {}, `${path}.required`)
+ }
+ break
+ }
+ case 'array':
+ assertSchemaKeys(value, path, ['type', 'items', ...requiredKey, ...ANNOTATION_KEYS])
+ if (Object.hasOwn(value, 'items')) {
+ tasks.push({
+ kind: 'value',
+ value: value.items,
+ path: `${path}.items`,
+ forceRequired: false,
+ raw: task.raw,
+ parameterProperty: false,
+ destination: { kind: 'item', target: prop },
+ })
+ }
+ break
+ case 'string':
+ case 'number':
+ case 'integer':
+ case 'boolean':
+ case 'null':
+ assertSchemaKeys(value, path, ['type', 'enum', 'const', ...requiredKey, ...ANNOTATION_KEYS])
+ if (Object.hasOwn(value, 'enum')) {
+ prop.enum = Array.isArray(value.enum)
+ ? Array.from(value.enum, (entry, index) => cloneJson(entry, `${path}.enum[${index}]`))
+ : value.enum
+ }
+ if (Object.hasOwn(value, 'const')) prop.const = cloneJson(value.const, `${path}.const`)
+ break
+ case 'json':
+ assertSchemaKeys(value, path, ['type', ...requiredKey, ...ANNOTATION_KEYS])
+ break
+ /* v8 ignore next 2 -- SCHEMA_TYPES narrows this closed switch before dispatch. */
+ default:
+ throw new Error(`harness.defineTool ${path} must declare a valid type: ${VALID_TYPES}`)
+ }
}
+ /* v8 ignore next -- the root map task assigns before scheduling descendants. */
+ return holder.value ?? {}
}
function markDynamicTool(tool: ToolDefinition): DynamicToolDefinition {
diff --git a/packages/cordis/tool-cordis/tests/mount.spec.ts b/packages/cordis/tool-cordis/tests/mount.spec.ts
index 22df219b1b..347e87c76a 100644
--- a/packages/cordis/tool-cordis/tests/mount.spec.ts
+++ b/packages/cordis/tool-cordis/tests/mount.spec.ts
@@ -274,6 +274,59 @@ describe('cordis_mount', () => {
})
})
+ it('normalizes and snapshots deeply nested sandbox schemas and annotations stack-safely', async () => {
+ const ctx = await setup()
+ const depth = 5_000
+ const result = await call(ctx, 'cordis_mount', {
+ code: `
+ return {
+ name: 'deep-unified-schema',
+ inject: ['tools'],
+ apply(ctx) {
+ let choice = { type: 'string' }
+ let example = 'leaf'
+ for (let index = 0; index < ${depth}; index++) {
+ choice = { oneOf: [choice, { type: 'null' }] }
+ example = [example]
+ }
+ harness.registerTool(ctx, harness.defineTool({
+ name: 'deep_unified_schema_tool',
+ description: 'deep unified nodes',
+ parameters: {
+ choice: { ...choice, required: true },
+ any: { type: 'json', default: example },
+ },
+ async execute() { return [] },
+ }))
+ },
+ }
+ `,
+ })
+ expect(result.isError).toBe(false)
+
+ const parameters = ctx.tools.schemas().find(s => s.name === 'deep_unified_schema_tool')!.parameters as {
+ properties: Record>
+ }
+ let choice = parameters.properties.choice!
+ let choiceDepth = 0
+ while (Array.isArray(choice.oneOf)) {
+ choice = choice.oneOf[0] as Record
+ choiceDepth++
+ }
+ let example: unknown = parameters.properties.any!.default
+ let exampleDepth = 0
+ while (Array.isArray(example)) {
+ example = example[0]
+ exampleDepth++
+ }
+ expect({ choiceDepth, choice, exampleDepth, example }).toEqual({
+ choiceDepth: depth,
+ choice: { type: 'string' },
+ exampleDepth: depth,
+ example: 'leaf',
+ })
+ })
+
it('normalizes unconstrained and closed nested nodes from a raw JSON Schema wrapper', async () => {
const ctx = await setup()
const result = await call(ctx, 'cordis_mount', {
@@ -372,6 +425,47 @@ describe('cordis_mount', () => {
expect(text(result)).toContain(message)
})
+ it.each([
+ [
+ `
+ const parameters = {}
+ const item = { type: 'array' }
+ item.items = item
+ parameters.item = item
+ `,
+ 'parameters.item.items is circular',
+ ],
+ [
+ `
+ const parameters = {}
+ const item = { type: 'object', additionalProperties: true, properties: parameters }
+ parameters.item = item
+ `,
+ 'parameters.item.properties is circular',
+ ],
+ ])('rejects circular sandbox schemas without exhausting the call stack', async (declaration, message) => {
+ const ctx = await setup()
+ const result = await call(ctx, 'cordis_mount', {
+ code: `
+ return {
+ name: 'circular-schema',
+ inject: ['tools'],
+ apply(ctx) {
+ ${declaration}
+ harness.registerTool(ctx, harness.defineTool({
+ name: 'circular_schema_tool',
+ description: 'circular',
+ parameters,
+ async execute() { return [] },
+ }))
+ },
+ }
+ `,
+ })
+ expect(result.isError).toBe(true)
+ expect(text(result)).toContain(message)
+ })
+
it('preserves literal __proto__ keys in sandbox schemas and annotations', async () => {
const ctx = await setup()
const result = await call(ctx, 'cordis_mount', {
diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md
index bc1f9ffeac..2105120241 100644
--- a/packages/core/tools/README.md
+++ b/packages/core/tools/README.md
@@ -84,7 +84,7 @@ ctx.tools.register(defineTool({
}))
```
-The unified schema DSL uses `ParameterSchemaSpec` for the implicit open parameter object and `ValueSchemaSpec` for any JSON-value root. It supports `string`, `number`, `integer`, `boolean`, `null`, `array`, `object`, author-only `json`, and exact-one `oneOf`; scalar `enum`/`const` values are type-correct. Every explicit DSL object declares `additionalProperties: true | false`, while the implicit parameter root and raw JSON Schema keep the standard open default.
+The unified schema DSL uses `ParameterSchemaSpec` for the implicit open parameter object and `ValueSchemaSpec` for any JSON-value root. It supports `string`, `number`, `integer`, `boolean`, `null`, `array`, `object`, author-only `json`, and exact-one `oneOf`; scalar `enum`/`const` values are type-correct. Every explicit DSL object declares `additionalProperties: true | false`, while the implicit parameter root and raw JSON Schema keep the standard open default. Compilation, validation, registry detachment, and schema-to-TypeScript rendering use explicit work stacks, so valid deep schemas are memory-bounded rather than call-stack-bounded.
A `defineTool` definition validates model arguments before execution and turns missing required values, wrong primitives, invalid enum members, and nested violations into `ToolArgsError` (`INVALID_ARGS`) for the normal error-result path. The implicit parameter root is open; an explicit object accepts extra keys only with `additionalProperties: true`, and a closed object with no declared properties accepts only `{}`. Raw JSON Schema objects remain open unless they explicitly set `additionalProperties: false`. Defaults are not applied; open objects without `properties` and arrays without `items` receive only a container type check. Raw-registered tools own their validation.
diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts
index 36d2fdd645..5ce9378762 100644
--- a/packages/core/tools/src/index.ts
+++ b/packages/core/tools/src/index.ts
@@ -773,10 +773,14 @@ export class ToolRegistry extends Service {
/** Project one definition onto the model-facing schema fields. */
private schemaOf(definition: ToolDefinition, detachParameters: boolean): ToolSchema {
const { name, description, parameters } = definition
+ const detached = detachParameters ? snapshotJsonValue(parameters) : parameters
+ if (detached === undefined) {
+ throw new Error(`tool "${name}" parameters must be lossless JSON before schema projection`)
+ }
return {
name,
description,
- parameters: detachParameters ? structuredClone(parameters) : parameters,
+ parameters: detached,
}
}
diff --git a/packages/core/tools/src/json-schema.ts b/packages/core/tools/src/json-schema.ts
index d849572789..d84b788aef 100644
--- a/packages/core/tools/src/json-schema.ts
+++ b/packages/core/tools/src/json-schema.ts
@@ -116,18 +116,70 @@ function scalarMatches(type: JsonSchemaScalarType, value: unknown): value is Jso
}
}
-/** Collect every violation for one raw schema node. */
-function checkSchemaNode(node: unknown, path: string, violations: string[], seen: Set): void {
- if (!isPlainJsonRecord(node)) {
- violations.push(`${path} must be a schema object`)
- return
+/** Deferred work for the stack-safe raw-schema walk. */
+type SchemaWalkTask =
+ | { kind: 'enter'; node: unknown; path: string }
+ | { kind: 'leave'; node: object }
+ | { kind: 'one-of-tail'; node: Record; path: string }
+ | { kind: 'object-tail'; node: Record; path: string; properties: unknown }
+
+/** Keywords that are invalid beside `oneOf`. */
+const ONE_OF_SIBLING_KEYWORDS = ['properties', 'required', 'additionalProperties', 'items', 'enum', 'const'] as const
+
+/** Validate object-only fields after its property schemas have been visited. */
+function checkObjectSchemaTail(
+ node: Record,
+ path: string,
+ properties: unknown,
+ violations: string[],
+): void {
+ const required = node.required
+ if (Object.hasOwn(node, 'required')) {
+ if (!Array.isArray(required) || required.some(entry => typeof entry !== 'string')) {
+ violations.push(`${path}.required must be an array of strings`)
+ } else {
+ const declared = isPlainJsonRecord(properties) ? properties : {}
+ for (const key of required as string[]) {
+ if (!Object.hasOwn(declared, key)) violations.push(`${path}.required names "${key}" which is not in properties`)
+ }
+ }
}
- if (seen.has(node)) {
- violations.push(`${path} is circular`)
- return
+ if (Object.hasOwn(node, 'additionalProperties') && typeof node.additionalProperties !== 'boolean') {
+ violations.push(`${path}.additionalProperties must be a boolean`)
}
- seen.add(node)
- try {
+}
+
+/** Collect every violation for one raw schema tree without using the JavaScript call stack. */
+function checkSchemaNode(root: unknown, rootPath: string, violations: string[], seen: Set): void {
+ const tasks: SchemaWalkTask[] = [{ kind: 'enter', node: root, path: rootPath }]
+ for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
+ if (task.kind === 'leave') {
+ seen.delete(task.node)
+ continue
+ }
+ if (task.kind === 'one-of-tail') {
+ for (const key of ONE_OF_SIBLING_KEYWORDS) {
+ if (Object.hasOwn(task.node, key)) violations.push(`${task.path}.${key} is not supported beside oneOf`)
+ }
+ continue
+ }
+ if (task.kind === 'object-tail') {
+ checkObjectSchemaTail(task.node, task.path, task.properties, violations)
+ continue
+ }
+
+ const { node, path } = task
+ if (!isPlainJsonRecord(node)) {
+ violations.push(`${path} must be a schema object`)
+ continue
+ }
+ if (seen.has(node)) {
+ violations.push(`${path} is circular`)
+ continue
+ }
+ seen.add(node)
+ tasks.push({ kind: 'leave', node })
+
for (const key of Object.keys(node)) {
if (CONSTRAINT_KEYWORDS.has(key)) continue
if (ANNOTATION_KEYWORDS.has(key)) {
@@ -151,28 +203,26 @@ function checkSchemaNode(node: unknown, path: string, violations: string[], seen
const hasOneOf = Object.hasOwn(node, 'oneOf')
if (hasType && hasOneOf) {
violations.push(`${path} cannot declare both type and oneOf`)
- return
+ continue
}
if (!hasType && !hasOneOf) {
- for (const key of ['properties', 'required', 'additionalProperties', 'items', 'enum', 'const']) {
+ for (const key of ONE_OF_SIBLING_KEYWORDS) {
if (Object.hasOwn(node, key)) violations.push(`${path}.${key} requires type or oneOf`)
}
- return
+ continue
}
if (hasOneOf) {
const oneOf = node.oneOf
+ tasks.push({ kind: 'one-of-tail', node, path })
if (!Array.isArray(oneOf) || oneOf.length < 2) {
violations.push(`${path}.oneOf must be an array of at least two schemas`)
} else {
- for (let index = 0; index < oneOf.length; index++) {
- checkSchemaNode(oneOf[index], `${path}.oneOf[${index}]`, violations, seen)
+ for (let index = oneOf.length - 1; index >= 0; index--) {
+ tasks.push({ kind: 'enter', node: oneOf[index], path: `${path}.oneOf[${index}]` })
}
}
- for (const key of ['properties', 'required', 'additionalProperties', 'items', 'enum', 'const']) {
- if (Object.hasOwn(node, key)) violations.push(`${path}.${key} is not supported beside oneOf`)
- }
- return
+ continue
}
const type = node.type
@@ -180,7 +230,7 @@ function checkSchemaNode(node: unknown, path: string, violations: string[], seen
violations.push(Array.isArray(type)
? `${path}.type must be a single type string (type arrays are not supported)`
: `${path}.type must be one of ${SCHEMA_TYPES.join('/')}`)
- return
+ continue
}
const schemaType = type as JsonSchemaType
const allowedFor: Record = {
@@ -200,33 +250,24 @@ function checkSchemaNode(node: unknown, path: string, violations: string[], seen
switch (schemaType) {
case 'object': {
const properties = node.properties
+ tasks.push({ kind: 'object-tail', node, path, properties })
if (Object.hasOwn(node, 'properties')) {
if (!isPlainJsonRecord(properties)) {
violations.push(`${path}.properties must be an object of schemas`)
} else {
- for (const [key, child] of Object.entries(properties)) {
- checkSchemaNode(child, `${path}.properties.${key}`, violations, seen)
+ const entries = Object.entries(properties)
+ for (let index = entries.length - 1; index >= 0; index--) {
+ const entry = entries[index]
+ /* v8 ignore next -- the loop is bounded by the captured entry count. */
+ if (entry === undefined) continue
+ tasks.push({ kind: 'enter', node: entry[1], path: `${path}.properties.${entry[0]}` })
}
}
}
- const required = node.required
- if (Object.hasOwn(node, 'required')) {
- if (!Array.isArray(required) || required.some(entry => typeof entry !== 'string')) {
- violations.push(`${path}.required must be an array of strings`)
- } else {
- const declared = isPlainJsonRecord(properties) ? properties : {}
- for (const key of required as string[]) {
- if (!Object.hasOwn(declared, key)) violations.push(`${path}.required names "${key}" which is not in properties`)
- }
- }
- }
- if (Object.hasOwn(node, 'additionalProperties') && typeof node.additionalProperties !== 'boolean') {
- violations.push(`${path}.additionalProperties must be a boolean`)
- }
break
}
case 'array': {
- if (Object.hasOwn(node, 'items')) checkSchemaNode(node.items, `${path}.items`, violations, seen)
+ if (Object.hasOwn(node, 'items')) tasks.push({ kind: 'enter', node: node.items, path: `${path}.items` })
break
}
case 'string':
@@ -238,10 +279,8 @@ function checkSchemaNode(node: unknown, path: string, violations: string[], seen
const enumValid = Array.isArray(allowed)
&& allowed.length > 0
&& allowed.every(entry => scalarMatches(schemaType, entry))
- if (Object.hasOwn(node, 'enum')) {
- if (!enumValid) {
- violations.push(`${path}.enum must be a non-empty array of ${schemaType} values`)
- }
+ if (Object.hasOwn(node, 'enum') && !enumValid) {
+ violations.push(`${path}.enum must be a non-empty array of ${schemaType} values`)
}
const constValid = scalarMatches(schemaType, node.const)
if (Object.hasOwn(node, 'const')) {
@@ -256,8 +295,6 @@ function checkSchemaNode(node: unknown, path: string, violations: string[], seen
/* v8 ignore next -- schemaType was narrowed from the closed SCHEMA_TYPES table above. */
default: assertNever(schemaType, 'JsonSchemaType')
}
- } finally {
- seen.delete(node)
}
}
@@ -308,81 +345,57 @@ function propertyPath(path: string, key: string): string {
return path === '' ? key : `${path}.${key}`
}
-/** Contain hostile getters/proxies so validation remains total for arbitrary values. */
-function checkValue(node: JsonSchemaNode, value: unknown, path: string): string[] {
- if (node.type !== undefined && !(SCHEMA_TYPES as readonly unknown[]).includes(node.type)) {
- return checkValueUnchecked(node, value, path)
- }
- try {
- return checkValueUnchecked(node, value, path)
- } catch {
- return [`"${diagnosticPath(path)}" must be a lossless JSON value`]
+/** One child evaluation deferred by a container or exact-one union frame. */
+interface ValueChild {
+ readonly node: JsonSchemaNode
+ readonly value: unknown
+ readonly path: string
+}
+
+/** Explicit call frame for stack-safe schema-value validation. */
+interface ValueFrame {
+ readonly node: JsonSchemaNode
+ readonly value: unknown
+ readonly path: string
+ catches: boolean
+ phase: 'start' | 'children'
+ kind?: 'oneOf' | 'object' | 'array'
+ children: ValueChild[]
+ childIndex: number
+ violations: string[]
+ tailViolations: string[]
+ matches: number
+}
+
+/** The generic exception-containment diagnostic owned by one valid schema node. */
+function losslessValueViolation(path: string): string[] {
+ return [`"${diagnosticPath(path)}" must be a lossless JSON value`]
+}
+
+/** Append diagnostics without spreading a potentially wide child result as call arguments. */
+function appendViolations(target: string[], source: readonly string[]): void {
+ for (const violation of source) target.push(violation)
+}
+
+/** Initialize one validation frame with empty aggregation state. */
+function valueFrame(node: JsonSchemaNode, value: unknown, path: string): ValueFrame {
+ return {
+ node,
+ value,
+ path,
+ catches: false,
+ phase: 'start',
+ children: [],
+ childIndex: 0,
+ violations: [],
+ tailViolations: [],
+ matches: 0,
}
}
-/** Collect value violations for one trusted schema node after the exception boundary. */
-function checkValueUnchecked(node: JsonSchemaNode, value: unknown, path: string): string[] {
- if (node.oneOf !== undefined) {
- const matches = node.oneOf.filter(branch => checkValue(branch, value, path).length === 0).length
- return matches === 1 ? [] : [`"${diagnosticPath(path)}" must match exactly one oneOf branch (matched ${matches})`]
- }
- if (node.type === undefined) {
- return safelyIsJsonValue(value) ? [] : [`"${diagnosticPath(path)}" must be a lossless JSON value`]
- }
-
- switch (node.type) {
- case 'object': {
- if (!isPlainJsonRecord(value)) return [`"${diagnosticPath(path)}" must be an object`]
- const violations: string[] = []
- const properties = node.properties ?? {}
- for (const key of node.required ?? []) {
- if (!Object.hasOwn(value, key) || value[key] === undefined) violations.push(`missing required property "${propertyPath(path, key)}"`)
- }
- for (const [key, child] of Object.entries(properties)) {
- if (!Object.hasOwn(value, key) || value[key] === undefined) continue
- violations.push(...checkValue(child, value[key], propertyPath(path, key)))
- }
- if (node.additionalProperties === false) {
- for (const key of Object.keys(value)) {
- if (!Object.hasOwn(properties, key)) violations.push(`"${propertyPath(path, key)}" is not a declared property (additionalProperties: false)`)
- }
- }
- if (violations.length > 0) return violations
- return safelyIsJsonValue(value) ? [] : [`"${diagnosticPath(path)}" must be a lossless JSON object`]
- }
- case 'array': {
- if (!Array.isArray(value)) return [`"${diagnosticPath(path)}" must be an array`]
- const items = node.items
- const violations = items === undefined
- ? []
- : value.flatMap((entry, index) => checkValue(items, entry, `${path}[${index}]`))
- if (violations.length > 0) return violations
- return safelyIsJsonValue(value) ? [] : [`"${diagnosticPath(path)}" must be a dense lossless JSON array`]
- }
- case 'string': {
- if (typeof value !== 'string') return [`"${diagnosticPath(path)}" must be a string`]
- break
- }
- case 'number': {
- if (typeof value !== 'number') return [`"${diagnosticPath(path)}" must be a number`]
- if (!isJsonNumber(value)) return [`"${diagnosticPath(path)}" must be a finite JSON number`]
- break
- }
- case 'integer': {
- if (!isJsonNumber(value) || !Number.isInteger(value)) return [`"${diagnosticPath(path)}" must be an integer`]
- break
- }
- case 'boolean': {
- if (typeof value !== 'boolean') return [`"${diagnosticPath(path)}" must be a boolean`]
- break
- }
- case 'null': {
- if (value !== null) return [`"${diagnosticPath(path)}" must be null`]
- break
- }
- default: return assertNever(node.type, 'JsonSchemaType')
- }
- if (node.enum !== undefined && !node.enum.includes(value)) {
+/** Validate one scalar node after its primitive type check. */
+function checkScalarValue(node: JsonSchemaNode, value: unknown, path: string): string[] {
+ if (node.enum !== undefined && !node.enum.includes(value as JsonSchemaScalar)) {
return [`"${diagnosticPath(path)}" must be one of ${JSON.stringify(node.enum)}`]
}
if (Object.hasOwn(node, 'const') && value !== node.const) {
@@ -391,6 +404,165 @@ function checkValueUnchecked(node: JsonSchemaNode, value: unknown, path: string)
return []
}
+/** Validate one trusted schema/value pair with explicit frames rather than recursive calls. */
+function checkValue(schema: JsonSchemaNode, value: unknown, path: string): string[] {
+ const frames: ValueFrame[] = [valueFrame(schema, value, path)]
+ let rootResult: string[] | undefined
+
+ const receive = (result: string[]): void => {
+ const parent = frames.at(-1)
+ if (parent === undefined) {
+ rootResult = result
+ return
+ }
+ if (parent.kind === 'oneOf') {
+ if (result.length === 0) parent.matches++
+ } else {
+ appendViolations(parent.violations, result)
+ }
+ }
+ const finish = (result: string[]): void => {
+ frames.pop()
+ receive(result)
+ }
+
+ while (frames.length > 0) {
+ const frame = frames.at(-1)
+ /* v8 ignore next -- the loop condition guarantees a current frame. */
+ if (frame === undefined) break
+ try {
+ if (frame.phase === 'children') {
+ if (frame.childIndex < frame.children.length) {
+ const child = frame.children[frame.childIndex]
+ /* v8 ignore next -- childIndex is bounded by children.length. */
+ if (child === undefined) throw new Error('missing schema-value child frame')
+ frame.childIndex++
+ frames.push(valueFrame(child.node, child.value, child.path))
+ continue
+ }
+ if (frame.kind === 'oneOf') {
+ finish(frame.matches === 1 ? [] : [`"${diagnosticPath(frame.path)}" must match exactly one oneOf branch (matched ${frame.matches})`])
+ continue
+ }
+ appendViolations(frame.violations, frame.tailViolations)
+ if (frame.violations.length > 0) {
+ finish(frame.violations)
+ } else if (frame.kind === 'object') {
+ finish(safelyIsJsonValue(frame.value) ? [] : [`"${diagnosticPath(frame.path)}" must be a lossless JSON object`])
+ } else {
+ finish(safelyIsJsonValue(frame.value) ? [] : [`"${diagnosticPath(frame.path)}" must be a dense lossless JSON array`])
+ }
+ continue
+ }
+
+ const nodeType = frame.node.type
+ frame.catches = !(nodeType !== undefined && !(SCHEMA_TYPES as readonly unknown[]).includes(nodeType))
+ const oneOf = frame.node.oneOf
+ if (oneOf !== undefined) {
+ frame.kind = 'oneOf'
+ frame.children = Array.from(oneOf, branch => ({ node: branch, value: frame.value, path: frame.path }))
+ frame.childIndex = 0
+ frame.matches = 0
+ frame.phase = 'children'
+ continue
+ }
+ if (nodeType === undefined) {
+ finish(safelyIsJsonValue(frame.value) ? [] : losslessValueViolation(frame.path))
+ continue
+ }
+
+ switch (nodeType) {
+ case 'object': {
+ if (!isPlainJsonRecord(frame.value)) {
+ finish([`"${diagnosticPath(frame.path)}" must be an object`])
+ break
+ }
+ const properties = frame.node.properties ?? {}
+ const violations: string[] = []
+ for (const key of frame.node.required ?? []) {
+ if (!Object.hasOwn(frame.value, key) || frame.value[key] === undefined) {
+ violations.push(`missing required property "${propertyPath(frame.path, key)}"`)
+ }
+ }
+ const children: ValueChild[] = []
+ for (const [key, child] of Object.entries(properties)) {
+ if (!Object.hasOwn(frame.value, key) || frame.value[key] === undefined) continue
+ children.push({ node: child, value: frame.value[key], path: propertyPath(frame.path, key) })
+ }
+ const tailViolations: string[] = []
+ if (frame.node.additionalProperties === false) {
+ for (const key of Object.keys(frame.value)) {
+ if (!Object.hasOwn(properties, key)) {
+ tailViolations.push(`"${propertyPath(frame.path, key)}" is not a declared property (additionalProperties: false)`)
+ }
+ }
+ }
+ frame.kind = 'object'
+ frame.children = children
+ frame.childIndex = 0
+ frame.violations = violations
+ frame.tailViolations = tailViolations
+ frame.phase = 'children'
+ break
+ }
+ case 'array': {
+ if (!Array.isArray(frame.value)) {
+ finish([`"${diagnosticPath(frame.path)}" must be an array`])
+ break
+ }
+ const items = frame.node.items
+ const children = items === undefined
+ ? []
+ : frame.value.flatMap((entry, index): ValueChild[] => [{ node: items, value: entry, path: `${frame.path}[${index}]` }])
+ frame.kind = 'array'
+ frame.children = children
+ frame.childIndex = 0
+ frame.violations = []
+ frame.phase = 'children'
+ break
+ }
+ case 'string':
+ finish(typeof frame.value === 'string'
+ ? checkScalarValue(frame.node, frame.value, frame.path)
+ : [`"${diagnosticPath(frame.path)}" must be a string`])
+ break
+ case 'number':
+ finish(typeof frame.value !== 'number'
+ ? [`"${diagnosticPath(frame.path)}" must be a number`]
+ : !isJsonNumber(frame.value)
+ ? [`"${diagnosticPath(frame.path)}" must be a finite JSON number`]
+ : checkScalarValue(frame.node, frame.value, frame.path))
+ break
+ case 'integer':
+ finish(!isJsonNumber(frame.value) || !Number.isInteger(frame.value)
+ ? [`"${diagnosticPath(frame.path)}" must be an integer`]
+ : checkScalarValue(frame.node, frame.value, frame.path))
+ break
+ case 'boolean':
+ finish(typeof frame.value === 'boolean'
+ ? checkScalarValue(frame.node, frame.value, frame.path)
+ : [`"${diagnosticPath(frame.path)}" must be a boolean`])
+ break
+ case 'null':
+ finish(frame.value === null
+ ? checkScalarValue(frame.node, frame.value, frame.path)
+ : [`"${diagnosticPath(frame.path)}" must be null`])
+ break
+ default:
+ finish(assertNever(nodeType, 'JsonSchemaType'))
+ }
+ } catch (error) {
+ let failed = frames.pop()
+ while (failed !== undefined && !failed.catches) failed = frames.pop()
+ if (failed === undefined) throw error
+ receive(losslessValueViolation(failed.path))
+ }
+ }
+
+ /* v8 ignore next -- every root frame finishes or throws. */
+ return rootResult ?? losslessValueViolation(path)
+}
+
/**
* Validate a candidate value against an asserted raw schema. The function is
* total for arbitrary values and returns path-qualified violations.
diff --git a/packages/core/tools/src/schema.ts b/packages/core/tools/src/schema.ts
index 9f52eb6e33..36fd444b64 100644
--- a/packages/core/tools/src/schema.ts
+++ b/packages/core/tools/src/schema.ts
@@ -180,66 +180,172 @@ function assertAuthorKeys(source: Record, path: string, allowed
}
}
-/** Compile one implicit property map, collecting per-property requiredness. */
-function compilePropertyMap(
- input: unknown,
- path: string,
- seen: Set,
-): { properties: Record; required?: string[] } {
- if (!isPlainJsonRecord(input)) authorError(`${path} must be an object of value schemas`)
- if (seen.has(input)) authorError(`${path} is circular`)
- seen.add(input)
- try {
- const properties: Record = {}
- const required: string[] = []
- for (const [key, property] of Object.entries(input)) {
- if (!isPlainJsonRecord(property)) authorError(`${path}.${key} must be a value schema object`)
- if (Object.hasOwn(property, 'required') && property.required !== true) {
- authorError(`${path}.${key}.required must be true when present`)
- }
- Object.defineProperty(properties, key, {
- value: compileValueSchema(property, `${path}.${key}`, seen, true),
+/** Compiled form of one implicit property map. */
+interface CompiledPropertyMap {
+ properties: Record
+ required?: string[]
+}
+
+/** Mutable holder used only while an iterative compilation root is unresolved. */
+interface CompileRoot {
+ value?: T
+}
+
+/** Where one compiled value node is installed. */
+type NodeDestination =
+ | { kind: 'root'; holder: CompileRoot }
+ | { kind: 'property'; target: Record; key: string }
+ | { kind: 'item'; target: JsonSchemaNode }
+ | { kind: 'one-of'; target: JsonSchemaNode[]; index: number }
+
+/** Where one compiled property map is installed. */
+type PropertyMapDestination =
+ | { kind: 'root'; holder: CompileRoot }
+ | { kind: 'object'; target: JsonSchemaNode }
+
+/** Deferred work for stack-safe author-schema compilation. */
+type CompileTask =
+ | { kind: 'value'; input: unknown; path: string; allowRequired: boolean; destination: NodeDestination }
+ | { kind: 'property-map'; input: unknown; path: string; destination: PropertyMapDestination }
+ | {
+ kind: 'property'
+ property: unknown
+ path: string
+ key: string
+ properties: Record
+ required: string[]
+ }
+ | {
+ kind: 'property-map-tail'
+ compiled: CompiledPropertyMap
+ required: string[]
+ destination: PropertyMapDestination
+ }
+ | { kind: 'leave'; input: object }
+
+/** Install a compiled node without giving `__proto__` assignment semantics. */
+function assignCompiledNode(destination: NodeDestination, node: JsonSchemaNode): void {
+ switch (destination.kind) {
+ case 'root':
+ destination.holder.value = node
+ break
+ case 'property':
+ Object.defineProperty(destination.target, destination.key, {
+ value: node,
enumerable: true,
configurable: true,
writable: true,
})
- if (property.required === true) required.push(key)
- }
- return required.length > 0 ? { properties, required } : { properties }
- } finally {
- seen.delete(input)
+ break
+ case 'item':
+ destination.target.items = node
+ break
+ case 'one-of':
+ destination.target[destination.index] = node
+ break
}
}
-/** Compile one author node without applying any consumer root restriction. */
-function compileValueSchema(
- input: unknown,
- path: string,
- seen: Set,
- allowRequired = false,
-): JsonSchemaNode {
- if (!isPlainJsonRecord(input)) authorError(`${path} must be a value schema object`)
- if (seen.has(input)) authorError(`${path} is circular`)
- seen.add(input)
- try {
- const authorKeys = [...ANNOTATION_KEYS, ...(allowRequired ? ['required'] : [])]
+/** Install a compiled property map at its root or containing object node. */
+function assignCompiledPropertyMap(destination: PropertyMapDestination, compiled: CompiledPropertyMap): void {
+ if (destination.kind === 'root') {
+ destination.holder.value = compiled
+ } else {
+ destination.target.properties = compiled.properties
+ }
+}
+
+/** Execute an author-schema compilation task graph without recursive descent. */
+function runSchemaCompiler(initial: CompileTask): void {
+ const seen = new Set()
+ const tasks: CompileTask[] = [initial]
+ for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
+ if (task.kind === 'leave') {
+ seen.delete(task.input)
+ continue
+ }
+ if (task.kind === 'property-map-tail') {
+ if (task.required.length > 0) {
+ task.compiled.required = task.required
+ if (task.destination.kind === 'object') task.destination.target.required = task.required
+ }
+ continue
+ }
+ if (task.kind === 'property') {
+ if (!isPlainJsonRecord(task.property)) authorError(`${task.path} must be a value schema object`)
+ if (Object.hasOwn(task.property, 'required') && task.property.required !== true) {
+ authorError(`${task.path}.required must be true when present`)
+ }
+ if (task.property.required === true) task.required.push(task.key)
+ tasks.push({
+ kind: 'value',
+ input: task.property,
+ path: task.path,
+ allowRequired: true,
+ destination: { kind: 'property', target: task.properties, key: task.key },
+ })
+ continue
+ }
+ if (task.kind === 'property-map') {
+ if (!isPlainJsonRecord(task.input)) authorError(`${task.path} must be an object of value schemas`)
+ if (seen.has(task.input)) authorError(`${task.path} is circular`)
+ seen.add(task.input)
+ const compiled: CompiledPropertyMap = { properties: {} }
+ const required: string[] = []
+ assignCompiledPropertyMap(task.destination, compiled)
+ tasks.push({ kind: 'leave', input: task.input })
+ tasks.push({ kind: 'property-map-tail', compiled, required, destination: task.destination })
+ const entries = Object.entries(task.input)
+ for (let index = entries.length - 1; index >= 0; index--) {
+ const entry = entries[index]
+ /* v8 ignore next -- the loop is bounded by the captured entry count. */
+ if (entry === undefined) continue
+ tasks.push({
+ kind: 'property',
+ property: entry[1],
+ path: `${task.path}.${entry[0]}`,
+ key: entry[0],
+ properties: compiled.properties,
+ required,
+ })
+ }
+ continue
+ }
+
+ const { input, path } = task
+ if (!isPlainJsonRecord(input)) authorError(`${path} must be a value schema object`)
+ if (seen.has(input)) authorError(`${path} is circular`)
+ seen.add(input)
+ const authorKeys = [...ANNOTATION_KEYS, ...(task.allowRequired ? ['required'] : [])]
const node: JsonSchemaNode = {}
+ assignCompiledNode(task.destination, node)
+ tasks.push({ kind: 'leave', input })
if (Object.hasOwn(input, 'oneOf')) {
assertAuthorKeys(input, path, [...authorKeys, 'oneOf', 'type'])
if (Object.hasOwn(input, 'type')) authorError(`${path} cannot declare both type and oneOf`)
if (!Array.isArray(input.oneOf)) authorError(`${path}.oneOf must be an array of at least two value schemas`)
- node.oneOf = input.oneOf.map((branch, index) => compileValueSchema(branch, `${path}.oneOf[${index}]`, seen))
+ const branches: JsonSchemaNode[] = []
+ node.oneOf = branches
copyAnnotations(input, node)
- return node
+ for (let index = input.oneOf.length - 1; index >= 0; index--) {
+ tasks.push({
+ kind: 'value',
+ input: input.oneOf[index],
+ path: `${path}.oneOf[${index}]`,
+ allowRequired: false,
+ destination: { kind: 'one-of', target: branches, index },
+ })
+ }
+ continue
}
switch (input.type) {
case 'json':
assertAuthorKeys(input, path, [...authorKeys, 'type'])
copyAnnotations(input, node)
- return node
- case 'object': {
+ break
+ case 'object':
assertAuthorKeys(input, path, [...authorKeys, 'type', 'properties', 'additionalProperties'])
if (!Object.hasOwn(input, 'additionalProperties') || typeof input.additionalProperties !== 'boolean') {
authorError(`${path}.additionalProperties must be explicitly true or false`)
@@ -248,18 +354,28 @@ function compileValueSchema(
copyAnnotations(input, node)
node.additionalProperties = input.additionalProperties
if (Object.hasOwn(input, 'properties')) {
- const compiled = compilePropertyMap(input.properties, `${path}.properties`, seen)
- node.properties = compiled.properties
- if (compiled.required !== undefined) node.required = compiled.required
+ tasks.push({
+ kind: 'property-map',
+ input: input.properties,
+ path: `${path}.properties`,
+ destination: { kind: 'object', target: node },
+ })
}
- return node
- }
+ break
case 'array':
assertAuthorKeys(input, path, [...authorKeys, 'type', 'items'])
node.type = 'array'
copyAnnotations(input, node)
- if (Object.hasOwn(input, 'items')) node.items = compileValueSchema(input.items, `${path}.items`, seen)
- return node
+ if (Object.hasOwn(input, 'items')) {
+ tasks.push({
+ kind: 'value',
+ input: input.items,
+ path: `${path}.items`,
+ allowRequired: false,
+ destination: { kind: 'item', target: node },
+ })
+ }
+ break
case 'string':
case 'number':
case 'integer':
@@ -274,15 +390,29 @@ function compileValueSchema(
: input.enum as JsonSchemaScalar[]
}
if (Object.hasOwn(input, 'const')) node.const = input.const as JsonSchemaScalar
- return node
+ break
default:
- return authorError(`${path}.type must be string/number/integer/boolean/null/array/object/json, or use oneOf`)
+ authorError(`${path}.type must be string/number/integer/boolean/null/array/object/json, or use oneOf`)
}
- } finally {
- seen.delete(input)
}
}
+/** Compile one implicit property map, collecting per-property requiredness. */
+function compilePropertyMap(input: unknown, path: string): CompiledPropertyMap {
+ const holder: CompileRoot = {}
+ runSchemaCompiler({ kind: 'property-map', input, path, destination: { kind: 'root', holder } })
+ /* v8 ignore next -- the root task assigns before scheduling any descendants. */
+ return holder.value ?? authorError(`${path} did not compile`)
+}
+
+/** Compile one author node without applying any consumer root restriction. */
+function compileValueSchema(input: unknown, path: string): JsonSchemaNode {
+ const holder: CompileRoot = {}
+ runSchemaCompiler({ kind: 'value', input, path, allowRequired: false, destination: { kind: 'root', holder } })
+ /* v8 ignore next -- the root task assigns before scheduling any descendants. */
+ return holder.value ?? authorError(`${path} did not compile`)
+}
+
/**
* Compile one author-facing value schema to the enforced raw JSON Schema
* subset. The author-only `json` node becomes an annotation-only schema.
@@ -290,7 +420,7 @@ function compileValueSchema(
* @returns The asserted raw schema projection.
*/
export function valueSchemaSpecToJsonSchema(spec: ValueSchemaSpec): JsonSchemaNode {
- const schema = compileValueSchema(spec, 'schema', new Set())
+ const schema = compileValueSchema(spec, 'schema')
assertSupportedJsonSchema(schema)
return schema
}
@@ -301,7 +431,7 @@ export function valueSchemaSpecToJsonSchema(spec: ValueSchemaSpec): JsonSchemaNo
* @returns An object-rooted raw schema with no implicit-root openness override.
*/
export function parameterSchemaSpecToJsonSchema(spec: ParameterSchemaSpec): ParameterJsonSchema {
- const compiled = compilePropertyMap(spec, 'parameters', new Set())
+ const compiled = compilePropertyMap(spec, 'parameters')
const schema: ParameterJsonSchema = {
type: 'object',
properties: compiled.properties,
diff --git a/packages/core/tools/src/ts-types.ts b/packages/core/tools/src/ts-types.ts
index 39cec5665e..6fa1a99c49 100644
--- a/packages/core/tools/src/ts-types.ts
+++ b/packages/core/tools/src/ts-types.ts
@@ -8,7 +8,7 @@
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
import { assertSupportedJsonSchema } from './json-schema.ts'
-import type { JsonSchemaScalar } from './json-schema.ts'
+import type { JsonSchemaNode, JsonSchemaScalar } from './json-schema.ts'
/** Property names that are valid bare TS identifiers; anything else is quoted. */
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
@@ -47,9 +47,181 @@ function renderConstrainedScalar(node: Record, type: string): s
return broad
}
-/** Parenthesize a union or object intersection before applying `[]`. */
-function arrayItem(type: string): string {
- return type.includes('|') || type.includes('&') ? `(${type})[]` : `${type}[]`
+/** A composable type document that can be flattened without recursive string concatenation. */
+interface TypeDocument {
+ readonly parts: readonly (string | TypeDocument)[]
+ readonly containsUnionOrIntersection: boolean
+}
+
+/** Build one document from captured parts while retaining the legacy array-parenthesization test. */
+function typeDocumentFrom(parts: readonly (string | TypeDocument)[]): TypeDocument {
+ return {
+ parts,
+ containsUnionOrIntersection: parts.some(part => typeof part === 'string'
+ ? part.includes('|') || part.includes('&')
+ : part.containsUnionOrIntersection),
+ }
+}
+
+/** Build a small document without an intermediate array at each call site. */
+function typeDocument(...parts: (string | TypeDocument)[]): TypeDocument {
+ return typeDocumentFrom(parts)
+}
+
+/** Flatten a nested document with an explicit work stack. */
+function flattenTypeDocument(document: TypeDocument): string {
+ const chunks: string[] = []
+ const tasks: (string | TypeDocument)[] = [document]
+ for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
+ if (typeof task === 'string') {
+ chunks.push(task)
+ continue
+ }
+ for (let index = task.parts.length - 1; index >= 0; index--) {
+ const part = task.parts[index]
+ /* v8 ignore next -- the loop is bounded by the captured part count. */
+ if (part !== undefined) tasks.push(part)
+ }
+ }
+ return chunks.join('')
+}
+
+/** One explicit call frame for stack-safe schema-to-TypeScript rendering. */
+interface SchemaRenderFrame {
+ readonly node: JsonSchemaNode
+ readonly indent: number
+ phase: 'start' | 'children'
+ kind?: 'oneOf' | 'array' | 'object'
+ children: { node: JsonSchemaNode; indent: number }[]
+ childIndex: number
+ childDocuments: TypeDocument[]
+ entries: [string, JsonSchemaNode][]
+}
+
+/** Initialize one schema-render frame with empty aggregation state. */
+function schemaRenderFrame(node: JsonSchemaNode, indent: number): SchemaRenderFrame {
+ return { node, indent, phase: 'start', children: [], childIndex: 0, childDocuments: [], entries: [] }
+}
+
+/** Render an already asserted schema to a composable document. */
+function renderSupportedSchema(schema: JsonSchemaNode, indent: number): TypeDocument {
+ const frames: SchemaRenderFrame[] = [schemaRenderFrame(schema, indent)]
+ let rootDocument: TypeDocument | undefined
+ const finish = (document: TypeDocument): void => {
+ frames.pop()
+ const parent = frames.at(-1)
+ if (parent === undefined) rootDocument = document
+ else parent.childDocuments.push(document)
+ }
+
+ while (frames.length > 0) {
+ const frame = frames.at(-1)
+ /* v8 ignore next -- the loop condition guarantees a current frame. */
+ if (frame === undefined) break
+ if (frame.phase === 'children') {
+ if (frame.childIndex < frame.children.length) {
+ const child = frame.children[frame.childIndex]
+ /* v8 ignore next -- childIndex is bounded by children.length. */
+ if (child === undefined) throw new Error('missing schema render child')
+ frame.childIndex++
+ frames.push(schemaRenderFrame(child.node, child.indent))
+ continue
+ }
+ if (frame.kind === 'oneOf') {
+ const parts: (string | TypeDocument)[] = []
+ for (let index = 0; index < frame.childDocuments.length; index++) {
+ if (index > 0) parts.push(' | ')
+ const child = frame.childDocuments[index]
+ /* v8 ignore next -- child documents correspond one-to-one with children. */
+ if (child !== undefined) parts.push(child)
+ }
+ finish(typeDocumentFrom(parts))
+ continue
+ }
+ if (frame.kind === 'array') {
+ const child = frame.childDocuments[0]
+ /* v8 ignore next -- array frames always schedule exactly one child. */
+ if (child === undefined) throw new Error('missing array item type')
+ finish(child.containsUnionOrIntersection
+ ? typeDocument('(', child, ')[]')
+ : typeDocument(child, '[]'))
+ continue
+ }
+
+ const required = new Set(frame.node.required)
+ const parts: (string | TypeDocument)[] = ['{']
+ for (let index = 0; index < frame.entries.length; index++) {
+ const entry = frame.entries[index]
+ const child = frame.childDocuments[index]
+ /* v8 ignore next -- object entries and child documents have the same length. */
+ if (entry === undefined || child === undefined) throw new Error('missing object property type')
+ const [name, prop] = entry
+ for (const line of docLines(prop.description, frame.indent + 1)) parts.push('\n', line)
+ parts.push('\n', `${pad(frame.indent + 1)}${renderKey(name)}${required.has(name) ? '' : '?'}: `, child, ';')
+ }
+ parts.push('\n', `${pad(frame.indent)}}`)
+ const declared = typeDocumentFrom(parts)
+ finish(frame.node.additionalProperties === false
+ ? declared
+ : typeDocument(declared, ' & Record'))
+ continue
+ }
+
+ const node = frame.node
+ if (node.oneOf !== undefined) {
+ frame.kind = 'oneOf'
+ frame.children = Array.from(node.oneOf, child => ({ node: child, indent: frame.indent }))
+ frame.childIndex = 0
+ frame.childDocuments = []
+ frame.phase = 'children'
+ continue
+ }
+ if (node.type === undefined) {
+ finish(typeDocument('JsonValue'))
+ continue
+ }
+ switch (node.type) {
+ case 'string':
+ case 'number':
+ case 'integer':
+ case 'boolean':
+ case 'null':
+ finish(typeDocument(renderConstrainedScalar(node as Record, node.type)))
+ break
+ case 'array':
+ if (node.items === undefined) {
+ finish(typeDocument('JsonValue[]'))
+ } else {
+ frame.kind = 'array'
+ frame.children = [{ node: node.items, indent: frame.indent }]
+ frame.childIndex = 0
+ frame.childDocuments = []
+ frame.phase = 'children'
+ }
+ break
+ case 'object': {
+ const open = node.additionalProperties !== false
+ const entries = Object.entries(node.properties ?? {})
+ if (entries.length === 0) {
+ finish(typeDocument(open ? 'Record' : 'Record'))
+ } else {
+ frame.kind = 'object'
+ frame.entries = entries
+ frame.children = entries.map(([, child]) => ({ node: child, indent: frame.indent + 1 }))
+ frame.childIndex = 0
+ frame.childDocuments = []
+ frame.phase = 'children'
+ }
+ break
+ }
+ /* v8 ignore next -- assertSupportedJsonSchema narrowed this closed type union. */
+ default:
+ finish(typeDocument('unknown'))
+ }
+ }
+
+ /* v8 ignore next -- every root frame produces one document. */
+ return rootDocument ?? typeDocument('unknown')
}
/**
@@ -63,43 +235,10 @@ function arrayItem(type: string): string {
export function jsonSchemaToTs(schema: unknown, indent = 0): string {
try {
assertSupportedJsonSchema(schema)
+ return flattenTypeDocument(renderSupportedSchema(schema, indent))
} catch {
return 'unknown'
}
- const node = schema as Record
- if (Object.hasOwn(node, 'oneOf')) {
- return (node.oneOf as unknown[]).map(branch => jsonSchemaToTs(branch, indent)).join(' | ')
- }
- if (!Object.hasOwn(node, 'type')) return 'JsonValue'
- switch (node.type) {
- case 'string': return renderConstrainedScalar(node, 'string')
- case 'number': return renderConstrainedScalar(node, 'number')
- case 'integer': return renderConstrainedScalar(node, 'integer')
- case 'boolean': return renderConstrainedScalar(node, 'boolean')
- case 'null': return renderConstrainedScalar(node, 'null')
- case 'array': {
- return arrayItem(Object.hasOwn(node, 'items') ? jsonSchemaToTs(node.items, indent) : 'JsonValue')
- }
- case 'object': {
- const properties = node.properties
- const open = node.additionalProperties !== false
- if (properties === undefined) return open ? 'Record' : 'Record'
- const entries = Object.entries(properties as Record)
- if (entries.length === 0) return open ? 'Record' : 'Record'
- const required = new Set(node.required as string[] | undefined)
- const lines: string[] = ['{']
- for (const [name, prop] of entries) {
- const description = (prop as Record).description
- lines.push(...docLines(description, indent + 1))
- lines.push(`${pad(indent + 1)}${renderKey(name)}${required.has(name) ? '' : '?'}: ${jsonSchemaToTs(prop, indent + 1)};`)
- }
- lines.push(`${pad(indent)}}`)
- const declared = lines.join('\n')
- return open ? `${declared} & Record` : declared
- }
- /* v8 ignore next -- assertSupportedJsonSchema narrowed this closed type union. */
- default: return 'unknown'
- }
}
/** The fixed model-facing usage contract rendered above the declarations (see the Code Mode Agent Note's "What the model sees"). */
diff --git a/packages/core/tools/tests/json-schema.spec.ts b/packages/core/tools/tests/json-schema.spec.ts
index 8be1bbd35d..67b5501fe0 100644
--- a/packages/core/tools/tests/json-schema.spec.ts
+++ b/packages/core/tools/tests/json-schema.spec.ts
@@ -214,6 +214,14 @@ describe('the enforced raw JSON Schema subset', () => {
.toEqual(['schema.properties.at must be a schema object'])
})
+ it('asserts deeply nested raw unions without using the JavaScript call stack', () => {
+ const depth = 5_000
+ let schema: JsonSchemaNode = { type: 'string' }
+ for (let index = 0; index < depth; index++) schema = { oneOf: [schema, { type: 'null' }] }
+
+ expect(() => { assertSupportedJsonSchema(schema) }).not.toThrow()
+ })
+
it('uses own-property semantics for required declarations', () => {
expect(violationsOf({ type: 'object', properties: {}, required: ['toString'] }))
.toEqual(['schema.required names "toString" which is not in properties'])
@@ -322,6 +330,17 @@ describe('validateJsonSchemaValue', () => {
expect(validateJsonSchemaValue(overlap, 1.5)).toEqual([])
})
+ it('validates deeply nested exact-one unions without using the JavaScript call stack', () => {
+ const depth = 5_000
+ let schema: JsonSchemaNode = { type: 'string' }
+ for (let index = 0; index < depth; index++) schema = { oneOf: [schema, { type: 'null' }] }
+ assertSupportedJsonSchema(schema)
+
+ expect(validateJsonSchemaValue(schema, 'leaf')).toEqual([])
+ expect(validateJsonSchemaValue(schema, 42))
+ .toEqual(['"value" must match exactly one oneOf branch (matched 0)'])
+ })
+
it('an unconstrained schema accepts only lossless JSON values', () => {
const anyJson = asserted({})
for (const value of [null, true, 1, 'x', [1], { x: null }]) {
diff --git a/packages/core/tools/tests/schema.spec.ts b/packages/core/tools/tests/schema.spec.ts
index 8de826aade..b16c0689b0 100644
--- a/packages/core/tools/tests/schema.spec.ts
+++ b/packages/core/tools/tests/schema.spec.ts
@@ -92,6 +92,23 @@ describe('the unified author schema DSL', () => {
expect(() => parameterSchemaSpecToJsonSchema(properties as ParameterSchemaSpec)).toThrow(/circular/)
})
+ it('compiles deeply nested author unions without using the JavaScript call stack', () => {
+ const depth = 5_000
+ let spec: unknown = { type: 'string' }
+ for (let index = 0; index < depth; index++) spec = { oneOf: [spec, { type: 'null' }] }
+
+ const compiled = valueSchemaSpecToJsonSchema(spec as ValueSchemaSpec)
+
+ let cursor = compiled
+ let layers = 0
+ while (cursor.oneOf !== undefined) {
+ cursor = cursor.oneOf[0]!
+ layers++
+ }
+ expect(layers).toBe(depth)
+ expect(cursor).toEqual({ type: 'string' })
+ })
+
it('preserves a property literally named __proto__ as schema data', () => {
const properties = Object.create(null) as ParameterSchemaSpec
properties.__proto__ = { type: 'string', required: true }
diff --git a/packages/core/tools/tests/tools.spec.ts b/packages/core/tools/tests/tools.spec.ts
index c1c920fed2..0150a10a2b 100644
--- a/packages/core/tools/tests/tools.spec.ts
+++ b/packages/core/tools/tests/tools.spec.ts
@@ -8,7 +8,7 @@ import ToolRegistry, {
defineTool, JsonSchemaError, parameterSchemaSpecToJsonSchema, validateArgs, ToolArgsError, ToolNotFoundError,
TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH,
type InferArgs, type JsonValue, type ParameterSchemaSpec, type PreToolDecision, type PostToolDecision,
- type ToolDispatchExecution, type ToolExecutionResult,
+ type JsonSchemaNode, type ToolDispatchExecution, type ToolExecutionResult,
} from '@deepseek-ai/dsh-tools'
const testToolSignal = new AbortController().signal
@@ -1289,6 +1289,41 @@ describe('ToolRegistry', () => {
}])
})
+ it('schemas() snapshots deeply nested parameters without using structured-clone recursion', async () => {
+ const ctx = await setup()
+ const depth = 5_000
+ let nested: JsonSchemaNode = { type: 'string' }
+ for (let index = 0; index < depth; index++) nested = { oneOf: [nested, { type: 'null' }] }
+ ctx.tools.register({
+ ...echoTool,
+ name: 'deep-schema',
+ parameters: { type: 'object', properties: { nested } },
+ })
+
+ const projected = ctx.tools.schemas()[0]!.parameters as JsonSchemaNode
+
+ let cursor = projected.properties!.nested!
+ let layers = 0
+ while (cursor.oneOf !== undefined) {
+ cursor = cursor.oneOf[0]!
+ layers++
+ }
+ expect(layers).toBe(depth)
+ expect(cursor).toEqual({ type: 'string' })
+ })
+
+ it('rejects schema projection when a raw registration is not lossless JSON', async () => {
+ const ctx = await setup()
+ ctx.tools.register({
+ ...echoTool,
+ name: 'lossy-schema',
+ parameters: { type: 'object', default: Number.NaN },
+ })
+
+ expect(() => ctx.tools.schemas())
+ .toThrow('tool "lossy-schema" parameters must be lossless JSON before schema projection')
+ })
+
it('rejects a non-positive or non-finite registration timeout', async () => {
const ctx = await setup()
expect(() => ctx.tools.register({ ...echoTool, name: 'zero-timeout', timeoutMs: 0 }))
diff --git a/packages/core/tools/tests/ts-types.spec.ts b/packages/core/tools/tests/ts-types.spec.ts
index 14b14f7ecd..078081f49e 100644
--- a/packages/core/tools/tests/ts-types.spec.ts
+++ b/packages/core/tools/tests/ts-types.spec.ts
@@ -93,6 +93,17 @@ describe('jsonSchemaToTs', () => {
expect(rendered).not.toContain('tool-*/ over')
expect(rendered).toContain(String.raw`tool-*\/ over`)
})
+
+ it('renders deeply nested unions without using the JavaScript call stack', () => {
+ const depth = 5_000
+ let schema: unknown = { type: 'string' }
+ for (let index = 0; index < depth; index++) schema = { oneOf: [schema, { type: 'null' }] }
+
+ const rendered = jsonSchemaToTs(schema)
+
+ expect(rendered.startsWith('string | null')).toBe(true)
+ expect(rendered.length).toBe('string'.length + depth * ' | null'.length)
+ })
})
describe('renderToolsSdk', () => {