chore: update merge branch with latest v2

This commit is contained in:
Aiden Cline 2026-07-13 21:39:11 +00:00
commit 6cbb2ce464
20 changed files with 257 additions and 257 deletions

View file

@ -16,8 +16,8 @@
## Future Design Notes ## Future Design Notes
- If a captured user-visible output channel returns (an earlier `output.text`/`output.file`/`output.image` API was removed from v1), keep `output` as its name, distinct from the program return value: `return` stays the structured result for the model, while `output.*` describes artifacts the host may render into a conversation or UI after execution. Keep this host-neutral and let applications decide how captured output is delivered. In v1, hosts collect media host-side (outside the sandbox) instead. - If a captured user-visible output channel returns (an earlier `output.text`/`output.file`/`output.image` API was removed from v1), keep `output` as its name, distinct from the program return value: `return` stays the structured result for the model, while `output.*` describes artifacts the host may render into a conversation or UI after execution. Keep this host-neutral and let applications decide how captured output is delivered. In v1, hosts collect media host-side (outside CodeMode) instead.
- Improve the sandbox failure taxonomy. Distinguish parse/compile mistakes, unsupported syntax, user-thrown errors, invalid returned data, tool refusal, tool internal failure, timeout, and genuine runtime defects so agents can recover accurately instead of treating everything as a generic execution failure. - Improve the failure taxonomy. Distinguish parse/compile mistakes, unsupported syntax, user-thrown errors, invalid returned data, tool refusal, tool internal failure, timeout, and genuine runtime defects so agents can recover accurately instead of treating everything as a generic execution failure.
- Preserve the public/private error split. Tool authors should be able to return a safe model-visible message while retaining a private cause for host diagnostics. Unknown host failures must remain sanitized by default. - Preserve the public/private error split. Tool authors should be able to return a safe model-visible message while retaining a private cause for host diagnostics. Unknown host failures must remain sanitized by default.
- Think deliberately about richer binary boundaries before allowing `Blob`, `File`, `ArrayBuffer`, streams, or typed arrays beyond today's JSON-like values. If CodeMode supports binary tool args/results, use explicit tagged data shapes and clear size limits rather than relying on ambient runtime serialization. - Think deliberately about richer binary boundaries before allowing `Blob`, `File`, `ArrayBuffer`, streams, or typed arrays beyond today's JSON-like values. If CodeMode supports binary tool args/results, use explicit tagged data shapes and clear size limits rather than relying on ambient runtime serialization.
- Keep host capabilities explicit. Globals such as `fetch`, `crypto`, filesystem handles, extra modules, or network clients should be opt-in runtime capabilities with obvious policy defaults, not ambient authority. Default to unavailable unless a host deliberately provides the capability. - Keep host capabilities explicit. Globals such as `fetch`, `crypto`, filesystem handles, extra modules, or network clients should be opt-in runtime capabilities with obvious policy defaults, not ambient authority. Default to unavailable unless a host deliberately provides the capability.

View file

