Merge branch 'codex/tool-json-schema-dsl' into codex/canonical-tool-output

This commit is contained in:
Tianyi Cui
2026-07-21 22:15:08 +08:00
6 changed files with 46 additions and 4 deletions

View File

@@ -15,7 +15,11 @@ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key:
/** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
function hasPlainArrayPrototype(value: unknown[]): boolean {
const prototype: unknown = Object.getPrototypeOf(value)
return Array.isArray(prototype) && Object.getPrototypeOf(Object.getPrototypeOf(prototype)) === null
if (!Array.isArray(prototype)) return false
const objectPrototype: unknown = Object.getPrototypeOf(prototype)
return objectPrototype !== null
&& !Array.isArray(objectPrototype)
&& Object.getPrototypeOf(objectPrototype) === null
}
/** Whether an object is a plain or null-prototype record from any JavaScript realm. */
@@ -24,6 +28,13 @@ function hasPlainObjectPrototype(value: object): boolean {
return prototype === null || Object.getPrototypeOf(prototype) === null
}
/** Return every JSON-visible object key, or reject own data JSON would discard. */
function enumerableStringKeys(value: object): string[] | undefined {
const keys = Reflect.ownKeys(value)
if (keys.some(key => typeof key !== 'string' || !Object.prototype.propertyIsEnumerable.call(value, key))) return undefined
return keys as string[]
}
/**
* Validate and detach lossless JSON in one read per property, so a stateful
* getter cannot change between validation and copying. Accepts ordinary arrays,
@@ -75,8 +86,10 @@ export function snapshotJsonValue<T>(value: T): T | undefined {
}
if (!hasPlainObjectPrototype(current)) return undefined
const keys = enumerableStringKeys(current)
if (keys === undefined) return undefined
const snapshot: { [key: string]: JsonValue } = {}
for (const key of Object.keys(current)) {
for (const key of keys) {
const item = visit((current as Record<string, unknown>)[key])
if (item === undefined) return undefined
// Define the key as data so a JSON field literally named "__proto__"
@@ -139,7 +152,8 @@ export function isJsonValue(value: unknown, seen: Set<object> = new Set()): bool
}
// Plain object only (reject Map/Set/Date/class instances).
if (!hasPlainObjectPrototype(value)) return false
return Object.values(value).every(v => isJsonValue(v, seen))
const keys = enumerableStringKeys(value)
return keys !== undefined && keys.every(key => isJsonValue((value as Record<string, unknown>)[key], seen))
} finally {
seen.delete(value)
}

View File

@@ -92,6 +92,12 @@ describe('snapshotJsonValue', () => {
Object.defineProperty(decorated, 'extra', { value: true })
const symbolDecorated = [1]
Object.defineProperty(symbolDecorated, Symbol('extra'), { value: true })
const hiddenObject = Object.defineProperty({}, 'hidden', { value: true })
const symbolObject = { [Symbol('extra')]: true }
const forgedPrototype: unknown[] = []
Object.setPrototypeOf(forgedPrototype, null)
const forgedArray = [1]
Object.setPrototypeOf(forgedArray, forgedPrototype)
const cyclic: Record<string, unknown> = {}
cyclic.self = cyclic
const foreignExotics = runInNewContext(`(() => {
@@ -109,6 +115,9 @@ describe('snapshotJsonValue', () => {
expect(snapshotJsonValue(compensatedSparse)).toBeUndefined()
expect(snapshotJsonValue(decorated)).toBeUndefined()
expect(snapshotJsonValue(symbolDecorated)).toBeUndefined()
expect(snapshotJsonValue(hiddenObject)).toBeUndefined()
expect(snapshotJsonValue(symbolObject)).toBeUndefined()
expect(snapshotJsonValue(forgedArray)).toBeUndefined()
expect(snapshotJsonValue(cyclic)).toBeUndefined()
expect(snapshotJsonValue([undefined])).toBeUndefined()
expect(snapshotJsonValue({ value: undefined })).toBeUndefined()
@@ -177,6 +186,12 @@ describe('isJsonValue', () => {
const decorated = Object.assign([1], { extra: true })
const symbolDecorated = [1]
Object.defineProperty(symbolDecorated, Symbol('extra'), { value: true })
const hiddenObject = Object.defineProperty({}, 'hidden', { value: true })
const symbolObject = { [Symbol('extra')]: true }
const forgedPrototype: unknown[] = []
Object.setPrototypeOf(forgedPrototype, null)
const forgedArray = [1]
Object.setPrototypeOf(forgedArray, forgedPrototype)
const cyclic: Record<string, unknown> = {}
cyclic.self = cyclic
@@ -184,6 +199,9 @@ describe('isJsonValue', () => {
expect(isJsonValue(compensatedSparse)).toBe(false)
expect(isJsonValue(decorated)).toBe(false)
expect(isJsonValue(symbolDecorated)).toBe(false)
expect(isJsonValue(hiddenObject)).toBe(false)
expect(isJsonValue(symbolObject)).toBe(false)
expect(isJsonValue(forgedArray)).toBe(false)
expect(isJsonValue(new ExoticArray(1))).toBe(false)
expect(isJsonValue([undefined])).toBe(false)
expect(isJsonValue({ value: undefined })).toBe(false)