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:
Tianyi Cui
2026-07-23 03:59:53 +08:00
8 changed files with 92 additions and 60 deletions

View File

@@ -21,9 +21,9 @@ Every field is validated and defaulted; `maxOutputBytes` is a safe integer of at
- **One fresh worker per run, no pooling** — a program's world dies with its worker: no cross-run state to log, state bleed unrepresentable, runs reconstructable from the session log alone.
- **Type-strip host-side, in execution context** — the program is wrapped in an async-function shell, stripped with `node:module`'s `stripTypeScriptTypes` (erasable syntax only — `enum`/namespaces are rejected as a program `exception` and no worker spawns), and sliced back out byte-positioned; it then executes as the body of an `AsyncFunction`, so top-level `await`/`return` work.
- **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.
- **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. Failures use module-captured error and property-definition intrinsics plus null-prototype descriptors, so later model mutations cannot turn a rejected binding into a worker crash.
- **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. 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.
- **Intermediate binding values are complete JSON** — binding arguments and resolutions undergo iterative lossless-JSON validation. Before program execution, the worker captures its own realm's plain-container prototype identities plus the native function-source check used only for foreign realms, so constructor-slot mutation and user-authored impostors cannot change container classification. It also captures every structural and metering intrinsic used by this JSON boundary, creates property descriptors without a prototype, and bypasses mutable collection prototypes for private traversal state; model mutations of globals, prototype methods, or descriptor-shaped `Object.prototype` fields therefore 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.

View File

