diff --git a/packages/codemode/AGENTS.md b/packages/codemode/AGENTS.md index 94c3fcb2d7..cbea866d4e 100644 --- a/packages/codemode/AGENTS.md +++ b/packages/codemode/AGENTS.md @@ -16,8 +16,8 @@ ## 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. -- 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. +- 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 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. - 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. diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 9962302c3c..692e5d03e2 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -20,7 +20,7 @@ ultimate source of truth. - [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] 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] The global `search(...)` built-in: synchronous tool discovery that counts as an admitted tool call and is shadowable by program declarations like other globals. @@ -94,7 +94,7 @@ ultimate source of truth. - [x] Optional property access and optional calls. - [x] Function/tool calls and spread arguments. - [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. - [x] `new` for Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise. - [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`. @@ -109,7 +109,7 @@ ultimate source of truth. ## 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] `Promise.resolve` and `Promise.reject`. - [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] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`. - [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`. - [ ] `Object.is`; runtime and tool-reference identity semantics need to be defined first. - [ ] `Object.groupBy`. diff --git a/packages/codemode/src/interpreter/methods.ts b/packages/codemode/src/interpreter/methods.ts index 5c3574088b..4757dd55df 100644 --- a/packages/codemode/src/interpreter/methods.ts +++ b/packages/codemode/src/interpreter/methods.ts @@ -13,13 +13,13 @@ import { import { rejectCircularInsertion } from "./references.js" import { isBlockedMember, type SafeObject } from "../tool-runtime.js" import { - SandboxDate, - SandboxMap, - SandboxPromise, - SandboxRegExp, - SandboxSet, - SandboxURL, - SandboxURLSearchParams, + CodeModeDate, + CodeModeMap, + CodeModePromise, + CodeModeRegExp, + CodeModeSet, + CodeModeURL, + CodeModeURLSearchParams, } from "../values.js" import { invokeDateMethod, invokeDateStatic } from "../stdlib/date.js" import { invokeJsonMethod } from "../stdlib/json.js" @@ -33,7 +33,7 @@ import { boundedData, coerceToNumber, coerceToString, invokeCoercion } from "../ export type CallbackRunner = { readonly invokeFunction: (fn: CodeModeFunction, args: Array) => Effect.Effect - readonly settlePromise: (promise: SandboxPromise) => Effect.Effect + readonly settlePromise: (promise: CodeModePromise) => Effect.Effect } export const invokeIntrinsic = ( @@ -57,22 +57,22 @@ export const invokeIntrinsic = ( if (Array.isArray(ref.receiver)) { 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)) } - if (ref.receiver instanceof SandboxRegExp) { + if (ref.receiver instanceof CodeModeRegExp) { 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) } - if (ref.receiver instanceof SandboxSet) { + if (ref.receiver instanceof CodeModeSet) { 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)) } - if (ref.receiver instanceof SandboxURLSearchParams) { + if (ref.receiver instanceof CodeModeURLSearchParams) { return invokeURLSearchParamsMethod(runner, ref.receiver, ref.name, args, 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, n result = [value] break } - if (args[0] instanceof SandboxRegExp) { + if (args[0] instanceof CodeModeRegExp) { result = value.split(args[0].regex, optNum(1)) break } @@ -181,7 +181,7 @@ const invokeStringMethod = (value: string, name: string, args: Array, n break case "replace": case "replaceAll": { - if (args[0] instanceof SandboxRegExp) { + if (args[0] instanceof CodeModeRegExp) { const pattern = args[0].regex const replacement = str(1) if (name === "replaceAll" && !pattern.global) { @@ -278,13 +278,13 @@ const invokeArrayStatic = (name: string, args: Array, node: AstNode): u [supportedSyntaxMessage], ) } - if (args[0] instanceof SandboxMap) 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 SandboxURLSearchParams) { + if (args[0] instanceof CodeModeMap) return Array.from(args[0].map.entries(), ([key, item]) => [key, item]) + if (args[0] instanceof CodeModeSet) return Array.from(args[0].set.values()) + if (args[0] instanceof CodeModeURLSearchParams) { return Array.from(args[0].params.entries(), ([key, value]) => [key, value]) } const source = args[0] - if (source instanceof SandboxPromise) { + if (source instanceof CodeModePromise) { throw new InterpreterRuntimeError( "Array.from received an un-awaited Promise; await it before creating the array.", node, @@ -341,7 +341,7 @@ const invokeStringReplacer = ( } const pattern = args[0] - if (pattern instanceof SandboxRegExp) { + if (pattern instanceof CodeModeRegExp) { if (name === "replaceAll" && !pattern.regex.global) { 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.`, @@ -364,7 +364,7 @@ const invokeStringReplacer = ( for (const match of matches) { const replacement = yield* apply(match.args) 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) : replacement output.push( @@ -404,7 +404,7 @@ export const applyCollectionCallback = ( const invokeMapMethod = ( runner: CallbackRunner, - target: SandboxMap, + target: CodeModeMap, name: string, args: Array, node: AstNode, @@ -446,7 +446,7 @@ const invokeMapMethod = ( const invokeSetMethod = ( runner: CallbackRunner, - target: SandboxSet, + target: CodeModeSet, name: string, args: Array, node: AstNode, @@ -485,7 +485,7 @@ const invokeSetMethod = ( const invokeURLSearchParamsMethod = ( runner: CallbackRunner, - target: SandboxURLSearchParams, + target: CodeModeURLSearchParams, name: string, args: Array, node: AstNode, diff --git a/packages/codemode/src/interpreter/model.ts b/packages/codemode/src/interpreter/model.ts index b6e6ad64f8..d9a2b25ef3 100644 --- a/packages/codemode/src/interpreter/model.ts +++ b/packages/codemode/src/interpreter/model.ts @@ -1,5 +1,5 @@ import type { SafeObject } from "../tool-runtime.js" -import type { SandboxPromise, SandboxURL } from "../values.js" +import type { CodeModePromise, CodeModeURL } from "../values.js" export type SourcePosition = { line: number @@ -35,7 +35,7 @@ export type StatementResult = | { kind: "continue" } export type MemberReference = { - target: SafeObject | Array | SandboxURL + target: SafeObject | Array | CodeModeURL key: string | number } @@ -71,7 +71,7 @@ export type PromiseInstanceMethodName = "then" | "catch" | "finally" export class PromiseInstanceMethodReference { constructor( - readonly promise: SandboxPromise, + readonly promise: CodeModePromise, readonly name: PromiseInstanceMethodName, ) {} } diff --git a/packages/codemode/src/interpreter/promises.ts b/packages/codemode/src/interpreter/promises.ts index 95f9b7741f..2bf1b1ea30 100644 --- a/packages/codemode/src/interpreter/promises.ts +++ b/packages/codemode/src/interpreter/promises.ts @@ -17,24 +17,24 @@ import { applyCollectionCallback, type CallbackRunner } from "./methods.js" import { typeofValue } from "./references.js" import { spreadItems } from "../stdlib/collections.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. export class PromiseRuntime { - private readonly active = new Set() - private readonly ids = new WeakMap() - private readonly observed = new WeakSet() + private readonly active = new Set() + private readonly ids = new WeakMap() + private readonly observed = new WeakSet() private readonly failures = new Map() private nextID = 0 constructor(private readonly scope: Scope.Scope) {} - create(effect: Effect.Effect): Effect.Effect { + create(effect: Effect.Effect): Effect.Effect { return Effect.suspend(() => { // Allocate before forking so reruns get distinct IDs and diagnostics retain creation order. const id = this.nextID++ 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.ids.set(promise, id) fiber.addObserver((exit) => { @@ -55,14 +55,14 @@ export class PromiseRuntime { } // Observation must be recorded when responsibility transfers, before the consumer fiber runs. - markObserved(promise: SandboxPromise): void { + markObserved(promise: CodeModePromise): void { this.observed.add(promise) const id = this.ids.get(promise) this.ids.delete(promise) if (id !== undefined) this.failures.delete(id) } - await(promise: SandboxPromise): Effect.Effect> { + await(promise: CodeModePromise): Effect.Effect> { return Fiber.await(promise.fiber) } @@ -94,7 +94,7 @@ export const invokePromiseMethod = ( ): Effect.Effect => { if (ref.name === "resolve") { 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") { return promises.create(Effect.fail(new ProgramThrow(args[0]))) @@ -114,19 +114,19 @@ export const invokePromiseMethod = ( const items = Array.from(spread) for (const item of items) { - if (item instanceof SandboxPromise) promises.markObserved(item) + if (item instanceof CodeModePromise) promises.markObserved(item) } switch (ref.name) { case "all": { 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" }))) } case "allSettled": { 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( @@ -168,13 +168,13 @@ export const invokePromiseMethod = ( ) } 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)))) } case "any": { const flipped = items.map((item) => - item instanceof SandboxPromise + item instanceof CodeModePromise ? Effect.flatMap(promises.await(item), (exit) => { if (Exit.isSuccess(exit)) return Effect.fail(new PromiseAnyFulfilled(exit.value)) if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause) @@ -201,7 +201,7 @@ export const invokePromiseInstanceMethod = ( ref: PromiseInstanceMethodReference, args: Array, node: AstNode, -): Effect.Effect => { +): Effect.Effect => { const method = `Promise.prototype.${ref.name}` promises.markObserved(ref.promise) if (ref.name === "finally") { @@ -217,7 +217,7 @@ export const constructPromise = ( promises: PromiseRuntime, executor: unknown, node: AstNode, -): Effect.Effect => { +): Effect.Effect => { if (!(executor instanceof CodeModeFunction)) { throw new InterpreterRuntimeError( "new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).", @@ -226,10 +226,10 @@ export const constructPromise = ( } return Effect.gen(function* () { const deferred = Deferred.makeUnsafe() - const box: { own?: SandboxPromise } = {} + const box: { own?: CodeModePromise } = {} const promise = yield* promises.create( 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)) 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. const reactionExit = ( promises: PromiseRuntime, - source: SandboxPromise, + source: CodeModePromise, ): Effect.Effect, unknown, R> => Effect.gen(function* () { const exit = yield* promises.await(source) @@ -293,13 +293,13 @@ const reactionExit = ( const chainReaction = ( runner: CallbackRunner, promises: PromiseRuntime, - source: SandboxPromise, + source: CodeModePromise, onFulfilled: ReactionHandler | undefined, onRejected: ReactionHandler | undefined, method: string, node: AstNode, -): Effect.Effect => { - const box: { derived?: SandboxPromise } = {} +): Effect.Effect => { + const box: { derived?: CodeModePromise } = {} const body = Effect.gen(function* () { const exit = yield* reactionExit(promises, source) const handler = Exit.isSuccess(exit) ? onFulfilled : onRejected @@ -307,7 +307,7 @@ const chainReaction = ( const input = Exit.isSuccess(exit) ? exit.value : caughtErrorValue(Cause.squash(exit.cause)) const result = yield* applyCollectionCallback(runner, handler, method, node)([input]) 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 Effect.map(promises.create(body), (derived) => { @@ -319,17 +319,17 @@ const chainReaction = ( const chainFinally = ( runner: CallbackRunner, promises: PromiseRuntime, - source: SandboxPromise, + source: CodeModePromise, cleanup: ReactionHandler | undefined, method: string, node: AstNode, -): Effect.Effect => +): Effect.Effect => promises.create( Effect.gen(function* () { const exit = yield* reactionExit(promises, source) if (cleanup !== undefined) { 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 }), diff --git a/packages/codemode/src/interpreter/references.ts b/packages/codemode/src/interpreter/references.ts index afbbdbf9e9..f32b726149 100644 --- a/packages/codemode/src/interpreter/references.ts +++ b/packages/codemode/src/interpreter/references.ts @@ -15,7 +15,7 @@ import { UriFunction, } from "./model.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 => value instanceof CodeModeFunction || @@ -26,13 +26,13 @@ export const isRuntimeReference = (value: unknown): boolean => value instanceof PromiseNamespace || value instanceof PromiseMethodReference || value instanceof PromiseInstanceMethodReference || - value instanceof SandboxPromise || + value instanceof CodeModePromise || value instanceof CoercionFunction || value instanceof UriFunction || value instanceof SearchFunction || value instanceof PromiseCapabilityFunction || value instanceof ErrorConstructorReference || - isSandboxValue(value) + isCodeModeValue(value) export const containsRuntimeReference = (value: unknown, seen = new Set()): boolean => { if (isRuntimeReference(value)) return true @@ -46,9 +46,9 @@ export const containsRuntimeReference = (value: unknown, seen = new Set( 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()): boolean => { - if (isSandboxValue(value)) return false + if (isCodeModeValue(value)) return false if (isRuntimeReference(value)) return true if (value === null || typeof value !== "object") return false if (seen.has(value)) return false diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index 3b9e18cc9d..3dfc9c5cb9 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -73,14 +73,14 @@ import { valueConstructors, } from "../stdlib/value.js" import { - isSandboxValue, - SandboxDate, - SandboxMap, - SandboxPromise, - SandboxRegExp, - SandboxSet, - SandboxURL, - SandboxURLSearchParams, + isCodeModeValue, + CodeModeDate, + CodeModeMap, + CodeModePromise, + CodeModeRegExp, + CodeModeSet, + CodeModeURL, + CodeModeURLSearchParams, } from "../values.js" 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) { switch (rhs.name) { case "Date": - return lhs instanceof SandboxDate + return lhs instanceof CodeModeDate case "RegExp": - return lhs instanceof SandboxRegExp + return lhs instanceof CodeModeRegExp case "Map": - return lhs instanceof SandboxMap + return lhs instanceof CodeModeMap case "Set": - return lhs instanceof SandboxSet + return lhs instanceof CodeModeSet case "URL": - return lhs instanceof SandboxURL + return lhs instanceof CodeModeURL case "URLSearchParams": - return lhs instanceof SandboxURLSearchParams + return lhs instanceof CodeModeURLSearchParams case "Array": return Array.isArray(lhs) case "Object": 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")) { return false } @@ -226,7 +226,7 @@ export class Interpreter { } // 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 }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) } @@ -235,16 +235,16 @@ export class Interpreter { private createToolCallPromise( path: ReadonlyArray, args: Array, - ): Effect.Effect { + ): Effect.Effect { return this.createPromise(Effect.suspend(() => this.invokeTool(path, args))) } - private createPromise(effect: Effect.Effect): Effect.Effect { + private createPromise(effect: Effect.Effect): Effect.Effect { return this.promises.create(effect) } // Fiber exits make settlement idempotent; yielding prevents inline continuation. - private settlePromise(promise: SandboxPromise): Effect.Effect { + private settlePromise(promise: CodeModePromise): Effect.Effect { const promises = this.promises return Effect.suspend(() => { promises.markObserved(promise) @@ -971,7 +971,7 @@ export class Interpreter { // Await always suspends, including for plain values. const self = this 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": @@ -1019,23 +1019,23 @@ export class Interpreter { throw unsupportedSyntax("NewExpression", node) } - private constructDate(args: Array): SandboxDate { - if (args.length === 0) return new SandboxDate(Date.now()) + private constructDate(args: Array): CodeModeDate { + if (args.length === 0) return new CodeModeDate(Date.now()) if (args.length === 1) { const arg = args[0] - if (arg instanceof SandboxDate) return new SandboxDate(arg.time) - if (typeof arg === "number") return new SandboxDate(new Date(arg).getTime()) - if (typeof arg === "string") return new SandboxDate(Date.parse(arg)) - return new SandboxDate(Number.NaN) + if (arg instanceof CodeModeDate) return new CodeModeDate(arg.time) + if (typeof arg === "number") return new CodeModeDate(new Date(arg).getTime()) + if (typeof arg === "string") return new CodeModeDate(Date.parse(arg)) + return new CodeModeDate(Number.NaN) } 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, node: AstNode): SandboxRegExp { + private constructRegExp(args: Array, node: AstNode): CodeModeRegExp { const first = args[0] 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] if (flagsArg !== undefined && typeof flagsArg !== "string") { throw new InterpreterRuntimeError( @@ -1043,9 +1043,9 @@ export class Interpreter { node, ) } - const flags = flagsArg ?? (first instanceof SandboxRegExp ? first.regex.flags : "") + const flags = flagsArg ?? (first instanceof CodeModeRegExp ? first.regex.flags : "") try { - return new SandboxRegExp(pattern, flags) + return new CodeModeRegExp(pattern, flags) } catch (error) { const reason = regexFailureReason(error) throw new InterpreterRuntimeError( @@ -1057,12 +1057,12 @@ export class Interpreter { } } - private constructMap(init: unknown, node: AstNode): SandboxMap { - const target = new SandboxMap() + private constructMap(init: unknown, node: AstNode): CodeModeMap { + const target = new CodeModeMap() if (init === undefined || init === null) return target const entries = Array.isArray(init) ? init - : init instanceof SandboxMap + : init instanceof CodeModeMap ? Array.from(init.map.entries(), ([key, item]): Array => [key, item]) : undefined if (entries === undefined) { @@ -1080,12 +1080,12 @@ export class Interpreter { return target } - private constructSet(init: unknown, node: AstNode): SandboxSet { - const target = new SandboxSet() + private constructSet(init: unknown, node: AstNode): CodeModeSet { + const target = new CodeModeSet() if (init === undefined || init === null) return target const items = Array.isArray(init) ? init - : init instanceof SandboxSet + : init instanceof CodeModeSet ? Array.from(init.set.values()) : typeof init === "string" ? Array.from(init) @@ -1097,7 +1097,7 @@ export class Interpreter { return target } - private constructURL(args: Array, node: AstNode): SandboxURL { + private constructURL(args: Array, node: AstNode): CodeModeURL { if (args.length === 0) { throw new InterpreterRuntimeError("new URL(...) requires a URL string and an optional base URL.", node).as( "TypeError", @@ -1106,7 +1106,7 @@ export class Interpreter { const input = urlArgument(args[0], "new URL input") const base = args[1] === undefined ? undefined : urlArgument(args[1], "new URL base") try { - return new SandboxURL(new URL(input, base)) + return new CodeModeURL(new URL(input, base)) } catch { throw new InterpreterRuntimeError( `new URL(...) received an invalid URL${base === undefined ? "" : " or base URL"}.`, @@ -1115,16 +1115,16 @@ export class Interpreter { } } - private constructURLSearchParams(init: unknown, node: AstNode): SandboxURLSearchParams { - if (init === undefined) return new SandboxURLSearchParams(new URLSearchParams()) - if (init instanceof SandboxURLSearchParams) { - return new SandboxURLSearchParams(new URLSearchParams(init.params)) + private constructURLSearchParams(init: unknown, node: AstNode): CodeModeURLSearchParams { + if (init === undefined) return new CodeModeURLSearchParams(new URLSearchParams()) + if (init instanceof CodeModeURLSearchParams) { + 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") { - 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( Array.from(init.map.entries(), ([key, value]) => [key, value]), node, @@ -1143,9 +1143,9 @@ export class Interpreter { 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") if (data === null || typeof data !== "object") { throw new InterpreterRuntimeError( @@ -1153,7 +1153,7 @@ export class Interpreter { node, ).as("TypeError") } - return new SandboxURLSearchParams( + return new CodeModeURLSearchParams( new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))), ) } @@ -1176,7 +1176,7 @@ export class Interpreter { // Null-prototype data needs explicit primitive coercion; identity and `in` retain raw objects. // Dates use string coercion for `+` and epoch time elsewhere. 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 } const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object" @@ -1261,7 +1261,7 @@ export class Interpreter { throw new InterpreterRuntimeError("Unary operators require data values in CodeMode.", node, "InvalidDataValue") } const operand = - value instanceof SandboxDate + value instanceof CodeModeDate ? value.time : value !== null && typeof value === "object" ? coerceToString(value) @@ -1520,11 +1520,11 @@ export class Interpreter { }) if (!fn.async) return run // The initial yield assigns `box.own` before the body can self-resolve. - const box: { own?: SandboxPromise } = {} + const box: { own?: CodeModePromise } = {} return Effect.map( this.createPromise( 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()) return invocation.settlePromise(value) }), @@ -1546,7 +1546,7 @@ export class Interpreter { if (property.type === "SpreadElement") { 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)) { throw new InterpreterRuntimeError( "Object spread requires a data object in CodeMode.", @@ -1748,28 +1748,28 @@ export class Interpreter { 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) return new ComputedValue(undefined) } - if (objectValue instanceof SandboxRegExp) { + if (objectValue instanceof CodeModeRegExp) { if (typeof key === "string" && regexpProperties.has(key)) { return new ComputedValue((objectValue.regex as unknown as Record)[key]) } if (typeof key === "string" && regexpMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) } - if (objectValue instanceof SandboxMap) { + if (objectValue instanceof CodeModeMap) { if (key === "size") return new ComputedValue(objectValue.map.size) if (typeof key === "string" && mapMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) } - if (objectValue instanceof SandboxSet) { + if (objectValue instanceof CodeModeSet) { if (key === "size") return new ComputedValue(objectValue.set.size) if (typeof key === "string" && setMethods.has(key)) return new IntrinsicReference(objectValue, key) return new ComputedValue(undefined) } - if (objectValue instanceof SandboxURL) { + if (objectValue instanceof CodeModeURL) { if (key === "searchParams") { return new ComputedValue(objectValue.searchParams) } @@ -1777,7 +1777,7 @@ export class Interpreter { if (typeof key === "string" && urlProperties.has(key)) return { target: objectValue, key } return new ComputedValue(undefined) } - if (objectValue instanceof SandboxURLSearchParams) { + if (objectValue instanceof CodeModeURLSearchParams) { if (key === "size") return new ComputedValue(objectValue.params.size) if (typeof key === "string" && urlSearchParamsMethods.has(key)) { return new IntrinsicReference(objectValue, key) @@ -1786,7 +1786,7 @@ export class Interpreter { } // 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") { return new PromiseInstanceMethodReference(objectValue, key) } @@ -1851,7 +1851,7 @@ export class Interpreter { } 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(reference.key)] } return reference.target[String(reference.key)] @@ -1891,7 +1891,7 @@ export class Interpreter { } const key = Array.isArray(reference.target) ? Number(reference.key) : String(reference.key) const current = - reference.target instanceof SandboxURL + reference.target instanceof CodeModeURL ? (reference.target.url as unknown as Record)[key] : (reference.target as Record)[key] const { write, next, result } = yield* compute(current) @@ -1915,7 +1915,7 @@ export class Interpreter { target[index] = next return } - if (reference.target instanceof SandboxURL) { + if (reference.target instanceof CodeModeURL) { const property = key as string if (!urlWritableProperties.has(property)) { throw new InterpreterRuntimeError(`URL.${property} is read-only.`, node).as("TypeError") diff --git a/packages/codemode/src/stdlib/collections.ts b/packages/codemode/src/stdlib/collections.ts index f4760ff706..3851339791 100644 --- a/packages/codemode/src/stdlib/collections.ts +++ b/packages/codemode/src/stdlib/collections.ts @@ -43,9 +43,9 @@ export const setMethods = new Set(["add", "has", "delete", "clear", "forEach", " export const spreadItems = (value: unknown): Array | undefined => { if (Array.isArray(value)) return 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 SandboxSet) return Array.from(value.set.values()) - if (value instanceof SandboxURLSearchParams) return Array.from(value.params.entries(), ([key, item]) => [key, item]) + if (value instanceof CodeModeMap) return Array.from(value.map.entries(), ([key, item]) => [key, item]) + if (value instanceof CodeModeSet) return Array.from(value.set.values()) + if (value instanceof CodeModeURLSearchParams) return Array.from(value.params.entries(), ([key, item]) => [key, item]) return undefined } -import { SandboxMap, SandboxSet, SandboxURLSearchParams } from "../values.js" +import { CodeModeMap, CodeModeSet, CodeModeURLSearchParams } from "../values.js" diff --git a/packages/codemode/src/stdlib/console.ts b/packages/codemode/src/stdlib/console.ts index 4663f62f1a..01445ea57a 100644 --- a/packages/codemode/src/stdlib/console.ts +++ b/packages/codemode/src/stdlib/console.ts @@ -1,14 +1,14 @@ import { containsOpaqueReference, containsRuntimeReference, isRuntimeReference } from "../interpreter/references.js" import { copyIn, copyOut } from "../tool-runtime.js" import { - isSandboxValue, - SandboxDate, - SandboxMap, - SandboxPromise, - SandboxRegExp, - SandboxSet, - SandboxURL, - SandboxURLSearchParams, + isCodeModeValue, + CodeModeDate, + CodeModeMap, + CodeModePromise, + CodeModeRegExp, + CodeModeSet, + CodeModeURL, + CodeModeURLSearchParams, } from "../values.js" import { boundedData, coerceToString } from "./value.js" @@ -34,14 +34,14 @@ const formatConsoleValue = (value: unknown, seen: Set, depth: number): s if (typeof value === "string") return JSON.stringify(value) if (typeof value === "number" || typeof value === "boolean") 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 SandboxDate) return coerceToString(value) - if (value instanceof SandboxRegExp) return coerceToString(value) - if (value instanceof SandboxURL) return coerceToString(value) - if (value instanceof SandboxURLSearchParams) return coerceToString(value) + if (value instanceof CodeModePromise) return "[Promise (await it to get its value)]" + if (value instanceof CodeModeDate) return coerceToString(value) + if (value instanceof CodeModeRegExp) return coerceToString(value) + if (value instanceof CodeModeURL) return coerceToString(value) + if (value instanceof CodeModeURLSearchParams) return coerceToString(value) if (depth > MAX_CONSOLE_DEPTH) return "..." if (seen.has(value)) return "[Circular]" - if (value instanceof SandboxMap) { + if (value instanceof CodeModeMap) { seen.add(value) try { const entries = Array.from(value.map.entries(), ([key, item]): Array => [key, item]) @@ -50,7 +50,7 @@ const formatConsoleValue = (value: unknown, seen: Set, depth: number): s seen.delete(value) } } - if (value instanceof SandboxSet) { + if (value instanceof CodeModeSet) { seen.add(value) try { return `Set(${value.set.size}) ${formatConsoleValue(Array.from(value.set.values()), seen, depth + 1)}` @@ -100,14 +100,14 @@ const consoleTableRows = ( if (Array.isArray(data)) { 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 [{ index: "0", values: { Value: data } }] } const consoleTableValues = (value: unknown, columns: ReadonlyArray | undefined): Record => { - 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 if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, source[column]])) return Object.fromEntries(Object.entries(source)) diff --git a/packages/codemode/src/stdlib/date.ts b/packages/codemode/src/stdlib/date.ts index 11566e2d7c..91c89d37a6 100644 --- a/packages/codemode/src/stdlib/date.ts +++ b/packages/codemode/src/stdlib/date.ts @@ -36,7 +36,7 @@ export const invokeDateStatic = (name: string, args: Array, 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) switch (name) { case "getTime": @@ -88,5 +88,5 @@ export const invokeDateMethod = (value: SandboxDate, name: string, node: AstNode } } import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" -import { SandboxDate } from "../values.js" +import { CodeModeDate } from "../values.js" import { coerceToNumber, coerceToString } from "./value.js" diff --git a/packages/codemode/src/stdlib/object.ts b/packages/codemode/src/stdlib/object.ts index f25077857f..def400344b 100644 --- a/packages/codemode/src/stdlib/object.ts +++ b/packages/codemode/src/stdlib/object.ts @@ -1,6 +1,6 @@ import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.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" export const objectMethodsPreservingIdentity = new Set(["assign", "values", "entries", "fromEntries"]) @@ -9,8 +9,8 @@ export const invokeObjectMethod = (name: string, args: Array, node: Ast const requireObject = (): Record => { const input = args[0] if (Array.isArray(input)) return input as unknown as Record - if (isSandboxValue(input)) return {} - if (input instanceof SandboxPromise) { + if (isCodeModeValue(input)) return {} + if (input instanceof CodeModePromise) { throw new InterpreterRuntimeError( `Object.${name} received an un-awaited Promise; await it before inspecting the result.`, node, @@ -46,12 +46,12 @@ export const invokeObjectMethod = (name: string, args: Array, node: Ast return Object.hasOwn(requireObject(), String(args[1])) case "assign": { 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) } const out = target as Record 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)) { throw new InterpreterRuntimeError("Object.assign expects data objects.", node) } @@ -60,17 +60,17 @@ export const invokeObjectMethod = (name: string, args: Array, node: Ast return out } case "fromEntries": { - if (args[0] instanceof SandboxMap) { + if (args[0] instanceof CodeModeMap) { const out: Record = Object.create(null) for (const [key, item] of args[0].map.entries()) addEntry(out, key, item) return out } - if (args[0] instanceof SandboxURLSearchParams) { + if (args[0] instanceof CodeModeURLSearchParams) { const out: Record = Object.create(null) for (const [key, value] of args[0].params.entries()) guardedSet(out, key, value) 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)) { boundedData(args[0], "Object.fromEntries input") throw new InterpreterRuntimeError("Object.fromEntries expects an array of [key, value] pairs.", node) @@ -78,7 +78,7 @@ export const invokeObjectMethod = (name: string, args: Array, node: Ast const out: Record = Object.create(null) for (const pair of pairs) { 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) const entry = pair as Record addEntry(out, entry[0], entry[1]) diff --git a/packages/codemode/src/stdlib/regexp.ts b/packages/codemode/src/stdlib/regexp.ts index eac123bc16..29281c2371 100644 --- a/packages/codemode/src/stdlib/regexp.ts +++ b/packages/codemode/src/stdlib/regexp.ts @@ -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.' 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") { try { return new RegExp(arg, extraFlags) @@ -50,7 +50,7 @@ export const matchToValue = (match: RegExpMatchArray): Array => { } export const invokeRegExpMethod = ( - value: SandboxRegExp, + value: CodeModeRegExp, name: string, args: Array, node: AstNode, @@ -70,5 +70,5 @@ export const invokeRegExpMethod = ( } import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js" import { isBlockedMember, type SafeObject } from "../tool-runtime.js" -import { SandboxRegExp } from "../values.js" +import { CodeModeRegExp } from "../values.js" import { coerceToString } from "./value.js" diff --git a/packages/codemode/src/stdlib/url.ts b/packages/codemode/src/stdlib/url.ts index 583d776d22..39e960539f 100644 --- a/packages/codemode/src/stdlib/url.ts +++ b/packages/codemode/src/stdlib/url.ts @@ -66,7 +66,7 @@ export const invokeUriFunction = (ref: UriFunction, args: Array, node: } 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, node: AstNode): unknown => { 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, node: AstNod const base = args[1] === undefined ? undefined : urlArgument(args[1], `URL.${name} base`) try { const url = new URL(input, base) - return name === "canParse" ? true : new SandboxURL(url) + return name === "canParse" ? true : new CodeModeURL(url) } catch { 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 throw new InterpreterRuntimeError(`URL method '${name}' is not available in CodeMode.`, node) } 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" diff --git a/packages/codemode/src/stdlib/value.ts b/packages/codemode/src/stdlib/value.ts index 7cdd8cc7dc..9a47285da5 100644 --- a/packages/codemode/src/stdlib/value.ts +++ b/packages/codemode/src/stdlib/value.ts @@ -34,13 +34,13 @@ export const boundedData = (value: unknown, label: string): unknown => copyIn(va export const coerceToString = (value: unknown): string => { if (value === null) return "null" 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" - if (value instanceof SandboxRegExp) return `/${value.regex.source}/${value.regex.flags}` - if (value instanceof SandboxMap) return "[object Map]" - if (value instanceof SandboxSet) return "[object Set]" - if (value instanceof SandboxURL) return value.url.href - if (value instanceof SandboxURLSearchParams) return value.params.toString() + if (value instanceof CodeModeRegExp) return `/${value.regex.source}/${value.regex.flags}` + if (value instanceof CodeModeMap) return "[object Map]" + if (value instanceof CodeModeSet) return "[object Set]" + if (value instanceof CodeModeURL) return value.url.href + if (value instanceof CodeModeURLSearchParams) return value.params.toString() if (typeof value === "object") { return Array.isArray(value) ? 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 => { - if (value instanceof SandboxDate) return value.time - if (isSandboxValue(value)) return Number.NaN + if (value instanceof CodeModeDate) return value.time + if (isCodeModeValue(value)) return Number.NaN return value !== null && typeof value === "object" && !Array.isArray(value) ? Number.NaN : Number(value) } export const invokeCoercion = (ref: CoercionFunction, args: Array, node: AstNode): unknown => { const raw = args[0] - if (isSandboxValue(raw)) { + if (isCodeModeValue(raw)) { if (ref.name === "Boolean") return true if (ref.name === "Number") return coerceToNumber(raw) if (ref.name === "String") return coerceToString(raw) @@ -80,11 +80,11 @@ export const invokeCoercion = (ref: CoercionFunction, args: Array, node import { type AstNode, CoercionFunction, InterpreterRuntimeError } from "../interpreter/model.js" import { copyIn, type SafeObject } from "../tool-runtime.js" import { - isSandboxValue, - SandboxDate, - SandboxMap, - SandboxRegExp, - SandboxSet, - SandboxURL, - SandboxURLSearchParams, + isCodeModeValue, + CodeModeDate, + CodeModeMap, + CodeModeRegExp, + CodeModeSet, + CodeModeURL, + CodeModeURLSearchParams, } from "../values.js" diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index 0bb8c00aec..90c3b2de60 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -10,13 +10,13 @@ import { } from "./tool-schema.js" import { isDefinition as isToolDefinition, type Definition } from "./tool.js" import { - SandboxDate, - SandboxMap, - SandboxPromise, - SandboxRegExp, - SandboxSet, - SandboxURL, - SandboxURLSearchParams, + CodeModeDate, + CodeModeMap, + CodeModePromise, + CodeModeRegExp, + CodeModeSet, + CodeModeURL, + CodeModeURLSearchParams, } from "./values.js" 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) -// Checkpoint mode preserves sandbox values; boundary mode JSON-normalizes them. -export const copyIn = (value: unknown, label: string, preserveSandboxValues = false): unknown => - copyBounded(value, label, 0, new Set(), preserveSandboxValues) +// Checkpoint mode preserves CodeMode values; boundary mode JSON-normalizes them. +export const copyIn = (value: unknown, label: string, preserveCodeModeValues = false): unknown => + copyBounded(value, label, 0, new Set(), preserveCodeModeValues) const copyBounded = ( value: unknown, label: string, depth: number, seen: Set, - preserveSandboxValues: boolean, + preserveCodeModeValues: boolean, ): unknown => { if (depth > 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.`) } - if (value instanceof SandboxPromise) { + if (value instanceof CodeModePromise) { throw new ToolRuntimeError( "InvalidDataValue", `${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 ( - value instanceof SandboxDate || - value instanceof SandboxRegExp || - value instanceof SandboxMap || - value instanceof SandboxSet || - value instanceof SandboxURL || - value instanceof SandboxURLSearchParams + value instanceof CodeModeDate || + value instanceof CodeModeRegExp || + value instanceof CodeModeMap || + value instanceof CodeModeSet || + value instanceof CodeModeURL || + value instanceof CodeModeURLSearchParams ) { return value } - if (value instanceof Date) return new SandboxDate(value.getTime()) - if (value instanceof RegExp) return new SandboxRegExp(value.source, value.flags) + if (value instanceof Date) return new CodeModeDate(value.getTime()) + if (value instanceof RegExp) return new CodeModeRegExp(value.source, value.flags) if (value instanceof Map) { - const wrapped = new SandboxMap() + const wrapped = new CodeModeMap() for (const [key, item] of value.entries()) { wrapped.map.set(copyBounded(key, label, depth + 1, seen, true), copyBounded(item, label, depth + 1, seen, true)) } return wrapped } 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)) return wrapped } - if (value instanceof URL) return new SandboxURL(new URL(value.href)) - if (value instanceof URLSearchParams) return new SandboxURLSearchParams(new URLSearchParams(value)) + if (value instanceof URL) return new CodeModeURL(new URL(value.href)) + 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 } if (value instanceof Date) { 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 SandboxRegExp || - value instanceof SandboxMap || - value instanceof SandboxSet || - value instanceof SandboxURLSearchParams || + value instanceof CodeModeRegExp || + value instanceof CodeModeMap || + value instanceof CodeModeSet || + value instanceof CodeModeURLSearchParams || value instanceof RegExp || value instanceof Map || value instanceof Set || @@ -233,8 +233,8 @@ const copyBounded = ( seen.add(value) if (Array.isArray(value)) { - const copied = value.map((item) => copyBounded(item, label, depth + 1, seen, preserveSandboxValues)) - if (preserveSandboxValues) { + const copied = value.map((item) => copyBounded(item, label, depth + 1, seen, preserveCodeModeValues)) + if (preserveCodeModeValues) { // Checkpoint copies retain array metadata that boundary copies omit. for (const [key, item] of Object.entries(value)) { if (Object.hasOwn(copied, key)) continue @@ -258,7 +258,7 @@ const copyBounded = ( if (isBlockedMember(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) return copied diff --git a/packages/codemode/src/values.ts b/packages/codemode/src/values.ts index 3ba2421d5d..539aba82e4 100644 --- a/packages/codemode/src/values.ts +++ b/packages/codemode/src/values.ts @@ -1,45 +1,45 @@ import type { Fiber } from "effect" -export class SandboxPromise { +export class CodeModePromise { constructor(readonly fiber: Fiber.Fiber) {} } -export class SandboxDate { +export class CodeModeDate { constructor(readonly time: number) {} } -export class SandboxRegExp { +export class CodeModeRegExp { readonly regex: RegExp constructor(pattern: string, flags: string) { this.regex = new RegExp(pattern, flags) } } -export class SandboxMap { +export class CodeModeMap { readonly map = new Map() } -export class SandboxSet { +export class CodeModeSet { readonly set = new Set() } -export class SandboxURLSearchParams { +export class CodeModeURLSearchParams { constructor(readonly params: URLSearchParams) {} } -export class SandboxURL { - readonly searchParams: SandboxURLSearchParams +export class CodeModeURL { + readonly searchParams: CodeModeURLSearchParams 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 is SandboxDate | SandboxRegExp | SandboxMap | SandboxSet | SandboxURL | SandboxURLSearchParams => - value instanceof SandboxDate || - value instanceof SandboxRegExp || - value instanceof SandboxMap || - value instanceof SandboxSet || - value instanceof SandboxURL || - value instanceof SandboxURLSearchParams +): value is CodeModeDate | CodeModeRegExp | CodeModeMap | CodeModeSet | CodeModeURL | CodeModeURLSearchParams => + value instanceof CodeModeDate || + value instanceof CodeModeRegExp || + value instanceof CodeModeMap || + value instanceof CodeModeSet || + value instanceof CodeModeURL || + value instanceof CodeModeURLSearchParams diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index a9218f0e7e..295a8f1db0 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/packages/codemode/test/codemode.test.ts @@ -276,7 +276,7 @@ describe("CodeMode console capture", () => { 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( CodeMode.execute({ code: ` @@ -311,7 +311,7 @@ describe("CodeMode console capture", () => { 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( CodeMode.execute({ code: ` diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index 33c6e22361..132b40b12c 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -8,7 +8,7 @@ import { ToolRuntime } from "../src/tool-runtime.js" // 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 -// 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. const run = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} })) 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) }) - 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 0/0`)).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] }) }) - 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 Infinity > 1e9`)).toBe(true) 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) - // 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}') }) @@ -321,12 +321,12 @@ describe("compound assignment matches its binary operator", () => { 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`) 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( await pair(`let d = new Date(1000); d -= 400; return d`, `let d = new Date(1000); d = d - 400; return d`), ).toBe(600) diff --git a/packages/codemode/test/promise-test262.test.ts b/packages/codemode/test/promise-test262.test.ts index e416ec942c..2ba3a3aa14 100644 --- a/packages/codemode/test/promise-test262.test.ts +++ b/packages/codemode/test/promise-test262.test.ts @@ -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: // test/built-ins/Promise/resolve/S25.4.4.5_A2.1_T1.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/allSettled/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) expect( 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_A5.1_T1.js // (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) expect( await value(` @@ -375,7 +375,7 @@ describe("Test262 Promise statics", () => { ).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 // (adapted: no executor construction, and identity is observed with Array includes // 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 () => { // 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( await value(` return [ @@ -1408,7 +1408,7 @@ describe("Test262 Promise constructor", () => { test.failing("calling Promise without new throws TypeError", async () => { // 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( await value(` try { diff --git a/packages/codemode/test/stdlib.test.ts b/packages/codemode/test/stdlib.test.ts index dc9710bd2f..ba779f33d7 100644 --- a/packages/codemode/test/stdlib.test.ts +++ b/packages/codemode/test/stdlib.test.ts @@ -3,7 +3,7 @@ import { Effect, Schema } from "effect" import { CodeMode, Tool } from "../src/index.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 // serialize exactly as JSON.stringify would: Date -> ISO string (invalid -> null), // URL -> href, and RegExp/Map/Set/URLSearchParams -> {}. @@ -69,7 +69,7 @@ describe("Date", () => { ).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 new Date("garbage").toJSON()`)).toBeNull() }) @@ -702,11 +702,11 @@ describe("stdlib integration", () => { 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 }) }) - test("dates inside Map values survive in-sandbox reads", async () => { + test("dates inside Map values survive in-CodeMode reads", async () => { expect( await value(` 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 () => { 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( @@ -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( await value(` 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) }) - 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) }) @@ -866,7 +866,7 @@ describe("sandbox values at intra-sandbox checkpoints", () => { 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.values(new Date(0))`)).toEqual([]) expect(await value(`return Object.entries(new Set([1]))`)).toEqual([])