Merge branch 'codex/code-mode-typed-results' into codex/code-mode-complete-result-card
# Conflicts: # .agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.i18n.yaml
This commit is contained in:
@@ -23,7 +23,7 @@ Every field is validated and defaulted; `maxOutputBytes` is a safe integer of at
|
||||
- **The port assumes a hostile peer** — model code can reach `parentPort` and forge traffic, so every inbound message is shape-validated and REBUILT before anything reads it (`null`, primitives, junk types, and malformed payloads drop without a throw; forged extra fields never ride along), the host answers each call id at most once, resolves binding names as OWN properties only (a forged `constructor` cannot walk a prototype chain), drops post-settlement replies, and validates every binding resolution and completion as lossless JSON. Forged `log`/`done` messages cannot bypass the outer cap: the host repeats validation and accounts every admitted log plus the completion or diagnostic. Worker-side namespaces are null-prototype with `defineProperty`, so `__proto__`-shaped binding names are ordinary keys.
|
||||
- **Binding rejection classes are request data** — an optional namespace descriptor names the constructor global and the own property that receives the failed member name. The worker materializes and injects that real class, so `instanceof` works without hardcoding `tools` or `ToolCallError`; declarations with invalid or colliding globals fail before a worker spawns.
|
||||
- **Two independent budgets, because the peer is hostile** — `computeMs` meters the worker's MEASURED busy time (`worker.performance.eventLoopUtilization()` polling): a hot loop cannot hide behind a pending decoy dispatch, and a program awaiting a slow tool accrues nothing. `maxWallMs` backstops what busy time cannot see (awaiting a promise nobody resolves). Both funnel into `worker.terminate()`, which ends hot synchronous loops too; heap overflow surfaces as the worker's OOM exit (`kind: 'worker-exit'`).
|
||||
- **Intermediate binding values are complete JSON** — binding arguments and resolutions undergo iterative lossless-JSON validation, including a native-constructor identity check captured before program execution so user-authored functions cannot impersonate plain-container prototypes. Values flatten into a bounded-depth pre-order wire value for structured clone and rebuild iteratively on the other side. They have no byte, JavaScript call-stack, or nested structured-clone depth cap. They never enter the outer-output ledger or model context; provider/executor acquisition bounds and process/worker memory remain the limits.
|
||||
- **Intermediate binding values are complete JSON** — binding arguments and resolutions undergo iterative lossless-JSON validation, including a native-constructor identity check captured before program execution so user-authored functions cannot impersonate plain-container prototypes. The worker also captures every structural and metering intrinsic used by this JSON boundary and bypasses mutable collection prototypes for private traversal state, so model mutations of global helpers cannot alter validation, wire transport, or byte accounting. Values flatten into a bounded-depth pre-order wire value for structured clone and rebuild iteratively on the other side. They have no byte, JavaScript call-stack, or nested structured-clone depth cap. They never enter the outer-output ledger or model context; provider/executor acquisition bounds and process/worker memory remain the limits.
|
||||
- **Logs stream eagerly into one outer ledger** — console/stdout/stderr text crosses the port in emission order, so a timed-out or killed program still shows what it printed. The worker charges exact JSON-string bytes and preflights completion values and exception diagnostics against the remaining combined budget before posting them; a thrown million-byte stack therefore becomes the fixed `output-limit` diagnostic at the worker boundary. Native writes that bypass the patched stream slots arrive on pipes independent of the completion port, so the host repeats the ledger for those bytes and hostile forged traffic; settlement continues bounded pipe capture until worker termination completes before materializing the result. `maxOutputBytes` accounts the JSON serialization of the outer `logs` array plus the completion value or failure-message payload; fixed `CodeRunResult` field names, braces, the bounded error-kind tag, and later presentation whitespace are outside that variable-payload ledger. At or below the cap the exact value returns; a lossy completion is `invalid-output`, and a combined overflow is `output-limit` rather than a substituted inspected string. The failure retains the fitting captured prefix and later follows the normal outer `run_code` spill policy.
|
||||
- **Empty environment** — the worker gets `env: {}` and `execArgv: []`: no ambient credentials (stronger than the scrubbed-env rule for spawned commands) and no inherited loader flags.
|
||||
- **Dispose to quiescence** — teardown fails in-flight runs as `abort` and AWAITS each worker's exit before resolving.
|
||||
|
||||
@@ -2,17 +2,62 @@
|
||||
|
||||
import type { CodeJsonValue } from '@deepseek-ai/dsh-code-runtime'
|
||||
|
||||
/** Control characters with a two-byte short JSON escape instead of `\u00XX`. */
|
||||
const SHORT_ESCAPE_CODES = new Set([0x08, 0x09, 0x0a, 0x0c, 0x0d])
|
||||
type IntrinsicCallable = (this: unknown, ...args: unknown[]) => unknown
|
||||
|
||||
const intrinsicReflectApply = Reflect.apply as (
|
||||
target: IntrinsicCallable,
|
||||
thisArgument: unknown,
|
||||
argumentsList: readonly unknown[],
|
||||
) => unknown
|
||||
const intrinsicArrayIsArray = Array.isArray
|
||||
const IntrinsicBuffer = Buffer
|
||||
const intrinsicBufferByteLength = Reflect.get(Buffer, 'byteLength') as IntrinsicCallable
|
||||
const intrinsicObjectDefineProperty = Object.defineProperty
|
||||
const intrinsicObjectKeys = Object.keys
|
||||
const intrinsicString = String
|
||||
const intrinsicStringCharCodeAt = Reflect.get(String.prototype, 'charCodeAt') as IntrinsicCallable
|
||||
const intrinsicStringCodePointAt = Reflect.get(String.prototype, 'codePointAt') as IntrinsicCallable
|
||||
const intrinsicStringSlice = Reflect.get(String.prototype, 'slice') as IntrinsicCallable
|
||||
|
||||
/** UTF-8 byte length through the module-captured Node intrinsic. */
|
||||
function byteLength(text: string): number {
|
||||
return intrinsicReflectApply(intrinsicBufferByteLength, IntrinsicBuffer, [text, 'utf8']) as number
|
||||
}
|
||||
|
||||
/** Append without consulting a model-mutated `Array.prototype`. */
|
||||
function append<T>(target: T[], value: T): void {
|
||||
intrinsicObjectDefineProperty(target, target.length, {
|
||||
value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
})
|
||||
}
|
||||
|
||||
/** Pop without consulting a model-mutated `Array.prototype`. */
|
||||
function takeLast<T>(target: T[]): T | undefined {
|
||||
if (target.length === 0) return undefined
|
||||
const index = target.length - 1
|
||||
const value = target[index]
|
||||
intrinsicObjectDefineProperty(target, 'length', { value: index })
|
||||
return value
|
||||
}
|
||||
|
||||
/** One code-point-aligned character from a string. */
|
||||
function characterAt(text: string, index: number): string {
|
||||
const codePoint = intrinsicReflectApply(intrinsicStringCodePointAt, text, [index]) as number
|
||||
const width = codePoint > 0xffff ? 2 : 1
|
||||
return intrinsicReflectApply(intrinsicStringSlice, text, [index, index + width]) as string
|
||||
}
|
||||
|
||||
/** Serialized bytes contributed by one complete Unicode code point inside JSON quotes. */
|
||||
function serializedCharacterBytes(character: string): number {
|
||||
if (character.length === 2) return 4
|
||||
if (character === '"' || character === '\\') return 2
|
||||
const code = character.charCodeAt(0)
|
||||
const code = intrinsicReflectApply(intrinsicStringCharCodeAt, character, [0]) as number
|
||||
if (code >= 0xd800 && code <= 0xdfff) return 6
|
||||
if (code < 0x20) return SHORT_ESCAPE_CODES.has(code) ? 2 : 6
|
||||
return Buffer.byteLength(character, 'utf8')
|
||||
if (code < 0x20) return code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d ? 2 : 6
|
||||
return byteLength(character)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,9 +69,11 @@ function serializedCharacterBytes(character: string): number {
|
||||
export function jsonStringBytesUpTo(text: string, maxBytes: number): number | undefined {
|
||||
if (maxBytes < 2) return undefined
|
||||
let bytes = 2
|
||||
for (const character of text) {
|
||||
for (let index = 0; index < text.length;) {
|
||||
const character = characterAt(text, index)
|
||||
bytes += serializedCharacterBytes(character)
|
||||
if (bytes > maxBytes) return undefined
|
||||
index += character.length
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
@@ -49,7 +96,7 @@ export function jsonValueBytesUpTo(value: CodeJsonValue, maxBytes: number): numb
|
||||
return bytes <= maxBytes
|
||||
}
|
||||
const tasks: Task[] = [{ kind: 'value', value }]
|
||||
for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
|
||||
for (let task = takeLast(tasks); task !== undefined; task = takeLast(tasks)) {
|
||||
if (task.kind === 'value') {
|
||||
const current = task.value
|
||||
if (current === null) {
|
||||
@@ -59,16 +106,16 @@ export function jsonValueBytesUpTo(value: CodeJsonValue, maxBytes: number): numb
|
||||
if (stringBytes === undefined) return undefined
|
||||
bytes += stringBytes
|
||||
} else if (typeof current === 'number') {
|
||||
if (!add(Buffer.byteLength(String(current), 'utf8'))) return undefined
|
||||
if (!add(byteLength(intrinsicString(current)))) return undefined
|
||||
} else if (typeof current === 'boolean') {
|
||||
if (!add(current ? 4 : 5)) return undefined
|
||||
} else if (Array.isArray(current)) {
|
||||
} else if (intrinsicArrayIsArray(current)) {
|
||||
if (!add(2)) return undefined
|
||||
if (current.length > 0) tasks.push({ kind: 'array', value: current, index: 0 })
|
||||
if (current.length > 0) append(tasks, { kind: 'array', value: current, index: 0 })
|
||||
} else {
|
||||
if (!add(2)) return undefined
|
||||
const keys = Object.keys(current)
|
||||
if (keys.length > 0) tasks.push({ kind: 'object', value: current, keys, index: 0 })
|
||||
const keys = intrinsicObjectKeys(current)
|
||||
if (keys.length > 0) append(tasks, { kind: 'object', value: current, keys, index: 0 })
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -77,8 +124,8 @@ export function jsonValueBytesUpTo(value: CodeJsonValue, maxBytes: number): numb
|
||||
if (task.kind === 'array') {
|
||||
const item = task.value[task.index]
|
||||
if (item === undefined) return undefined
|
||||
if (task.index + 1 < task.value.length) tasks.push({ ...task, index: task.index + 1 })
|
||||
tasks.push({ kind: 'value', value: item })
|
||||
if (task.index + 1 < task.value.length) append(tasks, { ...task, index: task.index + 1 })
|
||||
append(tasks, { kind: 'value', value: item })
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -90,8 +137,8 @@ export function jsonValueBytesUpTo(value: CodeJsonValue, maxBytes: number): numb
|
||||
if (!add(keyBytes + 1)) return undefined
|
||||
const item = task.value[key]
|
||||
if (item === undefined) return undefined
|
||||
if (task.index + 1 < task.keys.length) tasks.push({ ...task, index: task.index + 1 })
|
||||
tasks.push({ kind: 'value', value: item })
|
||||
if (task.index + 1 < task.keys.length) append(tasks, { ...task, index: task.index + 1 })
|
||||
append(tasks, { kind: 'value', value: item })
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
@@ -108,11 +155,13 @@ export function truncateJsonStringBytes(text: string, maxBytes: number): string
|
||||
if (maxBytes < 2) return ''
|
||||
let bytes = 2
|
||||
let end = 0
|
||||
for (const character of text) {
|
||||
for (let index = 0; index < text.length;) {
|
||||
const character = characterAt(text, index)
|
||||
const cost = serializedCharacterBytes(character)
|
||||
if (bytes + cost > maxBytes) break
|
||||
bytes += cost
|
||||
end += character.length
|
||||
index += character.length
|
||||
}
|
||||
return end === text.length ? text : text.slice(0, end)
|
||||
return end === text.length ? text : intrinsicReflectApply(intrinsicStringSlice, text, [0, end]) as string
|
||||
}
|
||||
|
||||
@@ -11,10 +11,60 @@ const intrinsicReflectApply = Reflect.get(Reflect, 'apply') as (
|
||||
thisArgument: unknown,
|
||||
argumentsList: readonly unknown[],
|
||||
) => unknown
|
||||
const IntrinsicError = Error
|
||||
const IntrinsicSet = Set
|
||||
const intrinsicArrayIsArray = Array.isArray
|
||||
const intrinsicNumberIsFinite = Number.isFinite
|
||||
const intrinsicNumberIsSafeInteger = Number.isSafeInteger
|
||||
const intrinsicObjectDefineProperty = Object.defineProperty
|
||||
const intrinsicObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor
|
||||
const intrinsicObjectGetPrototypeOf = Object.getPrototypeOf
|
||||
const intrinsicObjectHasOwn = Object.hasOwn
|
||||
const intrinsicObjectIs = Object.is
|
||||
const intrinsicObjectKeys = Object.keys
|
||||
const intrinsicObjectPropertyIsEnumerable = Reflect.get(Object.prototype, 'propertyIsEnumerable') as IntrinsicCallable
|
||||
const intrinsicReflectOwnKeys = Reflect.ownKeys
|
||||
const intrinsicSetAdd = Reflect.get(Set.prototype, 'add') as IntrinsicCallable
|
||||
const intrinsicSetDelete = Reflect.get(Set.prototype, 'delete') as IntrinsicCallable
|
||||
const intrinsicSetHas = Reflect.get(Set.prototype, 'has') as IntrinsicCallable
|
||||
|
||||
/** Append without consulting a model-mutated `Array.prototype`. */
|
||||
function append<T>(target: T[], value: T): void {
|
||||
intrinsicObjectDefineProperty(target, target.length, {
|
||||
value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
})
|
||||
}
|
||||
|
||||
/** Pop without consulting a model-mutated `Array.prototype`. */
|
||||
function takeLast<T>(target: T[]): T | undefined {
|
||||
if (target.length === 0) return undefined
|
||||
const index = target.length - 1
|
||||
const value = target[index]
|
||||
intrinsicObjectDefineProperty(target, 'length', { value: index })
|
||||
return value
|
||||
}
|
||||
|
||||
/** Whether one captured-intrinsic Set contains a value. */
|
||||
function setHas<T>(target: Set<T>, value: T): boolean {
|
||||
return intrinsicReflectApply(intrinsicSetHas, target, [value]) as boolean
|
||||
}
|
||||
|
||||
/** Add to one captured-intrinsic Set. */
|
||||
function setAdd<T>(target: Set<T>, value: T): void {
|
||||
intrinsicReflectApply(intrinsicSetAdd, target, [value])
|
||||
}
|
||||
|
||||
/** Delete from one captured-intrinsic Set. */
|
||||
function setDelete<T>(target: Set<T>, value: T): void {
|
||||
intrinsicReflectApply(intrinsicSetDelete, target, [value])
|
||||
}
|
||||
|
||||
/** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
|
||||
function hasIntrinsicConstructor(prototype: object, name: 'Array' | 'Object'): boolean {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'constructor')
|
||||
const descriptor = intrinsicObjectGetOwnPropertyDescriptor(prototype, 'constructor')
|
||||
const constructor: unknown = descriptor?.value
|
||||
if (typeof constructor !== 'function') return false
|
||||
try {
|
||||
@@ -28,14 +78,14 @@ function hasIntrinsicConstructor(prototype: object, name: 'Array' | 'Object'): b
|
||||
|
||||
/** Whether a candidate is one realm's intrinsic `Object.prototype`. */
|
||||
function isIntrinsicObjectPrototype(value: object): boolean {
|
||||
return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, 'Object')
|
||||
return intrinsicObjectGetPrototypeOf(value) === null && hasIntrinsicConstructor(value, 'Object')
|
||||
}
|
||||
|
||||
/** 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)
|
||||
if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
|
||||
const objectPrototype: unknown = Object.getPrototypeOf(prototype)
|
||||
const prototype: unknown = intrinsicObjectGetPrototypeOf(value)
|
||||
if (!intrinsicArrayIsArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
|
||||
const objectPrototype: unknown = intrinsicObjectGetPrototypeOf(prototype)
|
||||
return typeof objectPrototype === 'object'
|
||||
&& objectPrototype !== null
|
||||
&& isIntrinsicObjectPrototype(objectPrototype)
|
||||
@@ -43,15 +93,18 @@ function hasPlainArrayPrototype(value: unknown[]): boolean {
|
||||
|
||||
/** Whether an object is a plain or null-prototype record from any JavaScript realm. */
|
||||
function hasPlainObjectPrototype(value: object): boolean {
|
||||
const prototype: unknown = Object.getPrototypeOf(value)
|
||||
const prototype: unknown = intrinsicObjectGetPrototypeOf(value)
|
||||
return prototype === null
|
||||
|| typeof prototype === 'object' && isIntrinsicObjectPrototype(prototype)
|
||||
}
|
||||
|
||||
/** 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
|
||||
const keys = intrinsicReflectOwnKeys(value)
|
||||
for (let index = 0; index < keys.length; index++) {
|
||||
const key = keys[index]
|
||||
if (typeof key !== 'string' || !intrinsicReflectApply(intrinsicObjectPropertyIsEnumerable, value, [key])) return undefined
|
||||
}
|
||||
return keys as string[]
|
||||
}
|
||||
|
||||
@@ -76,15 +129,20 @@ type SnapshotTask =
|
||||
* @returns a detached lossless-JSON snapshot, or `undefined` when invalid.
|
||||
*/
|
||||
export function snapshotCodeJsonValue(value: unknown): CodeJsonValue | undefined {
|
||||
const active = new Set<object>()
|
||||
const active = new IntrinsicSet<object>()
|
||||
let root: CodeJsonValue | undefined
|
||||
const assign = (destination: SnapshotDestination, item: CodeJsonValue): void => {
|
||||
if (destination.kind === 'root') {
|
||||
root = item
|
||||
} else if (destination.kind === 'array') {
|
||||
destination.target[destination.index] = item
|
||||
intrinsicObjectDefineProperty(destination.target, destination.index, {
|
||||
value: item,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
})
|
||||
} else {
|
||||
Object.defineProperty(destination.target, destination.key, {
|
||||
intrinsicObjectDefineProperty(destination.target, destination.key, {
|
||||
value: item,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
@@ -94,14 +152,14 @@ export function snapshotCodeJsonValue(value: unknown): CodeJsonValue | undefined
|
||||
}
|
||||
|
||||
const tasks: SnapshotTask[] = [{ kind: 'visit', value, destination: { kind: 'root' } }]
|
||||
for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
|
||||
for (let task = takeLast(tasks); task !== undefined; task = takeLast(tasks)) {
|
||||
if (task.kind === 'leave') {
|
||||
active.delete(task.source)
|
||||
setDelete(active, task.source)
|
||||
continue
|
||||
}
|
||||
if (task.kind === 'array-item') {
|
||||
if (!Object.hasOwn(task.source, task.index)) return undefined
|
||||
tasks.push({
|
||||
if (!intrinsicObjectHasOwn(task.source, task.index)) return undefined
|
||||
append(tasks, {
|
||||
kind: 'visit',
|
||||
value: task.source[task.index],
|
||||
destination: { kind: 'array', target: task.target, index: task.index },
|
||||
@@ -109,7 +167,7 @@ export function snapshotCodeJsonValue(value: unknown): CodeJsonValue | undefined
|
||||
continue
|
||||
}
|
||||
if (task.kind === 'object-property') {
|
||||
tasks.push({
|
||||
append(tasks, {
|
||||
kind: 'visit',
|
||||
value: task.source[task.key],
|
||||
destination: { kind: 'object', target: task.target, key: task.key },
|
||||
@@ -127,23 +185,23 @@ export function snapshotCodeJsonValue(value: unknown): CodeJsonValue | undefined
|
||||
continue
|
||||
}
|
||||
if (typeof candidate === 'number') {
|
||||
if (!Number.isFinite(candidate) || Object.is(candidate, -0)) return undefined
|
||||
if (!intrinsicNumberIsFinite(candidate) || intrinsicObjectIs(candidate, -0)) return undefined
|
||||
assign(task.destination, candidate)
|
||||
continue
|
||||
}
|
||||
if (typeof candidate !== 'object') return undefined
|
||||
if (active.has(candidate)) return undefined
|
||||
if (setHas(active, candidate)) return undefined
|
||||
|
||||
if (Array.isArray(candidate)) {
|
||||
if (intrinsicArrayIsArray(candidate)) {
|
||||
if (!hasPlainArrayPrototype(candidate)) return undefined
|
||||
const length = candidate.length
|
||||
if (Reflect.ownKeys(candidate).length !== length + 1) return undefined
|
||||
if (intrinsicReflectOwnKeys(candidate).length !== length + 1) return undefined
|
||||
const target: CodeJsonValue[] = []
|
||||
assign(task.destination, target)
|
||||
active.add(candidate)
|
||||
tasks.push({ kind: 'leave', source: candidate })
|
||||
setAdd(active, candidate)
|
||||
append(tasks, { kind: 'leave', source: candidate })
|
||||
for (let index = length - 1; index >= 0; index--) {
|
||||
tasks.push({ kind: 'array-item', source: candidate, index, target })
|
||||
append(tasks, { kind: 'array-item', source: candidate, index, target })
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -153,13 +211,13 @@ export function snapshotCodeJsonValue(value: unknown): CodeJsonValue | undefined
|
||||
if (keys === undefined) return undefined
|
||||
const target: Record<string, CodeJsonValue> = {}
|
||||
assign(task.destination, target)
|
||||
active.add(candidate)
|
||||
tasks.push({ kind: 'leave', source: candidate })
|
||||
setAdd(active, candidate)
|
||||
append(tasks, { kind: 'leave', source: candidate })
|
||||
for (let index = keys.length - 1; index >= 0; index--) {
|
||||
const key = keys[index]
|
||||
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
||||
if (key === undefined) return undefined
|
||||
tasks.push({ kind: 'object-property', source: candidate as Record<string, unknown>, key, target })
|
||||
append(tasks, { kind: 'object-property', source: candidate as Record<string, unknown>, key, target })
|
||||
}
|
||||
}
|
||||
return root
|
||||
@@ -192,29 +250,29 @@ export type WorkerJsonWire = WorkerJsonToken[]
|
||||
export function encodeWorkerJson(value: CodeJsonValue): WorkerJsonWire {
|
||||
const wire: WorkerJsonWire = []
|
||||
const pending: CodeJsonValue[] = [value]
|
||||
for (let current = pending.pop(); current !== undefined; current = pending.pop()) {
|
||||
for (let current = takeLast(pending); current !== undefined; current = takeLast(pending)) {
|
||||
if (current === null || typeof current === 'boolean' || typeof current === 'number' || typeof current === 'string') {
|
||||
wire.push(current)
|
||||
append(wire, current)
|
||||
continue
|
||||
}
|
||||
if (Array.isArray(current)) {
|
||||
wire.push({ kind: 'array', length: current.length })
|
||||
if (intrinsicArrayIsArray(current)) {
|
||||
append(wire, { kind: 'array', length: current.length })
|
||||
for (let index = current.length - 1; index >= 0; index--) {
|
||||
const item = current[index]
|
||||
if (item === undefined) throw new Error('cannot encode a sparse JSON array')
|
||||
pending.push(item)
|
||||
if (item === undefined) throw new IntrinsicError('cannot encode a sparse JSON array')
|
||||
append(pending, item)
|
||||
}
|
||||
continue
|
||||
}
|
||||
const keys = Object.keys(current)
|
||||
wire.push({ kind: 'object', keys })
|
||||
const keys = intrinsicObjectKeys(current)
|
||||
append(wire, { kind: 'object', keys })
|
||||
for (let index = keys.length - 1; index >= 0; index--) {
|
||||
const key = keys[index]
|
||||
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
||||
if (key === undefined) throw new Error('cannot encode a missing JSON object key')
|
||||
if (key === undefined) throw new IntrinsicError('cannot encode a missing JSON object key')
|
||||
const item = current[key]
|
||||
if (item === undefined) throw new Error('cannot encode an undefined JSON object property')
|
||||
pending.push(item)
|
||||
if (item === undefined) throw new IntrinsicError('cannot encode an undefined JSON object property')
|
||||
append(pending, item)
|
||||
}
|
||||
}
|
||||
return wire
|
||||
@@ -226,36 +284,46 @@ type DecodeFrame =
|
||||
|
||||
/** Whether an array contains exactly its dense indexed slots and `length`. */
|
||||
function isDenseArray(value: unknown[]): boolean {
|
||||
if (!hasPlainArrayPrototype(value) || Reflect.ownKeys(value).length !== value.length + 1) return false
|
||||
if (!hasPlainArrayPrototype(value) || intrinsicReflectOwnKeys(value).length !== value.length + 1) return false
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
if (!Object.hasOwn(value, index)) return false
|
||||
if (!intrinsicObjectHasOwn(value, index)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** Whether one exact string-key list contains a key, without consulting its prototype. */
|
||||
function keysContain(keys: string[], expected: string): boolean {
|
||||
for (let index = 0; index < keys.length; index++) {
|
||||
if (keys[index] === expected) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Return one exact container marker, or reject any extra/missing fields. */
|
||||
function containerToken(value: object): ArrayWireToken | ObjectWireToken | undefined {
|
||||
if (Array.isArray(value) || !hasPlainObjectPrototype(value)) return undefined
|
||||
if (intrinsicArrayIsArray(value) || !hasPlainObjectPrototype(value)) return undefined
|
||||
const keys = enumerableStringKeys(value)
|
||||
if (keys === undefined) return undefined
|
||||
const token = value as Record<string, unknown>
|
||||
if (token.kind === 'array') {
|
||||
if (keys.length !== 2 || !keys.includes('kind') || !keys.includes('length')) return undefined
|
||||
if (keys.length !== 2 || !keysContain(keys, 'kind') || !keysContain(keys, 'length')) return undefined
|
||||
const length = token.length
|
||||
return typeof length === 'number' && Number.isSafeInteger(length) && length >= 0
|
||||
return typeof length === 'number' && intrinsicNumberIsSafeInteger(length) && length >= 0
|
||||
? { kind: 'array', length }
|
||||
: undefined
|
||||
}
|
||||
if (token.kind === 'object') {
|
||||
if (keys.length !== 2 || !keys.includes('kind') || !keys.includes('keys')) return undefined
|
||||
if (keys.length !== 2 || !keysContain(keys, 'kind') || !keysContain(keys, 'keys')) return undefined
|
||||
const objectKeys = token.keys
|
||||
if (!Array.isArray(objectKeys) || !isDenseArray(objectKeys)) return undefined
|
||||
const unique = new Set<string>()
|
||||
if (!intrinsicArrayIsArray(objectKeys) || !isDenseArray(objectKeys)) return undefined
|
||||
const unique = new IntrinsicSet<string>()
|
||||
const normalizedKeys: string[] = []
|
||||
for (const key of objectKeys as unknown[]) {
|
||||
if (typeof key !== 'string' || unique.has(key)) return undefined
|
||||
unique.add(key)
|
||||
normalizedKeys.push(key)
|
||||
const objectKeyValues = objectKeys as unknown[]
|
||||
for (let index = 0; index < objectKeyValues.length; index++) {
|
||||
const key = objectKeyValues[index]
|
||||
if (typeof key !== 'string' || setHas(unique, key)) return undefined
|
||||
setAdd(unique, key)
|
||||
append(normalizedKeys, key)
|
||||
}
|
||||
return { kind: 'object', keys: normalizedKeys }
|
||||
}
|
||||
@@ -271,14 +339,14 @@ function containerToken(value: object): ArrayWireToken | ObjectWireToken | undef
|
||||
*/
|
||||
export function decodeWorkerJson(input: unknown): CodeJsonValue | undefined {
|
||||
try {
|
||||
if (!Array.isArray(input) || !isDenseArray(input) || input.length === 0) return undefined
|
||||
if (!intrinsicArrayIsArray(input) || !isDenseArray(input) || input.length === 0) return undefined
|
||||
const wire = input as unknown[]
|
||||
const frames: DecodeFrame[] = []
|
||||
let root: CodeJsonValue | undefined
|
||||
let rootAssigned = false
|
||||
|
||||
const attach = (value: CodeJsonValue): boolean => {
|
||||
const parent = frames.at(-1)
|
||||
const parent = frames[frames.length - 1]
|
||||
if (!parent) {
|
||||
if (rootAssigned) return false
|
||||
root = value
|
||||
@@ -288,12 +356,12 @@ export function decodeWorkerJson(input: unknown): CodeJsonValue | undefined {
|
||||
/* v8 ignore next -- completed frames are popped before another token can attach. */
|
||||
if (parent.index >= (parent.kind === 'array' ? parent.length : parent.keys.length)) return false
|
||||
if (parent.kind === 'array') {
|
||||
parent.target.push(value)
|
||||
append(parent.target, value)
|
||||
} else {
|
||||
const key = parent.keys[parent.index]
|
||||
/* v8 ignore next -- object frames are built from validated keys and their exact length. */
|
||||
if (key === undefined) return false
|
||||
Object.defineProperty(parent.target, key, {
|
||||
intrinsicObjectDefineProperty(parent.target, key, {
|
||||
value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
@@ -311,7 +379,7 @@ export function decodeWorkerJson(input: unknown): CodeJsonValue | undefined {
|
||||
if (token === null || typeof token === 'boolean' || typeof token === 'string') {
|
||||
value = token
|
||||
} else if (typeof token === 'number') {
|
||||
if (!Number.isFinite(token) || Object.is(token, -0)) return undefined
|
||||
if (!intrinsicNumberIsFinite(token) || intrinsicObjectIs(token, -0)) return undefined
|
||||
value = token
|
||||
} else {
|
||||
if (typeof token !== 'object') return undefined
|
||||
@@ -331,13 +399,13 @@ export function decodeWorkerJson(input: unknown): CodeJsonValue | undefined {
|
||||
}
|
||||
}
|
||||
if (!attach(value)) return undefined
|
||||
if (frame) frames.push(frame)
|
||||
if (frame) append(frames, frame)
|
||||
while (frames.length > 0) {
|
||||
const current = frames.at(-1)
|
||||
const current = frames[frames.length - 1]
|
||||
/* v8 ignore next -- the loop condition guarantees a final frame. */
|
||||
if (current === undefined) break
|
||||
if (current.index < (current.kind === 'array' ? current.length : current.keys.length)) break
|
||||
frames.pop()
|
||||
takeLast(frames)
|
||||
}
|
||||
}
|
||||
return frames.length === 0 ? root : undefined
|
||||
|
||||
@@ -68,4 +68,42 @@ describe('jsonValueBytesUpTo', () => {
|
||||
expect(jsonValueBytesUpTo(value, 10_004)).toBe(10_004)
|
||||
expect(jsonValueBytesUpTo(value, 10_003)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('uses module-captured intrinsics after model-visible globals are mutated', () => {
|
||||
const value: CodeJsonValue = { payload: ['€', 42] }
|
||||
const bytes = Buffer.byteLength(JSON.stringify(value), 'utf8')
|
||||
const arrayIsArrayDescriptor = Object.getOwnPropertyDescriptor(Array, 'isArray')!
|
||||
const arrayPopDescriptor = Object.getOwnPropertyDescriptor(Array.prototype, 'pop')!
|
||||
const arrayPushDescriptor = Object.getOwnPropertyDescriptor(Array.prototype, 'push')!
|
||||
const byteLengthDescriptor = Object.getOwnPropertyDescriptor(Buffer, 'byteLength')!
|
||||
const objectKeysDescriptor = Object.getOwnPropertyDescriptor(Object, 'keys')!
|
||||
const charCodeAtDescriptor = Object.getOwnPropertyDescriptor(String.prototype, 'charCodeAt')!
|
||||
const codePointAtDescriptor = Object.getOwnPropertyDescriptor(String.prototype, 'codePointAt')!
|
||||
const sliceDescriptor = Object.getOwnPropertyDescriptor(String.prototype, 'slice')!
|
||||
let measured: number | undefined
|
||||
let prefix = ''
|
||||
try {
|
||||
Array.isArray = (_value: unknown): _value is never[] => false
|
||||
Array.prototype.pop = () => { throw new Error('mutated pop') }
|
||||
Array.prototype.push = () => { throw new Error('mutated push') }
|
||||
Buffer.byteLength = () => 0
|
||||
Object.keys = () => []
|
||||
String.prototype.charCodeAt = () => { throw new Error('mutated charCodeAt') }
|
||||
String.prototype.codePointAt = () => { throw new Error('mutated codePointAt') }
|
||||
String.prototype.slice = () => { throw new Error('mutated slice') }
|
||||
measured = jsonValueBytesUpTo(value, bytes)
|
||||
prefix = truncateJsonStringBytes('€x', 5)
|
||||
} finally {
|
||||
Object.defineProperty(Array, 'isArray', arrayIsArrayDescriptor)
|
||||
Object.defineProperty(Array.prototype, 'pop', arrayPopDescriptor)
|
||||
Object.defineProperty(Array.prototype, 'push', arrayPushDescriptor)
|
||||
Object.defineProperty(Buffer, 'byteLength', byteLengthDescriptor)
|
||||
Object.defineProperty(Object, 'keys', objectKeysDescriptor)
|
||||
Object.defineProperty(String.prototype, 'charCodeAt', charCodeAtDescriptor)
|
||||
Object.defineProperty(String.prototype, 'codePointAt', codePointAtDescriptor)
|
||||
Object.defineProperty(String.prototype, 'slice', sliceDescriptor)
|
||||
}
|
||||
expect(measured).toBe(bytes)
|
||||
expect(prefix).toBe('€')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -657,6 +657,41 @@ describe('WorkerCodeRuntime — hostile programs (real workers)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves binding and completion JSON after model code mutates boundary globals', async () => {
|
||||
const { runtime } = await setup()
|
||||
const result = await runtime.run({
|
||||
program: `
|
||||
const arrayPrototype = Array.prototype;
|
||||
const objectPrototype = Object.prototype;
|
||||
const setPrototype = Set.prototype;
|
||||
const stringPrototype = String.prototype;
|
||||
Array.isArray = () => false;
|
||||
arrayPrototype.at = arrayPrototype.includes = arrayPrototype.pop = arrayPrototype.push = () => { throw new Error('mutated array method') };
|
||||
Object.defineProperty = Object.getOwnPropertyDescriptor = Object.getPrototypeOf = Object.keys = () => { throw new Error('mutated object method') };
|
||||
Object.hasOwn = () => false;
|
||||
Object.is = () => true;
|
||||
objectPrototype.propertyIsEnumerable = () => false;
|
||||
Number.isFinite = Number.isSafeInteger = () => false;
|
||||
Reflect.apply = Reflect.ownKeys = () => { throw new Error('mutated reflect method') };
|
||||
setPrototype.add = setPrototype.delete = setPrototype.has = () => { throw new Error('mutated set method') };
|
||||
stringPrototype.charCodeAt = stringPrototype.codePointAt = stringPrototype.slice = () => { throw new Error('mutated string method') };
|
||||
Buffer.byteLength = () => 0;
|
||||
Function.prototype.toString = () => 'mutated';
|
||||
globalThis.Array = globalThis.Buffer = globalThis.Function = globalThis.Number = globalThis.Object = globalThis.Reflect = globalThis.Set = globalThis.String = undefined;
|
||||
const echoed = await tools.echo({ request: ['€', 1] });
|
||||
return { echoed, completion: { ok: true, amount: 42 } };
|
||||
`,
|
||||
bindings: tools({ echo: async args => args }),
|
||||
})
|
||||
expect(result).toEqual({
|
||||
logs: [],
|
||||
value: {
|
||||
echoed: { request: ['€', 1] },
|
||||
completion: { ok: true, amount: 42 },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects forged lossy binding arguments again at the host boundary', async () => {
|
||||
const { runtime } = await setup()
|
||||
let calls = 0
|
||||
|
||||
@@ -230,6 +230,7 @@ describe('flat worker JSON wire', () => {
|
||||
[foreignMarker],
|
||||
[hiddenMarker],
|
||||
[{ kind: 'unknown' }],
|
||||
[{ kind: 'array', bogus: 0 }],
|
||||
[{ kind: 'array' }],
|
||||
[{ kind: 'array', length: '1' }],
|
||||
[{ kind: 'array', length: -1 }],
|
||||
|
||||
Reference in New Issue
Block a user