@@ -10,6 +10,18 @@ import type { DoneMessage, ReplyMessage, WorkerBootData, WorkerToHost } from './
import { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from './output-json.ts'
import { decodeWorkerJson, encodeWorkerJson, snapshotCodeJsonValue } from './worker-json.ts'
const CapturedError = Error
const capturedObjectCreate = Object.create
const capturedObjectDefineProperty = Object.defineProperty
/** Define one public binding-error field without consulting mutable globals or descriptor prototypes. */
function defineBindingErrorField(error: Error, key: string, value: string): void {
const attributes = capturedObjectCreate(null) as PropertyDescriptor
attributes.enumerable = true
attributes.value = value
capturedObjectDefineProperty(error, key, attributes)
}
/** The port surface the bootstrap needs — satisfied by `parentPort` and by the tests' fake. */
export interface BootstrapPort {
postMessage(message: WorkerToHost): void
@@ -66,7 +78,7 @@ export class LogBuffer {
if (prefix.length > 0) {
const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes)
/* v8 ignore next -- truncateJsonStringBytes guarantees the returned prefix fits. */
if (prefixBytes === undefined) throw new Error('worker output ledger produced an oversized log prefix')
if (prefixBytes === undefined) throw new CapturedError('worker output ledger produced an oversized log prefix')
this.bytes += prefixBytes + separatorBytes
this.entries += 1
this.sink(prefix)
@@ -208,7 +220,7 @@ export function prepareException(
): Omit<DoneMessage, 'type'> {
let message: string
try {
const detail: unknown = error instanceof Error ? error.stack ?? error.message : error
const detail: unknown = error instanceof CapturedError ? error.stack ?? error.message : error
message = typeof detail === 'string' ? detail : String(detail)
} catch {
message = 'program threw an unrenderable value'
@@ -233,18 +245,18 @@ export type BindingErrorConstructor = new (memberName: string, message: string)
function makeBindingErrorClass(
descriptor: { name: string; memberNameProperty: string },
): BindingErrorConstructor {
return class BindingCallError extends Error {
return class BindingCallError extends CapturedError {
constructor(memberName: string, message: string) {
super(message)
Object.defineProperty(this, 'name', { enumerable: true, value: descriptor.name })
Object.defineProperty(this, descriptor.memberNameProperty, { enumerable: true, value: memberName })
defineBindingErrorField(this, 'name', descriptor.name)
defineBindingErrorField(this, descriptor.memberNameProperty, memberName)
}
}
}
/** Create the namespace-specific rejection for one failed binding call. */
function bindingFailure(errorClass: BindingErrorConstructor | undefined, memberName: string, message: string): Error {
return errorClass ? new errorClass(memberName, message) : new Error(message)
return errorClass ? new errorClass(memberName, message) : new CapturedError(message)
}
/**
@@ -278,10 +290,10 @@ export function wireReplies(port: BootstrapPort, pending: Map<number, PendingCal
pending.delete(message.id)
if (message.ok) {
const value = decodeWorkerJson(message.value)
if (value === undefined) entry.reject(new Error('binding resolution must be lossless JSON'))
if (value === undefined) entry.reject(new CapturedError('binding resolution must be lossless JSON'))
else entry.resolve(value)
} else {
entry.reject(new Error(message.message))
entry.reject(new CapturedError(message.message))
}
})
}
@@ -335,7 +347,7 @@ export function makeNamespaces(
port.postMessage({ type: 'call', id, global, name, args: encodeWorkerJson(detached) })
} catch (error: unknown) {
pending.delete(id)
const message = `binding arguments must be structured-cloneable: ${error instanceof Error ? error.message : String(error)}`
const message = `binding arguments must be structured-cloneable: ${error instanceof CapturedError ? error.message : String(error)}`
reject(bindingFailure(errorClass, name, message))
}
})
@@ -380,7 +392,7 @@ export async function runWorkerMain(
errorClassParameters.push(namespace.errorClass.name)
const errorClass = errorClasses.get(namespace.global)
/* v8 ignore next -- makeBindingErrorClasses covers every declaration in the same data. */
if (!errorClass) throw new Error(`missing binding error class for ${namespace.global}`)
if (!errorClass) throw new CapturedError(`missing binding error class for ${namespace.global}`)
errorClassValues.push(errorClass)
}
const consoleShim = makeConsoleShim(logs)

View File

@@ -12,6 +12,7 @@ const intrinsicReflectApply = Reflect.apply as (
const intrinsicArrayIsArray = Array.isArray
const IntrinsicBuffer = Buffer
const intrinsicBufferByteLength = Reflect.get(Buffer, 'byteLength') as IntrinsicCallable
const intrinsicObjectCreate = Object.create
const intrinsicObjectDefineProperty = Object.defineProperty
const intrinsicObjectKeys = Object.keys
const intrinsicString = String
@@ -19,6 +20,22 @@ const intrinsicStringCharCodeAt = Reflect.get(String.prototype, 'charCodeAt') as
const intrinsicStringCodePointAt = Reflect.get(String.prototype, 'codePointAt') as IntrinsicCallable
const intrinsicStringSlice = Reflect.get(String.prototype, 'slice') as IntrinsicCallable
/** Build a data descriptor that cannot inherit model-defined accessor fields. */
function dataDescriptor(value: unknown): PropertyDescriptor {
const descriptor = intrinsicObjectCreate(null) as PropertyDescriptor
descriptor.value = value
return descriptor
}
/** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
function defineEnumerableDataProperty(target: object, key: PropertyKey, value: unknown): void {
const descriptor = dataDescriptor(value)
descriptor.enumerable = true
descriptor.configurable = true
descriptor.writable = true
intrinsicObjectDefineProperty(target, key, descriptor)
}
/** UTF-8 byte length through the module-captured Node intrinsic. */
function byteLength(text: string): number {
return intrinsicReflectApply(intrinsicBufferByteLength, IntrinsicBuffer, [text, 'utf8']) as number
@@ -26,12 +43,7 @@ function byteLength(text: string): 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,
})
defineEnumerableDataProperty(target, target.length, value)
}
/** Pop without consulting a model-mutated `Array.prototype`. */
@@ -39,7 +51,7 @@ 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 })
intrinsicObjectDefineProperty(target, 'length', dataDescriptor(index))
return value
}

View File

@@ -14,28 +14,42 @@ const intrinsicReflectApply = Reflect.get(Reflect, 'apply') as (
const IntrinsicError = Error
const IntrinsicSet = Set
const intrinsicArrayIsArray = Array.isArray
const intrinsicArrayPrototype = Array.prototype
const intrinsicNumberIsFinite = Number.isFinite
const intrinsicNumberIsSafeInteger = Number.isSafeInteger
const intrinsicObjectCreate = Object.create
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 intrinsicObjectPrototype = Object.prototype
const intrinsicObjectPropertyIsEnumerable = Reflect.get(intrinsicObjectPrototype, '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
/** Build a data descriptor that cannot inherit model-defined accessor fields. */
function dataDescriptor(value: unknown): PropertyDescriptor {
const descriptor = intrinsicObjectCreate(null) as PropertyDescriptor
descriptor.value = value
return descriptor
}
/** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
function defineEnumerableDataProperty(target: object, key: PropertyKey, value: unknown): void {
const descriptor = dataDescriptor(value)
descriptor.enumerable = true
descriptor.configurable = true
descriptor.writable = true
intrinsicObjectDefineProperty(target, key, descriptor)
}
/** 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,
})
defineEnumerableDataProperty(target, target.length, value)
}
/** Pop without consulting a model-mutated `Array.prototype`. */
@@ -43,7 +57,7 @@ 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 })
intrinsicObjectDefineProperty(target, 'length', dataDescriptor(index))
return value
}
@@ -76,26 +90,28 @@ function hasIntrinsicConstructor(prototype: object, name: 'Array' | 'Object'): b
}
}
/** Whether a candidate is one realm's intrinsic `Object.prototype`. */
function isIntrinsicObjectPrototype(value: object): boolean {
/** Whether a candidate is a foreign realm's intrinsic `Object.prototype`. */
function isForeignIntrinsicObjectPrototype(value: object): boolean {
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 = intrinsicObjectGetPrototypeOf(value)
if (prototype === intrinsicArrayPrototype) return true
if (!intrinsicArrayIsArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
const objectPrototype: unknown = intrinsicObjectGetPrototypeOf(prototype)
return typeof objectPrototype === 'object'
&& objectPrototype !== null
&& isIntrinsicObjectPrototype(objectPrototype)
&& isForeignIntrinsicObjectPrototype(objectPrototype)
}
/** Whether an object is a plain or null-prototype record from any JavaScript realm. */
function hasPlainObjectPrototype(value: object): boolean {
const prototype: unknown = intrinsicObjectGetPrototypeOf(value)
return prototype === null
|| typeof prototype === 'object' && isIntrinsicObjectPrototype(prototype)
|| prototype === intrinsicObjectPrototype
|| typeof prototype === 'object' && isForeignIntrinsicObjectPrototype(prototype)
}
/** Return every JSON-visible object key, or reject own data JSON would discard. */
@@ -135,19 +151,9 @@ export function snapshotCodeJsonValue(value: unknown): CodeJsonValue | undefined
if (destination.kind === 'root') {
root = item
} else if (destination.kind === 'array') {
intrinsicObjectDefineProperty(destination.target, destination.index, {
value: item,
enumerable: true,
configurable: true,
writable: true,
})
defineEnumerableDataProperty(destination.target, destination.index, item)
} else {
intrinsicObjectDefineProperty(destination.target, destination.key, {
value: item,
enumerable: true,
configurable: true,
writable: true,
})
defineEnumerableDataProperty(destination.target, destination.key, item)
}
}
@@ -361,12 +367,7 @@ export function decodeWorkerJson(input: unknown): CodeJsonValue | undefined {
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
intrinsicObjectDefineProperty(parent.target, key, {
value,
enumerable: true,
configurable: true,
writable: true,
})
defineEnumerableDataProperty(parent.target, key, value)
}
parent.index += 1
return true

View File

@@ -677,16 +677,23 @@ describe('WorkerCodeRuntime — hostile programs (real workers)', () => {
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;
objectPrototype.get = () => undefined;
objectPrototype.constructor = arrayPrototype.constructor = null;
globalThis.Array = globalThis.Buffer = globalThis.Error = 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 } };
let failure;
try { await tools.fail({}) } catch (error) {
failure = { typed: error instanceof ToolCallError, name: error.name, toolName: error.toolName, message: error.message };
}
return { echoed, failure, completion: { ok: true, amount: 42 } };
`,
bindings: tools({ echo: async args => args }),
bindings: tools({ echo: async args => args, fail: async () => { throw new Error('nope') } }),
})
expect(result).toEqual({
logs: [],
value: {
echoed: { request: ['€', 1] },
failure: { typed: true, name: 'ToolCallError', toolName: 'fail', message: 'nope' },
completion: { ok: true, amount: 42 },
},
})