@ -20,7 +20,7 @@ ultimate source of truth.
- [x] Top-level `await` and `return` through the program's implicit async-function scope. - [x] Top-level `await` and `return` through the program's implicit async-function scope.
- [x] Explicit `return`, final top-level expression as a REPL-style result, and `null` when no value is produced. - [x] Explicit `return`, final top-level expression as a REPL-style result, and `null` when no value is produced.
- [x] JSON-like host boundaries with `undefined` and non-finite numbers normalized to `null`. - [x] JSON-like host boundaries with `undefined` and non-finite numbers normalized to `null`.
- [x] Live Date, RegExp, Map, Set, URL, and URLSearchParams values inside the sandbox. - [x] Live Date, RegExp, Map, Set, URL, and URLSearchParams values inside CodeMode.
- [x] Tool calls through the host-provided `tools` tree only. - [x] Tool calls through the host-provided `tools` tree only.
- [x] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is - [x] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is
shadowable by program declarations like other globals. shadowable by program declarations like other globals.
@ -94,7 +94,7 @@ ultimate source of truth.
- [x] Optional property access and optional calls. - [x] Optional property access and optional calls.
- [x] Function/tool calls and spread arguments. - [x] Function/tool calls and spread arguments.
- [x] Sequence expressions (the comma operator). - [x] Sequence expressions (the comma operator).
- [x] `await` for sandbox promises; a plain value passes through unchanged, though every `await` still defers its - [x] `await` for CodeMode promises; a plain value passes through unchanged, though every `await` still defers its
continuation one reaction turn. continuation one reaction turn.
- [x] `new` for Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise. - [x] `new` for Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise.
- [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`. - [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`.
@ -109,7 +109,7 @@ ultimate source of truth.
## Promises and tools ## Promises and tools
- [x] Tool calls start eagerly and return supervised, run-once sandbox promises. - [x] Tool calls start eagerly and return supervised, run-once CodeMode promises.
- [x] Direct `await`, repeated awaits, and implicit resolution when a promise is returned from a function/program. - [x] Direct `await`, repeated awaits, and implicit resolution when a promise is returned from a function/program.
- [x] `Promise.resolve` and `Promise.reject`. - [x] `Promise.resolve` and `Promise.reject`.
- [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over supported collections containing - [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over supported collections containing
@ -148,7 +148,7 @@ ultimate source of truth.
- [x] Computed property names and object spread. - [x] Computed property names and object spread.
- [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`. - [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`.
- [x] `Object.keys` over arrays and tool references. - [x] `Object.keys` over arrays and tool references.
- [x] Object identity is preserved by in-sandbox Object helpers. - [x] Object identity is preserved by in-CodeMode Object helpers.
- [x] Blocked access to `__proto__`, `constructor`, and `prototype`. - [x] Blocked access to `__proto__`, `constructor`, and `prototype`.
- [ ] `Object.is`; runtime and tool-reference identity semantics need to be defined first. - [ ] `Object.is`; runtime and tool-reference identity semantics need to be defined first.
- [ ] `Object.groupBy`. - [ ] `Object.groupBy`.

View file

@ -13,13 +13,13 @@ import {
import { rejectCircularInsertion } from "./references.js" import { rejectCircularInsertion } from "./references.js"
import { isBlockedMember, type SafeObject } from "../tool-runtime.js" import { isBlockedMember, type SafeObject } from "../tool-runtime.js"
import { import {
SandboxDate, CodeModeDate,
SandboxMap, CodeModeMap,
SandboxPromise, CodeModePromise,
SandboxRegExp, CodeModeRegExp,
SandboxSet, CodeModeSet,
SandboxURL, CodeModeURL,
SandboxURLSearchParams, CodeModeURLSearchParams,
} from "../values.js" } from "../values.js"
import { invokeDateMethod, invokeDateStatic } from "../stdlib/date.js" import { invokeDateMethod, invokeDateStatic } from "../stdlib/date.js"
import { invokeJsonMethod } from "../stdlib/json.js" import { invokeJsonMethod } from "../stdlib/json.js"
@ -33,7 +33,7 @@ import { boundedData, coerceToNumber, coerceToString, invokeCoercion } from "../
export type CallbackRunner<R> = { export type CallbackRunner<R> = {
readonly invokeFunction: (fn: CodeModeFunction, args: Array<unknown>) => Effect.Effect<unknown, unknown, R> readonly invokeFunction: (fn: CodeModeFunction, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
readonly settlePromise: (promise: SandboxPromise) => Effect.Effect<unknown, unknown, never> readonly settlePromise: (promise: CodeModePromise) => Effect.Effect<unknown, unknown, never>
} }
export const invokeIntrinsic = <R>( export const invokeIntrinsic = <R>(
@ -57,22 +57,22 @@ export const invokeIntrinsic = <R>(
if (Array.isArray(ref.receiver)) { if (Array.isArray(ref.receiver)) {
return invokeArrayMethod(runner, ref.receiver, ref.name, args, node) return invokeArrayMethod(runner, ref.receiver, ref.name, args, node)
} }
if (ref.receiver instanceof SandboxDate) { if (ref.receiver instanceof CodeModeDate) {
return Effect.succeed(invokeDateMethod(ref.receiver, ref.name, node)) return Effect.succeed(invokeDateMethod(ref.receiver, ref.name, node))
} }
if (ref.receiver instanceof SandboxRegExp) { if (ref.receiver instanceof CodeModeRegExp) {
return Effect.succeed(invokeRegExpMethod(ref.receiver, ref.name, args, node)) return Effect.succeed(invokeRegExpMethod(ref.receiver, ref.name, args, node))
} }
if (ref.receiver instanceof SandboxMap) { if (ref.receiver instanceof CodeModeMap) {
return invokeMapMethod(runner, ref.receiver, ref.name, args, node) return invokeMapMethod(runner, ref.receiver, ref.name, args, node)
} }
if (ref.receiver instanceof SandboxSet) { if (ref.receiver instanceof CodeModeSet) {
return invokeSetMethod(runner, ref.receiver, ref.name, args, node) return invokeSetMethod(runner, ref.receiver, ref.name, args, node)
} }
if (ref.receiver instanceof SandboxURL) { if (ref.receiver instanceof CodeModeURL) {
return Effect.succeed(invokeURLMethod(ref.receiver, ref.name, node)) return Effect.succeed(invokeURLMethod(ref.receiver, ref.name, node))
} }
if (ref.receiver instanceof SandboxURLSearchParams) { if (ref.receiver instanceof CodeModeURLSearchParams) {
return invokeURLSearchParamsMethod(runner, ref.receiver, ref.name, args, node) return invokeURLSearchParamsMethod(runner, ref.receiver, ref.name, args, node)
} }
throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node) throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node)
@ -153,7 +153,7 @@ const invokeStringMethod = (value: string, name: string, args: Array<unknown>, n
result = [value] result = [value]
break break
} }
if (args[0] instanceof SandboxRegExp) { if (args[0] instanceof CodeModeRegExp) {
result = value.split(args[0].regex, optNum(1)) result = value.split(args[0].regex, optNum(1))
break break
} }
@ -181,7 +181,7 @@ const invokeStringMethod = (value: string, name: string, args: Array<unknown>, n
break break
case "replace": case "replace":
case "replaceAll": { case "replaceAll": {
if (args[0] instanceof SandboxRegExp) { if (args[0] instanceof CodeModeRegExp) {
const pattern = args[0].regex const pattern = args[0].regex
const replacement = str(1) const replacement = str(1)
if (name === "replaceAll" && !pattern.global) { if (name === "replaceAll" && !pattern.global) {
@ -278,13 +278,13 @@ const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): u
[supportedSyntaxMessage], [supportedSyntaxMessage],
) )
} }
if (args[0] instanceof SandboxMap) return Array.from(args[0].map.entries(), ([key, item]) => [key, item]) if (args[0] instanceof CodeModeMap) return Array.from(args[0].map.entries(), ([key, item]) => [key, item])
if (args[0] instanceof SandboxSet) return Array.from(args[0].set.values()) if (args[0] instanceof CodeModeSet) return Array.from(args[0].set.values())
if (args[0] instanceof SandboxURLSearchParams) { if (args[0] instanceof CodeModeURLSearchParams) {
return Array.from(args[0].params.entries(), ([key, value]) => [key, value]) return Array.from(args[0].params.entries(), ([key, value]) => [key, value])
} }
const source = args[0] const source = args[0]
if (source instanceof SandboxPromise) { if (source instanceof CodeModePromise) {
throw new InterpreterRuntimeError( throw new InterpreterRuntimeError(
"Array.from received an un-awaited Promise; await it before creating the array.", "Array.from received an un-awaited Promise; await it before creating the array.",
node, node,
@ -341,7 +341,7 @@ const invokeStringReplacer = <R>(
} }
const pattern = args[0] const pattern = args[0]
if (pattern instanceof SandboxRegExp) { if (pattern instanceof CodeModeRegExp) {
if (name === "replaceAll" && !pattern.regex.global) { if (name === "replaceAll" && !pattern.regex.global) {
throw new InterpreterRuntimeError( throw new InterpreterRuntimeError(
`String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.regex.source}/${pattern.regex.flags}g, or use String.replace to replace only the first match.`, `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.regex.source}/${pattern.regex.flags}g, or use String.replace to replace only the first match.`,
@ -364,7 +364,7 @@ const invokeStringReplacer = <R>(
for (const match of matches) { for (const match of matches) {
const replacement = yield* apply(match.args) const replacement = yield* apply(match.args)
const resolved = const resolved =
args[1] instanceof CodeModeFunction && args[1].async && replacement instanceof SandboxPromise args[1] instanceof CodeModeFunction && args[1].async && replacement instanceof CodeModePromise
? yield* runner.settlePromise(replacement) ? yield* runner.settlePromise(replacement)
: replacement : replacement
output.push( output.push(
@ -404,7 +404,7 @@ export const applyCollectionCallback = <R>(
const invokeMapMethod = <R>( const invokeMapMethod = <R>(
runner: CallbackRunner<R>, runner: CallbackRunner<R>,
target: SandboxMap, target: CodeModeMap,
name: string, name: string,
args: Array<unknown>, args: Array<unknown>,
node: AstNode, node: AstNode,
@ -446,7 +446,7 @@ const invokeMapMethod = <R>(
const invokeSetMethod = <R>( const invokeSetMethod = <R>(
runner: CallbackRunner<R>, runner: CallbackRunner<R>,
target: SandboxSet, target: CodeModeSet,
name: string, name: string,
args: Array<unknown>, args: Array<unknown>,
node: AstNode, node: AstNode,
@ -485,7 +485,7 @@ const invokeSetMethod = <R>(
const invokeURLSearchParamsMethod = <R>( const invokeURLSearchParamsMethod = <R>(
runner: CallbackRunner<R>, runner: CallbackRunner<R>,
target: SandboxURLSearchParams, target: CodeModeURLSearchParams,
name: string, name: string,
args: Array<unknown>, args: Array<unknown>,
node: AstNode, node: AstNode,

View file

@ -1,5 +1,5 @@
import type { SafeObject } from "../tool-runtime.js" import type { SafeObject } from "../tool-runtime.js"
import type { SandboxPromise, SandboxURL } from "../values.js" import type { CodeModePromise, CodeModeURL } from "../values.js"
export type SourcePosition = { export type SourcePosition = {
line: number line: number
@ -35,7 +35,7 @@ export type StatementResult =
| { kind: "continue" } | { kind: "continue" }
export type MemberReference = { export type MemberReference = {
target: SafeObject | Array<unknown> | SandboxURL target: SafeObject | Array<unknown> | CodeModeURL
key: string | number key: string | number
} }
@ -71,7 +71,7 @@ export type PromiseInstanceMethodName = "then" | "catch" | "finally"
export class PromiseInstanceMethodReference { export class PromiseInstanceMethodReference {
constructor( constructor(
readonly promise: SandboxPromise, readonly promise: CodeModePromise,
readonly name: PromiseInstanceMethodName, readonly name: PromiseInstanceMethodName,
) {} ) {}
} }

View file

@ -17,24 +17,24 @@ import { applyCollectionCallback, type CallbackRunner } from "./methods.js"
import { typeofValue } from "./references.js" import { typeofValue } from "./references.js"
import { spreadItems } from "../stdlib/collections.js" import { spreadItems } from "../stdlib/collections.js"
import { createAggregateErrorValue } from "../stdlib/value.js" import { createAggregateErrorValue } from "../stdlib/value.js"
import { SandboxPromise } from "../values.js" import { CodeModePromise } from "../values.js"
// Observation only controls rejection reporting; program completion interrupts all promise work. // Observation only controls rejection reporting; program completion interrupts all promise work.
export class PromiseRuntime<R> { export class PromiseRuntime<R> {
private readonly active = new Set<SandboxPromise>() private readonly active = new Set<CodeModePromise>()
private readonly ids = new WeakMap<SandboxPromise, number>() private readonly ids = new WeakMap<CodeModePromise, number>()
private readonly observed = new WeakSet<SandboxPromise>() private readonly observed = new WeakSet<CodeModePromise>()
private readonly failures = new Map<number, Diagnostic>() private readonly failures = new Map<number, Diagnostic>()
private nextID = 0 private nextID = 0
constructor(private readonly scope: Scope.Scope) {} constructor(private readonly scope: Scope.Scope) {}
create(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<SandboxPromise, never, R> { create(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<CodeModePromise, never, R> {
return Effect.suspend(() => { return Effect.suspend(() => {
// Allocate before forking so reruns get distinct IDs and diagnostics retain creation order. // Allocate before forking so reruns get distinct IDs and diagnostics retain creation order.
const id = this.nextID++ const id = this.nextID++
return Effect.map(Effect.forkIn(effect, this.scope, { startImmediately: true }), (fiber) => { return Effect.map(Effect.forkIn(effect, this.scope, { startImmediately: true }), (fiber) => {
const promise = new SandboxPromise(fiber) const promise = new CodeModePromise(fiber)
this.active.add(promise) this.active.add(promise)
this.ids.set(promise, id) this.ids.set(promise, id)
fiber.addObserver((exit) => { fiber.addObserver((exit) => {
@ -55,14 +55,14 @@ export class PromiseRuntime<R> {
} }
// Observation must be recorded when responsibility transfers, before the consumer fiber runs. // Observation must be recorded when responsibility transfers, before the consumer fiber runs.
markObserved(promise: SandboxPromise): void { markObserved(promise: CodeModePromise): void {
this.observed.add(promise) this.observed.add(promise)
const id = this.ids.get(promise) const id = this.ids.get(promise)
this.ids.delete(promise) this.ids.delete(promise)
if (id !== undefined) this.failures.delete(id) if (id !== undefined) this.failures.delete(id)
} }
await(promise: SandboxPromise): Effect.Effect<Exit.Exit<unknown, unknown>> { await(promise: CodeModePromise): Effect.Effect<Exit.Exit<unknown, unknown>> {
return Fiber.await(promise.fiber) return Fiber.await(promise.fiber)
} }
@ -94,7 +94,7 @@ export const invokePromiseMethod = <R>(
): Effect.Effect<unknown, unknown, R> => { ): Effect.Effect<unknown, unknown, R> => {
if (ref.name === "resolve") { if (ref.name === "resolve") {
const value = args[0] const value = args[0]
return value instanceof SandboxPromise ? Effect.succeed(value) : promises.create(Effect.succeed(value)) return value instanceof CodeModePromise ? Effect.succeed(value) : promises.create(Effect.succeed(value))
} }
if (ref.name === "reject") { if (ref.name === "reject") {
return promises.create(Effect.fail(new ProgramThrow(args[0]))) return promises.create(Effect.fail(new ProgramThrow(args[0])))
@ -114,19 +114,19 @@ export const invokePromiseMethod = <R>(
const items = Array.from(spread) const items = Array.from(spread)
for (const item of items) { for (const item of items) {
if (item instanceof SandboxPromise) promises.markObserved(item) if (item instanceof CodeModePromise) promises.markObserved(item)
} }
switch (ref.name) { switch (ref.name) {
case "all": { case "all": {
const observations = items.map((item) => const observations = items.map((item) =>
item instanceof SandboxPromise ? Effect.flatten(promises.await(item)) : Effect.succeed(item), item instanceof CodeModePromise ? Effect.flatten(promises.await(item)) : Effect.succeed(item),
) )
return promises.create(settleAfterTurn(Effect.all(observations, { concurrency: "unbounded" }))) return promises.create(settleAfterTurn(Effect.all(observations, { concurrency: "unbounded" })))
} }
case "allSettled": { case "allSettled": {
const observations = items.map((item) => const observations = items.map((item) =>
item instanceof SandboxPromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)), item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)),
) )
return promises.create( return promises.create(
settleAfterTurn( settleAfterTurn(
@ -168,13 +168,13 @@ export const invokePromiseMethod = <R>(
) )
} }
const observations = items.map((item) => const observations = items.map((item) =>
item instanceof SandboxPromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)), item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)),
) )
return promises.create(settleAfterTurn(Effect.flatten(Effect.raceAll(observations)))) return promises.create(settleAfterTurn(Effect.flatten(Effect.raceAll(observations))))
} }
case "any": { case "any": {
const flipped = items.map((item) => const flipped = items.map((item) =>
item instanceof SandboxPromise item instanceof CodeModePromise
? Effect.flatMap(promises.await(item), (exit) => { ? Effect.flatMap(promises.await(item), (exit) => {
if (Exit.isSuccess(exit)) return Effect.fail(new PromiseAnyFulfilled(exit.value)) if (Exit.isSuccess(exit)) return Effect.fail(new PromiseAnyFulfilled(exit.value))
if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause) if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause)
@ -201,7 +201,7 @@ export const invokePromiseInstanceMethod = <R>(
ref: PromiseInstanceMethodReference, ref: PromiseInstanceMethodReference,
args: Array<unknown>, args: Array<unknown>,
node: AstNode, node: AstNode,
): Effect.Effect<SandboxPromise, never, R> => { ): Effect.Effect<CodeModePromise, never, R> => {
const method = `Promise.prototype.${ref.name}` const method = `Promise.prototype.${ref.name}`
promises.markObserved(ref.promise) promises.markObserved(ref.promise)
if (ref.name === "finally") { if (ref.name === "finally") {
@ -217,7 +217,7 @@ export const constructPromise = <R>(
promises: PromiseRuntime<R>, promises: PromiseRuntime<R>,
executor: unknown, executor: unknown,
node: AstNode, node: AstNode,
): Effect.Effect<SandboxPromise, unknown, R> => { ): Effect.Effect<CodeModePromise, unknown, R> => {
if (!(executor instanceof CodeModeFunction)) { if (!(executor instanceof CodeModeFunction)) {
throw new InterpreterRuntimeError( throw new InterpreterRuntimeError(
"new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).", "new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).",
@ -226,10 +226,10 @@ export const constructPromise = <R>(
} }
return Effect.gen(function* () { return Effect.gen(function* () {
const deferred = Deferred.makeUnsafe<unknown, unknown>() const deferred = Deferred.makeUnsafe<unknown, unknown>()
const box: { own?: SandboxPromise } = {} const box: { own?: CodeModePromise } = {}
const promise = yield* promises.create( const promise = yield* promises.create(
Effect.flatMap(Deferred.await(deferred), (value) => { Effect.flatMap(Deferred.await(deferred), (value) => {
if (!(value instanceof SandboxPromise)) return Effect.succeed(value) if (!(value instanceof CodeModePromise)) return Effect.succeed(value)
if (value === box.own) return Effect.fail(selfResolutionError(node)) if (value === box.own) return Effect.fail(selfResolutionError(node))
return runner.settlePromise(value) return runner.settlePromise(value)
}), }),
@ -281,7 +281,7 @@ const reactionHandler = (value: unknown, method: string, node: AstNode): Reactio
// Teardown bypasses handlers; settled reactions yield once so handlers never run inline. // Teardown bypasses handlers; settled reactions yield once so handlers never run inline.
const reactionExit = <R>( const reactionExit = <R>(
promises: PromiseRuntime<R>, promises: PromiseRuntime<R>,
source: SandboxPromise, source: CodeModePromise,
): Effect.Effect<Exit.Exit<unknown, unknown>, unknown, R> => ): Effect.Effect<Exit.Exit<unknown, unknown>, unknown, R> =>
Effect.gen(function* () { Effect.gen(function* () {
const exit = yield* promises.await(source) const exit = yield* promises.await(source)
@ -293,13 +293,13 @@ const reactionExit = <R>(
const chainReaction = <R>( const chainReaction = <R>(
runner: CallbackRunner<R>, runner: CallbackRunner<R>,
promises: PromiseRuntime<R>, promises: PromiseRuntime<R>,
source: SandboxPromise, source: CodeModePromise,
onFulfilled: ReactionHandler | undefined, onFulfilled: ReactionHandler | undefined,
onRejected: ReactionHandler | undefined, onRejected: ReactionHandler | undefined,
method: string, method: string,
node: AstNode, node: AstNode,
): Effect.Effect<SandboxPromise, never, R> => { ): Effect.Effect<CodeModePromise, never, R> => {
const box: { derived?: SandboxPromise } = {} const box: { derived?: CodeModePromise } = {}
const body = Effect.gen(function* () { const body = Effect.gen(function* () {
const exit = yield* reactionExit(promises, source) const exit = yield* reactionExit(promises, source)
const handler = Exit.isSuccess(exit) ? onFulfilled : onRejected const handler = Exit.isSuccess(exit) ? onFulfilled : onRejected
@ -307,7 +307,7 @@ const chainReaction = <R>(
const input = Exit.isSuccess(exit) ? exit.value : caughtErrorValue(Cause.squash(exit.cause)) const input = Exit.isSuccess(exit) ? exit.value : caughtErrorValue(Cause.squash(exit.cause))
const result = yield* applyCollectionCallback(runner, handler, method, node)([input]) const result = yield* applyCollectionCallback(runner, handler, method, node)([input])
if (result === box.derived) return yield* Effect.fail(selfResolutionError(node)) if (result === box.derived) return yield* Effect.fail(selfResolutionError(node))
if (result instanceof SandboxPromise) return yield* runner.settlePromise(result) if (result instanceof CodeModePromise) return yield* runner.settlePromise(result)
return result return result
}) })
return Effect.map(promises.create(body), (derived) => { return Effect.map(promises.create(body), (derived) => {
@ -319,17 +319,17 @@ const chainReaction = <R>(
const chainFinally = <R>( const chainFinally = <R>(
runner: CallbackRunner<R>, runner: CallbackRunner<R>,
promises: PromiseRuntime<R>, promises: PromiseRuntime<R>,
source: SandboxPromise, source: CodeModePromise,
cleanup: ReactionHandler | undefined, cleanup: ReactionHandler | undefined,
method: string, method: string,
node: AstNode, node: AstNode,
): Effect.Effect<SandboxPromise, never, R> => ): Effect.Effect<CodeModePromise, never, R> =>
promises.create( promises.create(
Effect.gen(function* () { Effect.gen(function* () {
const exit = yield* reactionExit(promises, source) const exit = yield* reactionExit(promises, source)
if (cleanup !== undefined) { if (cleanup !== undefined) {
const result = yield* applyCollectionCallback(runner, cleanup, method, node)([]) const result = yield* applyCollectionCallback(runner, cleanup, method, node)([])
if (result instanceof SandboxPromise) yield* runner.settlePromise(result) if (result instanceof CodeModePromise) yield* runner.settlePromise(result)
} }
return yield* exit return yield* exit
}), }),

View file

@ -15,7 +15,7 @@ import {
UriFunction, UriFunction,
} from "./model.js" } from "./model.js"
import { ToolReference } from "../tool-runtime.js" import { ToolReference } from "../tool-runtime.js"
import { isSandboxValue, SandboxPromise } from "../values.js" import { isCodeModeValue, CodeModePromise } from "../values.js"
export const isRuntimeReference = (value: unknown): boolean => export const isRuntimeReference = (value: unknown): boolean =>
value instanceof CodeModeFunction || value instanceof CodeModeFunction ||
@ -26,13 +26,13 @@ export const isRuntimeReference = (value: unknown): boolean =>
value instanceof PromiseNamespace || value instanceof PromiseNamespace ||
value instanceof PromiseMethodReference || value instanceof PromiseMethodReference ||
value instanceof PromiseInstanceMethodReference || value instanceof PromiseInstanceMethodReference ||
value instanceof SandboxPromise || value instanceof CodeModePromise ||
value instanceof CoercionFunction || value instanceof CoercionFunction ||
value instanceof UriFunction || value instanceof UriFunction ||
value instanceof SearchFunction || value instanceof SearchFunction ||
value instanceof PromiseCapabilityFunction || value instanceof PromiseCapabilityFunction ||
value instanceof ErrorConstructorReference || value instanceof ErrorConstructorReference ||
isSandboxValue(value) isCodeModeValue(value)
export const containsRuntimeReference = (value: unknown, seen = new Set<object>()): boolean => { export const containsRuntimeReference = (value: unknown, seen = new Set<object>()): boolean => {
if (isRuntimeReference(value)) return true if (isRuntimeReference(value)) return true
@ -46,9 +46,9 @@ export const containsRuntimeReference = (value: unknown, seen = new Set<object>(
return contains return contains
} }
// Sandbox values are data here, not opaque interpreter references. // CodeMode values are data here, not opaque interpreter references.
export const containsOpaqueReference = (value: unknown, seen = new Set<object>()): boolean => { export const containsOpaqueReference = (value: unknown, seen = new Set<object>()): boolean => {
if (isSandboxValue(value)) return false if (isCodeModeValue(value)) return false
if (isRuntimeReference(value)) return true if (isRuntimeReference(value)) return true
if (value === null || typeof value !== "object") return false if (value === null || typeof value !== "object") return false
if (seen.has(value)) return false if (seen.has(value)) return false

View file

@ -73,14 +73,14 @@ import {
valueConstructors, valueConstructors,
} from "../stdlib/value.js" } from "../stdlib/value.js"
import { import {
isSandboxValue, isCodeModeValue,
SandboxDate, CodeModeDate,
SandboxMap, CodeModeMap,
SandboxPromise, CodeModePromise,
SandboxRegExp, CodeModeRegExp,
SandboxSet, CodeModeSet,
SandboxURL, CodeModeURL,
SandboxURLSearchParams, CodeModeURLSearchParams,
} from "../values.js" } from "../values.js"
const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => { const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => {
@ -91,24 +91,24 @@ const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean =>
if (rhs instanceof GlobalNamespace) { if (rhs instanceof GlobalNamespace) {
switch (rhs.name) { switch (rhs.name) {
case "Date": case "Date":
return lhs instanceof SandboxDate return lhs instanceof CodeModeDate
case "RegExp": case "RegExp":
return lhs instanceof SandboxRegExp return lhs instanceof CodeModeRegExp
case "Map": case "Map":
return lhs instanceof SandboxMap return lhs instanceof CodeModeMap
case "Set": case "Set":
return lhs instanceof SandboxSet return lhs instanceof CodeModeSet
case "URL": case "URL":
return lhs instanceof SandboxURL return lhs instanceof CodeModeURL
case "URLSearchParams": case "URLSearchParams":
return lhs instanceof SandboxURLSearchParams return lhs instanceof CodeModeURLSearchParams
case "Array": case "Array":
return Array.isArray(lhs) return Array.isArray(lhs)
case "Object": case "Object":
return lhs !== null && (typeof lhs === "object" || typeofValue(lhs) === "function") return lhs !== null && (typeof lhs === "object" || typeofValue(lhs) === "function")
} }
} }
if (rhs instanceof PromiseNamespace) return lhs instanceof SandboxPromise if (rhs instanceof PromiseNamespace) return lhs instanceof CodeModePromise
if (rhs instanceof CoercionFunction && (rhs.name === "Number" || rhs.name === "String" || rhs.name === "Boolean")) { if (rhs instanceof CoercionFunction && (rhs.name === "Number" || rhs.name === "String" || rhs.name === "Boolean")) {
return false return false
} }
@ -226,7 +226,7 @@ export class Interpreter<R> {
} }
// The implicit async body adopts returned promises before copy-out. // The implicit async body adopts returned promises before copy-out.
if (value instanceof SandboxPromise) value = yield* self.settlePromise(value) if (value instanceof CodeModePromise) value = yield* self.settlePromise(value)
return value return value
}).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop())))
} }
@ -235,16 +235,16 @@ export class Interpreter<R> {
private createToolCallPromise( private createToolCallPromise(
path: ReadonlyArray<string>, path: ReadonlyArray<string>,
args: Array<unknown>, args: Array<unknown>,
): Effect.Effect<SandboxPromise, never, R> { ): Effect.Effect<CodeModePromise, never, R> {
return this.createPromise(Effect.suspend(() => this.invokeTool(path, args))) return this.createPromise(Effect.suspend(() => this.invokeTool(path, args)))
} }
private createPromise(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<SandboxPromise, never, R> { private createPromise(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<CodeModePromise, never, R> {
return this.promises.create(effect) return this.promises.create(effect)
} }
// Fiber exits make settlement idempotent; yielding prevents inline continuation. // Fiber exits make settlement idempotent; yielding prevents inline continuation.
private settlePromise(promise: SandboxPromise): Effect.Effect<unknown, unknown, never> { private settlePromise(promise: CodeModePromise): Effect.Effect<unknown, unknown, never> {
const promises = this.promises const promises = this.promises
return Effect.suspend(() => { return Effect.suspend(() => {
promises.markObserved(promise) promises.markObserved(promise)
@ -971,7 +971,7 @@ export class Interpreter<R> {
// Await always suspends, including for plain values. // Await always suspends, including for plain values.
const self = this const self = this
return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) => return Effect.flatMap(this.evaluateExpression(getNode(node, "argument")), (value) =>
value instanceof SandboxPromise ? self.settlePromise(value) : Effect.as(Effect.yieldNow, value), value instanceof CodeModePromise ? self.settlePromise(value) : Effect.as(Effect.yieldNow, value),
) )
} }
case "NewExpression": case "NewExpression":
@ -1019,23 +1019,23 @@ export class Interpreter<R> {
throw unsupportedSyntax("NewExpression", node) throw unsupportedSyntax("NewExpression", node)
} }
private constructDate(args: Array<unknown>): SandboxDate { private constructDate(args: Array<unknown>): CodeModeDate {
if (args.length === 0) return new SandboxDate(Date.now()) if (args.length === 0) return new CodeModeDate(Date.now())
if (args.length === 1) { if (args.length === 1) {
const arg = args[0] const arg = args[0]
if (arg instanceof SandboxDate) return new SandboxDate(arg.time) if (arg instanceof CodeModeDate) return new CodeModeDate(arg.time)
if (typeof arg === "number") return new SandboxDate(new Date(arg).getTime()) if (typeof arg === "number") return new CodeModeDate(new Date(arg).getTime())
if (typeof arg === "string") return new SandboxDate(Date.parse(arg)) if (typeof arg === "string") return new CodeModeDate(Date.parse(arg))
return new SandboxDate(Number.NaN) return new CodeModeDate(Number.NaN)
} }
const parts = args.map((arg) => coerceToNumber(arg)) const parts = args.map((arg) => coerceToNumber(arg))
return new SandboxDate(new Date(...(parts as [number, number])).getTime()) return new CodeModeDate(new Date(...(parts as [number, number])).getTime())
} }
private constructRegExp(args: Array<unknown>, node: AstNode): SandboxRegExp { private constructRegExp(args: Array<unknown>, node: AstNode): CodeModeRegExp {
const first = args[0] const first = args[0]
const pattern = const pattern =
first instanceof SandboxRegExp ? first.regex.source : first === undefined ? "" : coerceToString(first) first instanceof CodeModeRegExp ? first.regex.source : first === undefined ? "" : coerceToString(first)
const flagsArg = args[1] const flagsArg = args[1]
if (flagsArg !== undefined && typeof flagsArg !== "string") { if (flagsArg !== undefined && typeof flagsArg !== "string") {
throw new InterpreterRuntimeError( throw new InterpreterRuntimeError(
@ -1043,9 +1043,9 @@ export class Interpreter<R> {
node, node,
) )
} }
const flags = flagsArg ?? (first instanceof SandboxRegExp ? first.regex.flags : "") const flags = flagsArg ?? (first instanceof CodeModeRegExp ? first.regex.flags : "")
try { try {
return new SandboxRegExp(pattern, flags) return new CodeModeRegExp(pattern, flags)
} catch (error) { } catch (error) {
const reason = regexFailureReason(error) const reason = regexFailureReason(error)
throw new InterpreterRuntimeError( throw new InterpreterRuntimeError(
@ -1057,12 +1057,12 @@ export class Interpreter<R> {
} }
} }
private constructMap(init: unknown, node: AstNode): SandboxMap { private constructMap(init: unknown, node: AstNode): CodeModeMap {
const target = new SandboxMap() const target = new CodeModeMap()
if (init === undefined || init === null) return target if (init === undefined || init === null) return target
const entries = Array.isArray(init) const entries = Array.isArray(init)
? init ? init
: init instanceof SandboxMap : init instanceof CodeModeMap
? Array.from(init.map.entries(), ([key, item]): Array<unknown> => [key, item]) ? Array.from(init.map.entries(), ([key, item]): Array<unknown> => [key, item])
: undefined : undefined
if (entries === undefined) { if (entries === undefined) {
@ -1080,12 +1080,12 @@ export class Interpreter<R> {
return target return target
} }
private constructSet(init: unknown, node: AstNode): SandboxSet { private constructSet(init: unknown, node: AstNode): CodeModeSet {
const target = new SandboxSet() const target = new CodeModeSet()
if (init === undefined || init === null) return target if (init === undefined || init === null) return target
const items = Array.isArray(init) const items = Array.isArray(init)
? init ? init
: init instanceof SandboxSet : init instanceof CodeModeSet
? Array.from(init.set.values()) ? Array.from(init.set.values())
: typeof init === "string" : typeof init === "string"
? Array.from(init) ? Array.from(init)
@ -1097,7 +1097,7 @@ export class Interpreter<R> {
return target return target
} }
private constructURL(args: Array<unknown>, node: AstNode): SandboxURL { private constructURL(args: Array<unknown>, node: AstNode): CodeModeURL {
if (args.length === 0) { if (args.length === 0) {
throw new InterpreterRuntimeError("new URL(...) requires a URL string and an optional base URL.", node).as( throw new InterpreterRuntimeError("new URL(...) requires a URL string and an optional base URL.", node).as(
"TypeError", "TypeError",
@ -1106,7 +1106,7 @@ export class Interpreter<R> {
const input = urlArgument(args[0], "new URL input") const input = urlArgument(args[0], "new URL input")
const base = args[1] === undefined ? undefined : urlArgument(args[1], "new URL base") const base = args[1] === undefined ? undefined : urlArgument(args[1], "new URL base")
try { try {
return new SandboxURL(new URL(input, base)) return new CodeModeURL(new URL(input, base))
} catch { } catch {
throw new InterpreterRuntimeError( throw new InterpreterRuntimeError(
`new URL(...) received an invalid URL${base === undefined ? "" : " or base URL"}.`, `new URL(...) received an invalid URL${base === undefined ? "" : " or base URL"}.`,
@ -1115,16 +1115,16 @@ export class Interpreter<R> {
} }
} }
private constructURLSearchParams(init: unknown, node: AstNode): SandboxURLSearchParams { private constructURLSearchParams(init: unknown, node: AstNode): CodeModeURLSearchParams {
if (init === undefined) return new SandboxURLSearchParams(new URLSearchParams()) if (init === undefined) return new CodeModeURLSearchParams(new URLSearchParams())
if (init instanceof SandboxURLSearchParams) { if (init instanceof CodeModeURLSearchParams) {
return new SandboxURLSearchParams(new URLSearchParams(init.params)) return new CodeModeURLSearchParams(new URLSearchParams(init.params))
} }
if (typeof init === "string") return new SandboxURLSearchParams(new URLSearchParams(init)) if (typeof init === "string") return new CodeModeURLSearchParams(new URLSearchParams(init))
if (init === null || typeof init === "number" || typeof init === "boolean") { if (init === null || typeof init === "number" || typeof init === "boolean") {
return new SandboxURLSearchParams(new URLSearchParams(coerceToString(init))) return new CodeModeURLSearchParams(new URLSearchParams(coerceToString(init)))
} }
if (init instanceof SandboxMap) { if (init instanceof CodeModeMap) {
return this.constructURLSearchParams( return this.constructURLSearchParams(
Array.from(init.map.entries(), ([key, value]) => [key, value]), Array.from(init.map.entries(), ([key, value]) => [key, value]),
node, node,
@ -1143,9 +1143,9 @@ export class Interpreter<R> {
string, string,
] ]
}) })
return new SandboxURLSearchParams(new URLSearchParams(entries)) return new CodeModeURLSearchParams(new URLSearchParams(entries))
} }
if (isSandboxValue(init)) return new SandboxURLSearchParams(new URLSearchParams()) if (isCodeModeValue(init)) return new CodeModeURLSearchParams(new URLSearchParams())
const data = boundedData(init, "new URLSearchParams input") const data = boundedData(init, "new URLSearchParams input")
if (data === null || typeof data !== "object") { if (data === null || typeof data !== "object") {
throw new InterpreterRuntimeError( throw new InterpreterRuntimeError(
@ -1153,7 +1153,7 @@ export class Interpreter<R> {
node, node,
).as("TypeError") ).as("TypeError")
} }
return new SandboxURLSearchParams( return new CodeModeURLSearchParams(
new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))), new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))),
) )
} }
@ -1176,7 +1176,7 @@ export class Interpreter<R> {
// Null-prototype data needs explicit primitive coercion; identity and `in` retain raw objects. // Null-prototype data needs explicit primitive coercion; identity and `in` retain raw objects.
// Dates use string coercion for `+` and epoch time elsewhere. // Dates use string coercion for `+` and epoch time elsewhere.
const coerceOperand = (operand: unknown): unknown => { const coerceOperand = (operand: unknown): unknown => {
if (operand instanceof SandboxDate) return operator === "+" ? coerceToString(operand) : operand.time if (operand instanceof CodeModeDate) return operator === "+" ? coerceToString(operand) : operand.time
return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand return operand !== null && typeof operand === "object" ? coerceToString(operand) : operand
} }
const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object" const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object"
@ -1261,7 +1261,7 @@ export class Interpreter<R> {
throw new InterpreterRuntimeError("Unary operators require data values in CodeMode.", node, "InvalidDataValue") throw new InterpreterRuntimeError("Unary operators require data values in CodeMode.", node, "InvalidDataValue")
} }
const operand = const operand =
value instanceof SandboxDate value instanceof CodeModeDate
? value.time ? value.time
: value !== null && typeof value === "object" : value !== null && typeof value === "object"
? coerceToString(value) ? coerceToString(value)
@ -1520,11 +1520,11 @@ export class Interpreter<R> {
}) })
if (!fn.async) return run if (!fn.async) return run
// The initial yield assigns `box.own` before the body can self-resolve. // The initial yield assigns `box.own` before the body can self-resolve.
const box: { own?: SandboxPromise } = {} const box: { own?: CodeModePromise } = {}
return Effect.map( return Effect.map(
this.createPromise( this.createPromise(
Effect.flatMap(run, (value) => { Effect.flatMap(run, (value) => {
if (!(value instanceof SandboxPromise)) return Effect.succeed(value) if (!(value instanceof CodeModePromise)) return Effect.succeed(value)
if (value === box.own) return Effect.fail(selfResolutionError()) if (value === box.own) return Effect.fail(selfResolutionError())
return invocation.settlePromise(value) return invocation.settlePromise(value)
}), }),
@ -1546,7 +1546,7 @@ export class Interpreter<R> {
if (property.type === "SpreadElement") { if (property.type === "SpreadElement") {
const spread = yield* self.evaluateExpression(getNode(property, "argument")) const spread = yield* self.evaluateExpression(getNode(property, "argument"))
if (spread === null || spread === undefined || isSandboxValue(spread)) continue if (spread === null || spread === undefined || isCodeModeValue(spread)) continue
if (typeof spread !== "object" || Array.isArray(spread) || isRuntimeReference(spread)) { if (typeof spread !== "object" || Array.isArray(spread) || isRuntimeReference(spread)) {
throw new InterpreterRuntimeError( throw new InterpreterRuntimeError(
"Object spread requires a data object in CodeMode.", "Object spread requires a data object in CodeMode.",
@ -1748,28 +1748,28 @@ export class Interpreter<R> {
if (objectValue.name === "String" && stringStatics.has(key)) return new GlobalMethodReference("String", key) if (objectValue.name === "String" && stringStatics.has(key)) return new GlobalMethodReference("String", key)
} }
if (objectValue instanceof SandboxDate) { if (objectValue instanceof CodeModeDate) {
if (typeof key === "string" && dateMethods.has(key)) return new IntrinsicReference(objectValue, key) if (typeof key === "string" && dateMethods.has(key)) return new IntrinsicReference(objectValue, key)
return new ComputedValue(undefined) return new ComputedValue(undefined)
} }
if (objectValue instanceof SandboxRegExp) { if (objectValue instanceof CodeModeRegExp) {
if (typeof key === "string" && regexpProperties.has(key)) { if (typeof key === "string" && regexpProperties.has(key)) {
return new ComputedValue((objectValue.regex as unknown as Record<string, unknown>)[key]) return new ComputedValue((objectValue.regex as unknown as Record<string, unknown>)[key])
} }
if (typeof key === "string" && regexpMethods.has(key)) return new IntrinsicReference(objectValue, key) if (typeof key === "string" && regexpMethods.has(key)) return new IntrinsicReference(objectValue, key)
return new ComputedValue(undefined) return new ComputedValue(undefined)
} }
if (objectValue instanceof SandboxMap) { if (objectValue instanceof CodeModeMap) {
if (key === "size") return new ComputedValue(objectValue.map.size) if (key === "size") return new ComputedValue(objectValue.map.size)
if (typeof key === "string" && mapMethods.has(key)) return new IntrinsicReference(objectValue, key) if (typeof key === "string" && mapMethods.has(key)) return new IntrinsicReference(objectValue, key)
return new ComputedValue(undefined) return new ComputedValue(undefined)
} }
if (objectValue instanceof SandboxSet) { if (objectValue instanceof CodeModeSet) {
if (key === "size") return new ComputedValue(objectValue.set.size) if (key === "size") return new ComputedValue(objectValue.set.size)
if (typeof key === "string" && setMethods.has(key)) return new IntrinsicReference(objectValue, key) if (typeof key === "string" && setMethods.has(key)) return new IntrinsicReference(objectValue, key)
return new ComputedValue(undefined) return new ComputedValue(undefined)
} }
if (objectValue instanceof SandboxURL) { if (objectValue instanceof CodeModeURL) {
if (key === "searchParams") { if (key === "searchParams") {
return new ComputedValue(objectValue.searchParams) return new ComputedValue(objectValue.searchParams)
} }
@ -1777,7 +1777,7 @@ export class Interpreter<R> {
if (typeof key === "string" && urlProperties.has(key)) return { target: objectValue, key } if (typeof key === "string" && urlProperties.has(key)) return { target: objectValue, key }
return new ComputedValue(undefined) return new ComputedValue(undefined)
} }
if (objectValue instanceof SandboxURLSearchParams) { if (objectValue instanceof CodeModeURLSearchParams) {
if (key === "size") return new ComputedValue(objectValue.params.size) if (key === "size") return new ComputedValue(objectValue.params.size)
if (typeof key === "string" && urlSearchParamsMethods.has(key)) { if (typeof key === "string" && urlSearchParamsMethods.has(key)) {
return new IntrinsicReference(objectValue, key) return new IntrinsicReference(objectValue, key)
@ -1786,7 +1786,7 @@ export class Interpreter<R> {
} }
// Reject unknown promise properties so a missing await cannot hide. // Reject unknown promise properties so a missing await cannot hide.
if (objectValue instanceof SandboxPromise) { if (objectValue instanceof CodeModePromise) {
if (key === "then" || key === "catch" || key === "finally") { if (key === "then" || key === "catch" || key === "finally") {
return new PromiseInstanceMethodReference(objectValue, key) return new PromiseInstanceMethodReference(objectValue, key)
} }
@ -1851,7 +1851,7 @@ export class Interpreter<R> {
} }
return reference.key === "length" ? reference.target.length : reference.target[Number(reference.key)] return reference.key === "length" ? reference.target.length : reference.target[Number(reference.key)]
} }
if (reference.target instanceof SandboxURL) { if (reference.target instanceof CodeModeURL) {
return (reference.target.url as unknown as Record<string, unknown>)[String(reference.key)] return (reference.target.url as unknown as Record<string, unknown>)[String(reference.key)]
} }
return reference.target[String(reference.key)] return reference.target[String(reference.key)]
@ -1891,7 +1891,7 @@ export class Interpreter<R> {
} }
const key = Array.isArray(reference.target) ? Number(reference.key) : String(reference.key) const key = Array.isArray(reference.target) ? Number(reference.key) : String(reference.key)
const current = const current =
reference.target instanceof SandboxURL reference.target instanceof CodeModeURL
? (reference.target.url as unknown as Record<string, unknown>)[key] ? (reference.target.url as unknown as Record<string, unknown>)[key]
: (reference.target as Record<PropertyKey, unknown>)[key] : (reference.target as Record<PropertyKey, unknown>)[key]
const { write, next, result } = yield* compute(current) const { write, next, result } = yield* compute(current)
@ -1915,7 +1915,7 @@ export class Interpreter<R> {
target[index] = next target[index] = next
return return
} }
if (reference.target instanceof SandboxURL) { if (reference.target instanceof CodeModeURL) {
const property = key as string const property = key as string
if (!urlWritableProperties.has(property)) { if (!urlWritableProperties.has(property)) {
throw new InterpreterRuntimeError(`URL.${property} is read-only.`, node).as("TypeError") throw new InterpreterRuntimeError(`URL.${property} is read-only.`, node).as("TypeError")

View file

@ -43,9 +43,9 @@ export const setMethods = new Set(["add", "has", "delete", "clear", "forEach", "
export const spreadItems = (value: unknown): Array<unknown> | undefined => { export const spreadItems = (value: unknown): Array<unknown> | undefined => {
if (Array.isArray(value)) return value if (Array.isArray(value)) return value
if (typeof value === "string") return Array.from(value) if (typeof value === "string") return Array.from(value)
if (value instanceof SandboxMap) return Array.from(value.map.entries(), ([key, item]) => [key, item]) if (value instanceof CodeModeMap) return Array.from(value.map.entries(), ([key, item]) => [key, item])
if (value instanceof SandboxSet) return Array.from(value.set.values()) if (value instanceof CodeModeSet) return Array.from(value.set.values())
if (value instanceof SandboxURLSearchParams) return Array.from(value.params.entries(), ([key, item]) => [key, item]) if (value instanceof CodeModeURLSearchParams) return Array.from(value.params.entries(), ([key, item]) => [key, item])
return undefined return undefined
} }
import { SandboxMap, SandboxSet, SandboxURLSearchParams } from "../values.js" import { CodeModeMap, CodeModeSet, CodeModeURLSearchParams } from "../values.js"

View file

@ -1,14 +1,14 @@
import { containsOpaqueReference, containsRuntimeReference, isRuntimeReference } from "../interpreter/references.js" import { containsOpaqueReference, containsRuntimeReference, isRuntimeReference } from "../interpreter/references.js"
import { copyIn, copyOut } from "../tool-runtime.js" import { copyIn, copyOut } from "../tool-runtime.js"
import { import {
isSandboxValue, isCodeModeValue,
SandboxDate, CodeModeDate,
SandboxMap, CodeModeMap,
SandboxPromise, CodeModePromise,
SandboxRegExp, CodeModeRegExp,
SandboxSet, CodeModeSet,
SandboxURL, CodeModeURL,
SandboxURLSearchParams, CodeModeURLSearchParams,
} from "../values.js" } from "../values.js"
import { boundedData, coerceToString } from "./value.js" import { boundedData, coerceToString } from "./value.js"
@ -34,14 +34,14 @@ const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): s
if (typeof value === "string") return JSON.stringify(value) if (typeof value === "string") return JSON.stringify(value)
if (typeof value === "number" || typeof value === "boolean") return String(value) if (typeof value === "number" || typeof value === "boolean") return String(value)
if (typeof value !== "object") return String(value) if (typeof value !== "object") return String(value)
if (value instanceof SandboxPromise) return "[Promise (await it to get its value)]" if (value instanceof CodeModePromise) return "[Promise (await it to get its value)]"
if (value instanceof SandboxDate) return coerceToString(value) if (value instanceof CodeModeDate) return coerceToString(value)
if (value instanceof SandboxRegExp) return coerceToString(value) if (value instanceof CodeModeRegExp) return coerceToString(value)
if (value instanceof SandboxURL) return coerceToString(value) if (value instanceof CodeModeURL) return coerceToString(value)
if (value instanceof SandboxURLSearchParams) return coerceToString(value) if (value instanceof CodeModeURLSearchParams) return coerceToString(value)
if (depth > MAX_CONSOLE_DEPTH) return "..." if (depth > MAX_CONSOLE_DEPTH) return "..."
if (seen.has(value)) return "[Circular]" if (seen.has(value)) return "[Circular]"
if (value instanceof SandboxMap) { if (value instanceof CodeModeMap) {
seen.add(value) seen.add(value)
try { try {
const entries = Array.from(value.map.entries(), ([key, item]): Array<unknown> => [key, item]) const entries = Array.from(value.map.entries(), ([key, item]): Array<unknown> => [key, item])
@ -50,7 +50,7 @@ const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): s
seen.delete(value) seen.delete(value)
} }
} }
if (value instanceof SandboxSet) { if (value instanceof CodeModeSet) {
seen.add(value) seen.add(value)
try { try {
return `Set(${value.set.size}) ${formatConsoleValue(Array.from(value.set.values()), seen, depth + 1)}` return `Set(${value.set.size}) ${formatConsoleValue(Array.from(value.set.values()), seen, depth + 1)}`
@ -100,14 +100,14 @@ const consoleTableRows = (
if (Array.isArray(data)) { if (Array.isArray(data)) {
return data.map((item, index) => ({ index: String(index), values: consoleTableValues(item, columns) })) return data.map((item, index) => ({ index: String(index), values: consoleTableValues(item, columns) }))
} }
if (data !== null && typeof data === "object" && !isSandboxValue(data)) { if (data !== null && typeof data === "object" && !isCodeModeValue(data)) {
return Object.entries(data).map(([index, item]) => ({ index, values: consoleTableValues(item, columns) })) return Object.entries(data).map(([index, item]) => ({ index, values: consoleTableValues(item, columns) }))
} }
return [{ index: "0", values: { Value: data } }] return [{ index: "0", values: { Value: data } }]
} }
const consoleTableValues = (value: unknown, columns: ReadonlyArray<string> | undefined): Record<string, unknown> => { const consoleTableValues = (value: unknown, columns: ReadonlyArray<string> | undefined): Record<string, unknown> => {
if (value !== null && typeof value === "object" && !Array.isArray(value) && !isSandboxValue(value)) { if (value !== null && typeof value === "object" && !Array.isArray(value) && !isCodeModeValue(value)) {
const source = value as Record<string, unknown> const source = value as Record<string, unknown>
if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, source[column]])) if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, source[column]]))
return Object.fromEntries(Object.entries(source)) return Object.fromEntries(Object.entries(source))

View file

@ -36,7 +36,7 @@ export const invokeDateStatic = (name: string, args: Array<unknown>, node: AstNo
} }
} }
export const invokeDateMethod = (value: SandboxDate, name: string, node: AstNode): unknown => { export const invokeDateMethod = (value: CodeModeDate, name: string, node: AstNode): unknown => {
const hosted = new Date(value.time) const hosted = new Date(value.time)
switch (name) { switch (name) {
case "getTime": case "getTime":
@ -88,5 +88,5 @@ export const invokeDateMethod = (value: SandboxDate, name: string, node: AstNode
} }
} }
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { SandboxDate } from "../values.js" import { CodeModeDate } from "../values.js"
import { coerceToNumber, coerceToString } from "./value.js" import { coerceToNumber, coerceToString } from "./value.js"

View file

@ -1,6 +1,6 @@
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { isBlockedMember } from "../tool-runtime.js" import { isBlockedMember } from "../tool-runtime.js"
import { isSandboxValue, SandboxMap, SandboxPromise, SandboxSet, SandboxURLSearchParams } from "../values.js" import { isCodeModeValue, CodeModeMap, CodeModePromise, CodeModeSet, CodeModeURLSearchParams } from "../values.js"
import { boundedData, coerceToString } from "./value.js" import { boundedData, coerceToString } from "./value.js"
export const objectMethodsPreservingIdentity = new Set(["assign", "values", "entries", "fromEntries"]) export const objectMethodsPreservingIdentity = new Set(["assign", "values", "entries", "fromEntries"])
@ -9,8 +9,8 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
const requireObject = (): Record<string, unknown> => { const requireObject = (): Record<string, unknown> => {
const input = args[0] const input = args[0]
if (Array.isArray(input)) return input as unknown as Record<string, unknown> if (Array.isArray(input)) return input as unknown as Record<string, unknown>
if (isSandboxValue(input)) return {} if (isCodeModeValue(input)) return {}
if (input instanceof SandboxPromise) { if (input instanceof CodeModePromise) {
throw new InterpreterRuntimeError( throw new InterpreterRuntimeError(
`Object.${name} received an un-awaited Promise; await it before inspecting the result.`, `Object.${name} received an un-awaited Promise; await it before inspecting the result.`,
node, node,
@ -46,12 +46,12 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
return Object.hasOwn(requireObject(), String(args[1])) return Object.hasOwn(requireObject(), String(args[1]))
case "assign": { case "assign": {
const target = args[0] const target = args[0]
if (target === null || typeof target !== "object" || Array.isArray(target) || isSandboxValue(target)) { if (target === null || typeof target !== "object" || Array.isArray(target) || isCodeModeValue(target)) {
throw new InterpreterRuntimeError("Object.assign expects a data object target.", node) throw new InterpreterRuntimeError("Object.assign expects a data object target.", node)
} }
const out = target as Record<string, unknown> const out = target as Record<string, unknown>
for (const source of args.slice(1)) { for (const source of args.slice(1)) {
if (source === null || source === undefined || isSandboxValue(source)) continue if (source === null || source === undefined || isCodeModeValue(source)) continue
if (typeof source !== "object" || Array.isArray(source)) { if (typeof source !== "object" || Array.isArray(source)) {
throw new InterpreterRuntimeError("Object.assign expects data objects.", node) throw new InterpreterRuntimeError("Object.assign expects data objects.", node)
} }
@ -60,17 +60,17 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
return out return out
} }
case "fromEntries": { case "fromEntries": {
if (args[0] instanceof SandboxMap) { if (args[0] instanceof CodeModeMap) {
const out: Record<string, unknown> = Object.create(null) const out: Record<string, unknown> = Object.create(null)
for (const [key, item] of args[0].map.entries()) addEntry(out, key, item) for (const [key, item] of args[0].map.entries()) addEntry(out, key, item)
return out return out
} }
if (args[0] instanceof SandboxURLSearchParams) { if (args[0] instanceof CodeModeURLSearchParams) {
const out: Record<string, unknown> = Object.create(null) const out: Record<string, unknown> = Object.create(null)
for (const [key, value] of args[0].params.entries()) guardedSet(out, key, value) for (const [key, value] of args[0].params.entries()) guardedSet(out, key, value)
return out return out
} }
const pairs = args[0] instanceof SandboxSet ? Array.from(args[0].set.values()) : args[0] const pairs = args[0] instanceof CodeModeSet ? Array.from(args[0].set.values()) : args[0]
if (!Array.isArray(pairs)) { if (!Array.isArray(pairs)) {
boundedData(args[0], "Object.fromEntries input") boundedData(args[0], "Object.fromEntries input")
throw new InterpreterRuntimeError("Object.fromEntries expects an array of [key, value] pairs.", node) throw new InterpreterRuntimeError("Object.fromEntries expects an array of [key, value] pairs.", node)
@ -78,7 +78,7 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
const out: Record<string, unknown> = Object.create(null) const out: Record<string, unknown> = Object.create(null)
for (const pair of pairs) { for (const pair of pairs) {
const validated = boundedData(pair, "Object.fromEntries entry") const validated = boundedData(pair, "Object.fromEntries entry")
if (validated === null || typeof validated !== "object" || isSandboxValue(validated)) if (validated === null || typeof validated !== "object" || isCodeModeValue(validated))
throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node) throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node)
const entry = pair as Record<string, unknown> const entry = pair as Record<string, unknown>
addEntry(out, entry[0], entry[1]) addEntry(out, entry[0], entry[1])

View file

@ -19,7 +19,7 @@ export const escapeRegexHint =
'To match special characters like ( ) [ ] { } + * ? . literally, escape them with a backslash (e.g. "\\\\(") or test for them with String.includes instead.' 'To match special characters like ( ) [ ] { } + * ? . literally, escape them with a backslash (e.g. "\\\\(") or test for them with String.includes instead.'
export const toHostRegex = (arg: unknown, method: string, node: AstNode, extraFlags = ""): RegExp => { export const toHostRegex = (arg: unknown, method: string, node: AstNode, extraFlags = ""): RegExp => {
if (arg instanceof SandboxRegExp) return arg.regex if (arg instanceof CodeModeRegExp) return arg.regex
if (typeof arg === "string") { if (typeof arg === "string") {
try { try {
return new RegExp(arg, extraFlags) return new RegExp(arg, extraFlags)
@ -50,7 +50,7 @@ export const matchToValue = (match: RegExpMatchArray): Array<unknown> => {
} }
export const invokeRegExpMethod = ( export const invokeRegExpMethod = (
value: SandboxRegExp, value: CodeModeRegExp,
name: string, name: string,
args: Array<unknown>, args: Array<unknown>,
node: AstNode, node: AstNode,
@ -70,5 +70,5 @@ export const invokeRegExpMethod = (
} }
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { isBlockedMember, type SafeObject } from "../tool-runtime.js" import { isBlockedMember, type SafeObject } from "../tool-runtime.js"
import { SandboxRegExp } from "../values.js" import { CodeModeRegExp } from "../values.js"
import { coerceToString } from "./value.js" import { coerceToString } from "./value.js"

View file

@ -66,7 +66,7 @@ export const invokeUriFunction = (ref: UriFunction, args: Array<unknown>, node:
} }
export const urlArgument = (value: unknown, label: string): string => export const urlArgument = (value: unknown, label: string): string =>
value instanceof SandboxURL ? value.url.href : uriArgument(value, label) value instanceof CodeModeURL ? value.url.href : uriArgument(value, label)
export const invokeURLStatic = (name: string, args: Array<unknown>, node: AstNode): unknown => { export const invokeURLStatic = (name: string, args: Array<unknown>, node: AstNode): unknown => {
if (!urlStatics.has(name)) throw new InterpreterRuntimeError(`URL.${name} is not available in CodeMode.`, node) if (!urlStatics.has(name)) throw new InterpreterRuntimeError(`URL.${name} is not available in CodeMode.`, node)
@ -75,16 +75,16 @@ export const invokeURLStatic = (name: string, args: Array<unknown>, node: AstNod
const base = args[1] === undefined ? undefined : urlArgument(args[1], `URL.${name} base`) const base = args[1] === undefined ? undefined : urlArgument(args[1], `URL.${name} base`)
try { try {
const url = new URL(input, base) const url = new URL(input, base)
return name === "canParse" ? true : new SandboxURL(url) return name === "canParse" ? true : new CodeModeURL(url)
} catch { } catch {
return name === "canParse" ? false : null return name === "canParse" ? false : null
} }
} }
export const invokeURLMethod = (value: SandboxURL, name: string, node: AstNode): string => { export const invokeURLMethod = (value: CodeModeURL, name: string, node: AstNode): string => {
if (name === "toString" || name === "toJSON") return value.url.href if (name === "toString" || name === "toJSON") return value.url.href
throw new InterpreterRuntimeError(`URL method '${name}' is not available in CodeMode.`, node) throw new InterpreterRuntimeError(`URL method '${name}' is not available in CodeMode.`, node)
} }
import { type AstNode, InterpreterRuntimeError, UriFunction } from "../interpreter/model.js" import { type AstNode, InterpreterRuntimeError, UriFunction } from "../interpreter/model.js"
import { SandboxURL } from "../values.js" import { CodeModeURL } from "../values.js"
import { boundedData, coerceToString } from "./value.js" import { boundedData, coerceToString } from "./value.js"

View file

@ -34,13 +34,13 @@ export const boundedData = (value: unknown, label: string): unknown => copyIn(va
export const coerceToString = (value: unknown): string => { export const coerceToString = (value: unknown): string => {
if (value === null) return "null" if (value === null) return "null"
if (value === undefined) return "undefined" if (value === undefined) return "undefined"
if (value instanceof SandboxDate) if (value instanceof CodeModeDate)
return Number.isFinite(value.time) ? new Date(value.time).toISOString() : "Invalid Date" return Number.isFinite(value.time) ? new Date(value.time).toISOString() : "Invalid Date"
if (value instanceof SandboxRegExp) return `/${value.regex.source}/${value.regex.flags}` if (value instanceof CodeModeRegExp) return `/${value.regex.source}/${value.regex.flags}`
if (value instanceof SandboxMap) return "[object Map]" if (value instanceof CodeModeMap) return "[object Map]"
if (value instanceof SandboxSet) return "[object Set]" if (value instanceof CodeModeSet) return "[object Set]"
if (value instanceof SandboxURL) return value.url.href if (value instanceof CodeModeURL) return value.url.href
if (value instanceof SandboxURLSearchParams) return value.params.toString() if (value instanceof CodeModeURLSearchParams) return value.params.toString()
if (typeof value === "object") { if (typeof value === "object") {
return Array.isArray(value) return Array.isArray(value)
? value.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",") ? value.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",")
@ -50,14 +50,14 @@ export const coerceToString = (value: unknown): string => {
} }
export const coerceToNumber = (value: unknown): number => { export const coerceToNumber = (value: unknown): number => {
if (value instanceof SandboxDate) return value.time if (value instanceof CodeModeDate) return value.time
if (isSandboxValue(value)) return Number.NaN if (isCodeModeValue(value)) return Number.NaN
return value !== null && typeof value === "object" && !Array.isArray(value) ? Number.NaN : Number(value) return value !== null && typeof value === "object" && !Array.isArray(value) ? Number.NaN : Number(value)
} }
export const invokeCoercion = (ref: CoercionFunction, args: Array<unknown>, node: AstNode): unknown => { export const invokeCoercion = (ref: CoercionFunction, args: Array<unknown>, node: AstNode): unknown => {
const raw = args[0] const raw = args[0]
if (isSandboxValue(raw)) { if (isCodeModeValue(raw)) {
if (ref.name === "Boolean") return true if (ref.name === "Boolean") return true
if (ref.name === "Number") return coerceToNumber(raw) if (ref.name === "Number") return coerceToNumber(raw)
if (ref.name === "String") return coerceToString(raw) if (ref.name === "String") return coerceToString(raw)
@ -80,11 +80,11 @@ export const invokeCoercion = (ref: CoercionFunction, args: Array<unknown>, node
import { type AstNode, CoercionFunction, InterpreterRuntimeError } from "../interpreter/model.js" import { type AstNode, CoercionFunction, InterpreterRuntimeError } from "../interpreter/model.js"
import { copyIn, type SafeObject } from "../tool-runtime.js" import { copyIn, type SafeObject } from "../tool-runtime.js"
import { import {
isSandboxValue, isCodeModeValue,
SandboxDate, CodeModeDate,
SandboxMap, CodeModeMap,
SandboxRegExp, CodeModeRegExp,
SandboxSet, CodeModeSet,
SandboxURL, CodeModeURL,
SandboxURLSearchParams, CodeModeURLSearchParams,
} from "../values.js" } from "../values.js"

View file

@ -10,13 +10,13 @@ import {
} from "./tool-schema.js" } from "./tool-schema.js"
import { isDefinition as isToolDefinition, type Definition } from "./tool.js" import { isDefinition as isToolDefinition, type Definition } from "./tool.js"
import { import {
SandboxDate, CodeModeDate,
SandboxMap, CodeModeMap,
SandboxPromise, CodeModePromise,
SandboxRegExp, CodeModeRegExp,
SandboxSet, CodeModeSet,
SandboxURL, CodeModeURL,
SandboxURLSearchParams, CodeModeURLSearchParams,
} from "./values.js" } from "./values.js"
const estimateTokens = (input: string) => Math.max(0, Math.round(input.length / 4)) const estimateTokens = (input: string) => Math.max(0, Math.round(input.length / 4))
@ -141,16 +141,16 @@ const blockedMemberNames = new Set(["__proto__", "constructor", "prototype"])
export const isBlockedMember = (name: string): boolean => blockedMemberNames.has(name) export const isBlockedMember = (name: string): boolean => blockedMemberNames.has(name)
// Checkpoint mode preserves sandbox values; boundary mode JSON-normalizes them. // Checkpoint mode preserves CodeMode values; boundary mode JSON-normalizes them.
export const copyIn = (value: unknown, label: string, preserveSandboxValues = false): unknown => export const copyIn = (value: unknown, label: string, preserveCodeModeValues = false): unknown =>
copyBounded(value, label, 0, new Set(), preserveSandboxValues) copyBounded(value, label, 0, new Set(), preserveCodeModeValues)
const copyBounded = ( const copyBounded = (
value: unknown, value: unknown,
label: string, label: string,
depth: number, depth: number,
seen: Set<object>, seen: Set<object>,
preserveSandboxValues: boolean, preserveCodeModeValues: boolean,
): unknown => { ): unknown => {
if (depth > MAX_VALUE_DEPTH) { if (depth > MAX_VALUE_DEPTH) {
throw new ToolRuntimeError("InvalidDataValue", `${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`) throw new ToolRuntimeError("InvalidDataValue", `${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`)
@ -169,55 +169,55 @@ const copyBounded = (
throw new ToolRuntimeError("InvalidDataValue", `${label} must contain data only.`) throw new ToolRuntimeError("InvalidDataValue", `${label} must contain data only.`)
} }
if (value instanceof SandboxPromise) { if (value instanceof CodeModePromise) {
throw new ToolRuntimeError( throw new ToolRuntimeError(
"InvalidDataValue", "InvalidDataValue",
`${label} contains an un-awaited Promise; await tool calls (e.g. \`const result = await tools.ns.tool(...)\`) before using their results.`, `${label} contains an un-awaited Promise; await tool calls (e.g. \`const result = await tools.ns.tool(...)\`) before using their results.`,
) )
} }
if (preserveSandboxValues) { if (preserveCodeModeValues) {
if ( if (
value instanceof SandboxDate || value instanceof CodeModeDate ||
value instanceof SandboxRegExp || value instanceof CodeModeRegExp ||
value instanceof SandboxMap || value instanceof CodeModeMap ||
value instanceof SandboxSet || value instanceof CodeModeSet ||
value instanceof SandboxURL || value instanceof CodeModeURL ||
value instanceof SandboxURLSearchParams value instanceof CodeModeURLSearchParams
) { ) {
return value return value
} }
if (value instanceof Date) return new SandboxDate(value.getTime()) if (value instanceof Date) return new CodeModeDate(value.getTime())
if (value instanceof RegExp) return new SandboxRegExp(value.source, value.flags) if (value instanceof RegExp) return new CodeModeRegExp(value.source, value.flags)
if (value instanceof Map) { if (value instanceof Map) {
const wrapped = new SandboxMap() const wrapped = new CodeModeMap()
for (const [key, item] of value.entries()) { for (const [key, item] of value.entries()) {
wrapped.map.set(copyBounded(key, label, depth + 1, seen, true), copyBounded(item, label, depth + 1, seen, true)) wrapped.map.set(copyBounded(key, label, depth + 1, seen, true), copyBounded(item, label, depth + 1, seen, true))
} }
return wrapped return wrapped
} }
if (value instanceof Set) { if (value instanceof Set) {
const wrapped = new SandboxSet() const wrapped = new CodeModeSet()
for (const item of value.values()) wrapped.set.add(copyBounded(item, label, depth + 1, seen, true)) for (const item of value.values()) wrapped.set.add(copyBounded(item, label, depth + 1, seen, true))
return wrapped return wrapped
} }
if (value instanceof URL) return new SandboxURL(new URL(value.href)) if (value instanceof URL) return new CodeModeURL(new URL(value.href))
if (value instanceof URLSearchParams) return new SandboxURLSearchParams(new URLSearchParams(value)) if (value instanceof URLSearchParams) return new CodeModeURLSearchParams(new URLSearchParams(value))
} }
if (value instanceof SandboxDate) { if (value instanceof CodeModeDate) {
return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null
} }
if (value instanceof Date) { if (value instanceof Date) {
return Number.isFinite(value.getTime()) ? value.toISOString() : null return Number.isFinite(value.getTime()) ? value.toISOString() : null
} }
if (value instanceof SandboxURL) return value.url.href if (value instanceof CodeModeURL) return value.url.href
if (value instanceof URL) return value.href if (value instanceof URL) return value.href
if ( if (
value instanceof SandboxRegExp || value instanceof CodeModeRegExp ||
value instanceof SandboxMap || value instanceof CodeModeMap ||
value instanceof SandboxSet || value instanceof CodeModeSet ||
value instanceof SandboxURLSearchParams || value instanceof CodeModeURLSearchParams ||
value instanceof RegExp || value instanceof RegExp ||
value instanceof Map || value instanceof Map ||
value instanceof Set || value instanceof Set ||
@ -233,8 +233,8 @@ const copyBounded = (
seen.add(value) seen.add(value)
if (Array.isArray(value)) { if (Array.isArray(value)) {
const copied = value.map((item) => copyBounded(item, label, depth + 1, seen, preserveSandboxValues)) const copied = value.map((item) => copyBounded(item, label, depth + 1, seen, preserveCodeModeValues))
if (preserveSandboxValues) { if (preserveCodeModeValues) {
// Checkpoint copies retain array metadata that boundary copies omit. // Checkpoint copies retain array metadata that boundary copies omit.
for (const [key, item] of Object.entries(value)) { for (const [key, item] of Object.entries(value)) {
if (Object.hasOwn(copied, key)) continue if (Object.hasOwn(copied, key)) continue
@ -258,7 +258,7 @@ const copyBounded = (
if (isBlockedMember(key)) { if (isBlockedMember(key)) {
throw new ToolRuntimeError("InvalidDataValue", `${label} contains blocked property '${key}'.`) throw new ToolRuntimeError("InvalidDataValue", `${label} contains blocked property '${key}'.`)
} }
copied[key] = copyBounded(item, label, depth + 1, seen, preserveSandboxValues) copied[key] = copyBounded(item, label, depth + 1, seen, preserveCodeModeValues)
} }
seen.delete(value) seen.delete(value)
return copied return copied

View file

@ -1,45 +1,45 @@
import type { Fiber } from "effect" import type { Fiber } from "effect"
export class SandboxPromise { export class CodeModePromise {
constructor(readonly fiber: Fiber.Fiber<unknown, unknown>) {} constructor(readonly fiber: Fiber.Fiber<unknown, unknown>) {}
} }
export class SandboxDate { export class CodeModeDate {
constructor(readonly time: number) {} constructor(readonly time: number) {}
} }
export class SandboxRegExp { export class CodeModeRegExp {
readonly regex: RegExp readonly regex: RegExp
constructor(pattern: string, flags: string) { constructor(pattern: string, flags: string) {
this.regex = new RegExp(pattern, flags) this.regex = new RegExp(pattern, flags)
} }
} }
export class SandboxMap { export class CodeModeMap {
readonly map = new Map<unknown, unknown>() readonly map = new Map<unknown, unknown>()
} }
export class SandboxSet { export class CodeModeSet {
readonly set = new Set<unknown>() readonly set = new Set<unknown>()
} }
export class SandboxURLSearchParams { export class CodeModeURLSearchParams {
constructor(readonly params: URLSearchParams) {} constructor(readonly params: URLSearchParams) {}
} }
export class SandboxURL { export class CodeModeURL {
readonly searchParams: SandboxURLSearchParams readonly searchParams: CodeModeURLSearchParams
constructor(readonly url: URL) { constructor(readonly url: URL) {
this.searchParams = new SandboxURLSearchParams(url.searchParams) this.searchParams = new CodeModeURLSearchParams(url.searchParams)
} }
} }
export const isSandboxValue = ( export const isCodeModeValue = (
value: unknown, value: unknown,
): value is SandboxDate | SandboxRegExp | SandboxMap | SandboxSet | SandboxURL | SandboxURLSearchParams => ): value is CodeModeDate | CodeModeRegExp | CodeModeMap | CodeModeSet | CodeModeURL | CodeModeURLSearchParams =>
value instanceof SandboxDate || value instanceof CodeModeDate ||
value instanceof SandboxRegExp || value instanceof CodeModeRegExp ||
value instanceof SandboxMap || value instanceof CodeModeMap ||
value instanceof SandboxSet || value instanceof CodeModeSet ||
value instanceof SandboxURL || value instanceof CodeModeURL ||
value instanceof SandboxURLSearchParams value instanceof CodeModeURLSearchParams

View file

@ -276,7 +276,7 @@ describe("CodeMode console capture", () => {
expect(result.logs).toStrictEqual(["NaN", "Infinity -Infinity", '{"ratio":NaN,"bounds":[Infinity]}']) expect(result.logs).toStrictEqual(["NaN", "Infinity -Infinity", '{"ratio":NaN,"bounds":[Infinity]}'])
}) })
test("renders sandbox values nested inside logged containers", async () => { test("renders CodeMode values nested inside logged containers", async () => {
const result = await Effect.runPromise( const result = await Effect.runPromise(
CodeMode.execute({ CodeMode.execute({
code: ` code: `
@ -311,7 +311,7 @@ describe("CodeMode console capture", () => {
expect(result.logs).toStrictEqual(['{"box":Map(1) [["self",[Circular]]]}', '{"fn":[CodeMode reference],"ok":1}']) expect(result.logs).toStrictEqual(['{"box":Map(1) [["self",[Circular]]]}', '{"fn":[CodeMode reference],"ok":1}'])
}) })
test("console.table renders sandbox value cells", async () => { test("console.table renders CodeMode value cells", async () => {
const result = await Effect.runPromise( const result = await Effect.runPromise(
CodeMode.execute({ CodeMode.execute({
code: ` code: `

View file

@ -8,7 +8,7 @@ import { ToolRuntime } from "../src/tool-runtime.js"
// a strict interpreter would throw but idiomatic JS yields undefined / succeeds. // a strict interpreter would throw but idiomatic JS yields undefined / succeeds.
// //
// Note on the result boundary: this package normalizes a bare `undefined` result to `null` when // Note on the result boundary: this package normalizes a bare `undefined` result to `null` when
// it crosses out of the sandbox (results are JSON data), so tests asserting an in-sandbox // it crosses out of CodeMode (results are JSON data), so tests asserting an in-CodeMode
// `undefined` read check `=== undefined` inside the program and `null` at the boundary. // `undefined` read check `=== undefined` inside the program and `null` at the boundary.
const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} })) const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} }))
const value = async (code: string) => { const value = async (code: string) => {
@ -108,7 +108,7 @@ describe("H1: NaN/Infinity flow as intermediates and normalize to null at the bo
expect(await value(`const a = []; return a.length ? a.reduce((s,x)=>s+x,0)/a.length : 0`)).toBe(0) expect(await value(`const a = []; return a.length ? a.reduce((s,x)=>s+x,0)/a.length : 0`)).toBe(0)
}) })
test("a non-finite value becomes null when it leaves the sandbox", async () => { test("a non-finite value becomes null when it leaves CodeMode", async () => {
expect(await value(`return 5/0`)).toBeNull() expect(await value(`return 5/0`)).toBeNull()
expect(await value(`return 0/0`)).toBeNull() expect(await value(`return 0/0`)).toBeNull()
expect(await value(`return Math.max()`)).toBeNull() expect(await value(`return Math.max()`)).toBeNull()
@ -116,12 +116,12 @@ describe("H1: NaN/Infinity flow as intermediates and normalize to null at the bo
expect(await value(`return { a: Number("x"), b: 2, c: [1/0] }`)).toEqual({ a: null, b: 2, c: [null] }) expect(await value(`return { a: Number("x"), b: 2, c: [1/0] }`)).toEqual({ a: null, b: 2, c: [null] })
}) })
test("NaN and Infinity are usable identifiers and inspectable in-sandbox", async () => { test("NaN and Infinity are usable identifiers and inspectable in-CodeMode", async () => {
expect(await value(`return Number.isNaN(NaN)`)).toBe(true) expect(await value(`return Number.isNaN(NaN)`)).toBe(true)
expect(await value(`return Infinity > 1e9`)).toBe(true) expect(await value(`return Infinity > 1e9`)).toBe(true)
expect(await value(`return Number.isFinite(1/0)`)).toBe(false) expect(await value(`return Number.isFinite(1/0)`)).toBe(false)
expect(await value(`return [3,1,2].reduce((a,b)=>Math.max(a,b), -Infinity)`)).toBe(3) expect(await value(`return [3,1,2].reduce((a,b)=>Math.max(a,b), -Infinity)`)).toBe(3)
// JSON.stringify inside the sandbox matches JS: non-finite serializes to null // JSON.stringify inside CodeMode matches JS: non-finite serializes to null
expect(await value(`return JSON.stringify({ x: Number("z") })`)).toBe('{"x":null}') expect(await value(`return JSON.stringify({ x: Number("z") })`)).toBe('{"x":null}')
}) })
@ -321,12 +321,12 @@ describe("compound assignment matches its binary operator", () => {
return a return a
} }
test("sandbox Date += concatenates its string form, like d = d + 1", async () => { test("CodeMode Date += concatenates its string form, like d = d + 1", async () => {
const result = await pair(`let d = new Date(1000); d += 1; return d`, `let d = new Date(1000); d = d + 1; return d`) const result = await pair(`let d = new Date(1000); d += 1; return d`, `let d = new Date(1000); d = d + 1; return d`)
expect(result).toBe("1970-01-01T00:00:01.000Z1") expect(result).toBe("1970-01-01T00:00:01.000Z1")
}) })
test("sandbox Date numeric compound ops use its time value", async () => { test("CodeMode Date numeric compound ops use its time value", async () => {
expect( expect(
await pair(`let d = new Date(1000); d -= 400; return d`, `let d = new Date(1000); d = d - 400; return d`), await pair(`let d = new Date(1000); d -= 400; return d`, `let d = new Date(1000); d = d - 400; return d`),
).toBe(600) ).toBe(600)

View file

@ -212,7 +212,7 @@ describe("Test262 Promise statics", () => {
]) ])
}) })
test("Promise.resolve adopts values and preserves sandbox-promise identity", async () => { test("Promise.resolve adopts values and preserves CodeMode-promise identity", async () => {
// Sources: // Sources:
// test/built-ins/Promise/resolve/S25.4.4.5_A2.1_T1.js // test/built-ins/Promise/resolve/S25.4.4.5_A2.1_T1.js
// test/built-ins/Promise/resolve/resolve-non-obj.js // test/built-ins/Promise/resolve/resolve-non-obj.js
@ -289,7 +289,7 @@ describe("Test262 Promise statics", () => {
// test/built-ins/Promise/all/reject-immed.js // test/built-ins/Promise/all/reject-immed.js
// test/built-ins/Promise/allSettled/reject-immed.js // test/built-ins/Promise/allSettled/reject-immed.js
// test/built-ins/Promise/race/reject-immed.js // test/built-ins/Promise/race/reject-immed.js
// (adapted: immediately-rejecting thenables become sandbox promises that settled, // (adapted: immediately-rejecting thenables become CodeMode promises that settled,
// and were even observed, before the combinator call) // and were even observed, before the combinator call)
expect( expect(
await value(` await value(`
@ -360,7 +360,7 @@ describe("Test262 Promise statics", () => {
// test/built-ins/Promise/race/S25.4.4.3_A2.1_T1.js // test/built-ins/Promise/race/S25.4.4.3_A2.1_T1.js
// test/built-ins/Promise/race/S25.4.4.3_A5.1_T1.js // test/built-ins/Promise/race/S25.4.4.3_A5.1_T1.js
// (adapted: upstream requires Promise.race([]) to never settle; CodeMode intentionally // (adapted: upstream requires Promise.race([]) to never settle; CodeMode intentionally
// rejects with a catchable diagnostic instead of hanging, so this asserts the sandbox // rejects with a catchable diagnostic instead of hanging, so this asserts CodeMode
// divergence rather than the spec never-settles behavior) // divergence rather than the spec never-settles behavior)
expect( expect(
await value(` await value(`
@ -375,7 +375,7 @@ describe("Test262 Promise statics", () => {
).toEqual([true, true]) ).toEqual([true, true])
}) })
test("Promise.resolve passes the same sandbox promise through nested chains", async () => { test("Promise.resolve passes the same CodeMode promise through nested chains", async () => {
// Source: test/built-ins/Promise/resolve/S25.4.4.5_A2.2_T1.js // Source: test/built-ins/Promise/resolve/S25.4.4.5_A2.2_T1.js
// (adapted: no executor construction, and identity is observed with Array includes // (adapted: no executor construction, and identity is observed with Array includes
// because promises are not comparable data values in CodeMode) // because promises are not comparable data values in CodeMode)
@ -1208,7 +1208,7 @@ describe("Test262 AggregateError", () => {
test("coerces a non-string message to a string", async () => { test("coerces a non-string message to a string", async () => {
// Source: test/built-ins/AggregateError/message-method-prop-cast.js (value coercion only; the // Source: test/built-ins/AggregateError/message-method-prop-cast.js (value coercion only; the
// upstream object-with-toString case is omitted because the sandbox has no user toString dispatch) // upstream object-with-toString case is omitted because CodeMode has no user toString dispatch)
expect( expect(
await value(` await value(`
return [ return [
@ -1408,7 +1408,7 @@ describe("Test262 Promise constructor", () => {
test.failing("calling Promise without new throws TypeError", async () => { test.failing("calling Promise without new throws TypeError", async () => {
// Source: test/built-ins/Promise/undefined-newtarget.js // Source: test/built-ins/Promise/undefined-newtarget.js
// The sandbox currently reports a generic Error ("Only tools are callable in CodeMode."). // CodeMode currently reports a generic Error ("Only tools are callable in CodeMode.").
expect( expect(
await value(` await value(`
try { try {

View file

@ -3,7 +3,7 @@ import { Effect, Schema } from "effect"
import { CodeMode, Tool } from "../src/index.js" import { CodeMode, Tool } from "../src/index.js"
// Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS; // Standard-library value types: Date, RegExp, Map, Set. Programs use them as ordinary JS;
// intra-sandbox checkpoints (Object.* helpers, spread, coercion inputs) preserve the live // intra-CodeMode checkpoints (Object.* helpers, spread, coercion inputs) preserve the live
// values, while at the host boundary (final result, tool arguments, JSON.stringify) they // values, while at the host boundary (final result, tool arguments, JSON.stringify) they
// serialize exactly as JSON.stringify would: Date -> ISO string (invalid -> null), // serialize exactly as JSON.stringify would: Date -> ISO string (invalid -> null),
// URL -> href, and RegExp/Map/Set/URLSearchParams -> {}. // URL -> href, and RegExp/Map/Set/URLSearchParams -> {}.
@ -69,7 +69,7 @@ describe("Date", () => {
).toEqual([2024, 2, 5, 6, 7, 8, 9]) ).toEqual([2024, 2, 5, 6, 7, 8, 9])
}) })
test("invalid dates yield NaN times, guardable in-sandbox", async () => { test("invalid dates yield NaN times, guardable in-CodeMode", async () => {
expect(await value(`return Number.isNaN(new Date("garbage").getTime())`)).toBe(true) expect(await value(`return Number.isNaN(new Date("garbage").getTime())`)).toBe(true)
expect(await value(`return new Date("garbage").toJSON()`)).toBeNull() expect(await value(`return new Date("garbage").toJSON()`)).toBeNull()
}) })
@ -702,11 +702,11 @@ describe("stdlib integration", () => {
expect(await value(`const fn = () => 1; return !fn`)).toBe(false) expect(await value(`const fn = () => 1; return !fn`)).toBe(false)
}) })
test("object spread of sandbox values is a no-op, like JS", async () => { test("object spread of CodeMode values is a no-op, like JS", async () => {
expect(await value(`return { ...new Map([["a", 1]]), kept: true }`)).toEqual({ kept: true }) expect(await value(`return { ...new Map([["a", 1]]), kept: true }`)).toEqual({ kept: true })
}) })
test("dates inside Map values survive in-sandbox reads", async () => { test("dates inside Map values survive in-CodeMode reads", async () => {
expect( expect(
await value(` await value(`
const m = new Map([["start", new Date(1000)]]) const m = new Map([["start", new Date(1000)]])
@ -748,7 +748,7 @@ describe("stdlib integration", () => {
}) })
}) })
describe("sandbox values at intra-sandbox checkpoints", () => { describe("CodeMode values at intra-CodeMode checkpoints", () => {
test("Object.values/entries keep Dates usable", async () => { test("Object.values/entries keep Dates usable", async () => {
expect(await value(`return Object.values({ d: new Date(0) })[0].getTime()`)).toBe(0) expect(await value(`return Object.values({ d: new Date(0) })[0].getTime()`)).toBe(0)
expect(await value(`const [key, d] = Object.entries({ d: new Date(0) })[0]; return key + ":" + d.getTime()`)).toBe( expect(await value(`const [key, d] = Object.entries({ d: new Date(0) })[0]; return key + ":" + d.getTime()`)).toBe(
@ -799,7 +799,7 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
) )
}) })
test("object and array spread keep sandbox values usable", async () => { test("object and array spread keep CodeMode values usable", async () => {
expect( expect(
await value(` await value(`
const src = { m: new Map([["a", 1]]) } const src = { m: new Map([["a", 1]]) }
@ -811,7 +811,7 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
expect(await value(`const list = [new Date(1000)]; const copy = [...list]; return copy[0].getTime()`)).toBe(1000) expect(await value(`const list = [new Date(1000)]; const copy = [...list]; return copy[0].getTime()`)).toBe(1000)
}) })
test("Array.from over arrays keeps nested sandbox values usable", async () => { test("Array.from over arrays keeps nested CodeMode values usable", async () => {
expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5) expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
}) })
@ -866,7 +866,7 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true) expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
}) })
test("Object.* helpers see sandbox values as empty objects, never internals", async () => { test("Object.* helpers see CodeMode values as empty objects, never internals", async () => {
expect(await value(`return Object.keys(new Map([["a", 1]]))`)).toEqual([]) expect(await value(`return Object.keys(new Map([["a", 1]]))`)).toEqual([])
expect(await value(`return Object.values(new Date(0))`)).toEqual([]) expect(await value(`return Object.values(new Date(0))`)).toEqual([])
expect(await value(`return Object.entries(new Set([1]))`)).toEqual([]) expect(await value(`return Object.entries(new Set([1]))`)).toEqual([])