feat(codemode): sync v2 implementation (#35574)

This commit is contained in:
Aiden Cline 2026-07-06 13:19:21 -05:00 committed by GitHub
parent eb6ff0c1e0
commit d5aa79c73a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 9544 additions and 6643 deletions

View file

@ -0,0 +1,51 @@
export const arrayMethods = new Set([
"map",
"filter",
"find",
"findIndex",
"findLast",
"findLastIndex",
"some",
"every",
"includes",
"join",
"reduce",
"reduceRight",
"flatMap",
"forEach",
"sort",
"toSorted",
"slice",
"concat",
"indexOf",
"lastIndexOf",
"at",
"flat",
"reverse",
"toReversed",
"with",
"push",
"pop",
"shift",
"unshift",
"splice",
"fill",
"copyWithin",
"keys",
"values",
"entries",
])
export const mapMethods = new Set(["get", "set", "has", "delete", "clear", "forEach", "keys", "values", "entries"])
export const setMethods = new Set(["add", "has", "delete", "clear", "forEach", "keys", "values", "entries"])
export const spreadItems = (value: unknown): Array<unknown> | 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])
return undefined
}
import { SandboxMap, SandboxSet, SandboxURLSearchParams } from "../values.js"

View file

@ -0,0 +1,4 @@
export const consoleMethods = new Set(["log", "info", "debug", "warn", "error", "dir", "table"])
/** Console formatting recursion ceiling; deeper values render as "...". */
export const MAX_CONSOLE_DEPTH = 32

View file

@ -0,0 +1,94 @@
export const dateMethods = new Set([
"getTime",
"valueOf",
"toISOString",
"toJSON",
"toString",
"getFullYear",
"getMonth",
"getDate",
"getDay",
"getHours",
"getMinutes",
"getSeconds",
"getMilliseconds",
"getUTCFullYear",
"getUTCMonth",
"getUTCDate",
"getUTCDay",
"getUTCHours",
"getUTCMinutes",
"getUTCSeconds",
"getUTCMilliseconds",
"getTimezoneOffset",
])
export const dateStatics = new Set(["now", "parse", "UTC"])
export const invokeDateStatic = (name: string, args: Array<unknown>, node: AstNode): number => {
switch (name) {
case "now":
return Date.now()
case "parse":
return Date.parse(coerceToString(args[0]))
case "UTC":
return Date.UTC(...(args.map((arg) => coerceToNumber(arg)) as Parameters<typeof Date.UTC>))
default:
throw new InterpreterRuntimeError(`Date.${name} is not available in CodeMode.`, node)
}
}
export const invokeDateMethod = (value: SandboxDate, name: string, node: AstNode): unknown => {
const hosted = new Date(value.time)
switch (name) {
case "getTime":
case "valueOf":
return value.time
case "toISOString":
if (!Number.isFinite(value.time)) throw new InterpreterRuntimeError("Invalid time value.", node)
return hosted.toISOString()
case "toJSON":
return Number.isFinite(value.time) ? hosted.toISOString() : null
case "toString":
return coerceToString(value)
case "getFullYear":
return hosted.getFullYear()
case "getMonth":
return hosted.getMonth()
case "getDate":
return hosted.getDate()
case "getDay":
return hosted.getDay()
case "getHours":
return hosted.getHours()
case "getMinutes":
return hosted.getMinutes()
case "getSeconds":
return hosted.getSeconds()
case "getMilliseconds":
return hosted.getMilliseconds()
case "getUTCFullYear":
return hosted.getUTCFullYear()
case "getUTCMonth":
return hosted.getUTCMonth()
case "getUTCDate":
return hosted.getUTCDate()
case "getUTCDay":
return hosted.getUTCDay()
case "getUTCHours":
return hosted.getUTCHours()
case "getUTCMinutes":
return hosted.getUTCMinutes()
case "getUTCSeconds":
return hosted.getUTCSeconds()
case "getUTCMilliseconds":
return hosted.getUTCMilliseconds()
case "getTimezoneOffset":
return hosted.getTimezoneOffset()
default:
throw new InterpreterRuntimeError(`Date method '${name}' is not available in CodeMode.`, node)
}
}
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { SandboxDate } from "../values.js"
import { coerceToNumber, coerceToString } from "./value.js"

View file

@ -0,0 +1,42 @@
import {
type AstNode,
CodeModeFunction,
InterpreterRuntimeError,
supportedSyntaxMessage,
} from "../interpreter/model.js"
import { copyIn, copyOut } from "../tool-runtime.js"
export const jsonStatics = new Set(["stringify", "parse"])
export const invokeJsonMethod = (name: string, args: Array<unknown>, node: AstNode): unknown => {
if (!jsonStatics.has(name)) throw new InterpreterRuntimeError(`JSON.${name} is not available in CodeMode.`, node)
switch (name) {
case "stringify": {
const replacer = args[1]
if (Array.isArray(replacer) || replacer instanceof CodeModeFunction) {
throw new InterpreterRuntimeError(
"JSON.stringify replacers are not supported in CodeMode.",
node,
"UnsupportedSyntax",
[supportedSyntaxMessage],
)
}
const space = args[2]
const indent = typeof space === "number" || typeof space === "string" ? space : undefined
return JSON.stringify(copyOut(copyIn(args[0], "JSON.stringify value")), null, indent)
}
case "parse": {
const text = args[0]
if (typeof text !== "string") throw new InterpreterRuntimeError("JSON.parse expects a string.", node)
try {
return copyIn(JSON.parse(text), "JSON.parse result")
} catch (error) {
throw new InterpreterRuntimeError(
`JSON.parse received invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
node,
).as("SyntaxError")
}
}
}
throw new InterpreterRuntimeError(`JSON.${name} is not available in CodeMode.`, node)
}

View file

@ -0,0 +1,65 @@
export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"])
export const mathMethods = new Set([
"max",
"min",
"abs",
"floor",
"ceil",
"round",
"trunc",
"sign",
"sqrt",
"cbrt",
"pow",
"hypot",
"log",
"log2",
"log10",
"exp",
])
export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNode): number => {
if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
const nums = args.map((arg) => {
if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node)
return arg
})
const [a = Number.NaN, b = Number.NaN] = nums
switch (name) {
case "max":
return Math.max(...nums)
case "min":
return Math.min(...nums)
case "abs":
return Math.abs(a)
case "floor":
return Math.floor(a)
case "ceil":
return Math.ceil(a)
case "round":
return Math.round(a)
case "trunc":
return Math.trunc(a)
case "sign":
return Math.sign(a)
case "sqrt":
return Math.sqrt(a)
case "cbrt":
return Math.cbrt(a)
case "pow":
return Math.pow(a, b)
case "hypot":
return Math.hypot(...nums)
case "log":
return Math.log(a)
case "log2":
return Math.log2(a)
case "log10":
return Math.log10(a)
case "exp":
return Math.exp(a)
}
throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
}
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"

View file

@ -0,0 +1,66 @@
export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString"])
export const numberConstants = new Set(["MAX_SAFE_INTEGER", "MIN_SAFE_INTEGER", "MAX_VALUE", "MIN_VALUE", "EPSILON"])
export const numberStatics = new Set(["isInteger", "isFinite", "isNaN", "isSafeInteger", "parseInt", "parseFloat"])
export const invokeNumberMethod = (value: number, name: string, args: Array<unknown>, node: AstNode): unknown => {
const optNum = (index: number): number | undefined => {
const arg = args[index]
if (arg === undefined) return undefined
if (typeof arg !== "number") throw new InterpreterRuntimeError(`Number.${name} expects a number argument.`, node)
return arg
}
let result: unknown
switch (name) {
case "toFixed":
result = value.toFixed(optNum(0))
break
case "toExponential":
result = value.toExponential(optNum(0))
break
case "toPrecision": {
const digits = optNum(0)
result = digits === undefined ? value.toString() : value.toPrecision(digits)
break
}
case "toString": {
const radix = optNum(0)
if (radix !== undefined && (radix < 2 || radix > 36)) {
throw new InterpreterRuntimeError("Number.toString radix must be between 2 and 36.", node)
}
result = value.toString(radix)
break
}
default:
throw new InterpreterRuntimeError(`Number method '${name}' is not available in CodeMode.`, node)
}
return boundedData(result, `Number.${name} result`)
}
export const invokeNumberStatic = (name: string, args: Array<unknown>, node: AstNode): unknown => {
const value = args[0]
switch (name) {
case "isInteger":
return Number.isInteger(value)
case "isFinite":
return Number.isFinite(value)
case "isNaN":
return Number.isNaN(value)
case "isSafeInteger":
return Number.isSafeInteger(value)
case "parseInt": {
const radix = args[1]
if (radix !== undefined && typeof radix !== "number") {
throw new InterpreterRuntimeError("Number.parseInt expects a numeric radix.", node)
}
return parseInt(coerceToString(value), radix)
}
case "parseFloat":
return parseFloat(coerceToString(value))
default:
throw new InterpreterRuntimeError(`Number.${name} is not available in CodeMode.`, node)
}
}
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { boundedData, coerceToString } from "./value.js"

View file

@ -0,0 +1,77 @@
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { isBlockedMember } from "../tool-runtime.js"
import { isSandboxValue, SandboxMap, SandboxURLSearchParams } from "../values.js"
import { boundedData, coerceToString } from "./value.js"
export const objectStatics = new Set(["keys", "values", "entries", "hasOwn", "assign", "fromEntries"])
export const invokeObjectMethod = (name: string, args: Array<unknown>, node: AstNode): unknown => {
if (!objectStatics.has(name)) throw new InterpreterRuntimeError(`Object.${name} is not available in CodeMode.`, node)
const requireObject = (): Record<string, unknown> => {
const value = boundedData(args[0], `Object.${name} input`)
if (isSandboxValue(value)) return {}
if (value === null || typeof value !== "object" || Array.isArray(value)) {
throw new InterpreterRuntimeError(`Object.${name} expects a data object.`, node)
}
return value as Record<string, unknown>
}
const guardedSet = (out: Record<string, unknown>, key: string, item: unknown): void => {
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, node)
out[key] = item
}
switch (name) {
case "keys": {
const value = boundedData(args[0], "Object.keys input")
if (isSandboxValue(value)) return []
if (Array.isArray(value)) return Object.keys(value)
if (value === null || typeof value !== "object") {
throw new InterpreterRuntimeError("Object.keys expects a data object or array.", node)
}
return Object.keys(value)
}
case "values":
return Object.values(requireObject())
case "entries":
return Object.entries(requireObject()).map(([key, item]) => [key, item])
case "hasOwn":
return Object.hasOwn(requireObject(), String(args[1]))
case "assign": {
const out: Record<string, unknown> = Object.create(null)
for (const source of args) {
if (source === null || source === undefined) continue
const value = boundedData(source, "Object.assign input")
if (isSandboxValue(value)) continue
if (value === null || typeof value !== "object" || Array.isArray(value)) {
throw new InterpreterRuntimeError("Object.assign expects data objects.", node)
}
for (const [key, item] of Object.entries(value)) guardedSet(out, key, item)
}
return out
}
case "fromEntries": {
if (args[0] instanceof SandboxMap) {
const out: Record<string, unknown> = Object.create(null)
for (const [key, item] of args[0].map.entries()) guardedSet(out, coerceToString(key), item)
return out
}
if (args[0] instanceof SandboxURLSearchParams) {
const out: Record<string, unknown> = Object.create(null)
for (const [key, value] of args[0].params.entries()) guardedSet(out, key, value)
return out
}
const pairs = boundedData(args[0], "Object.fromEntries input")
if (!Array.isArray(pairs)) {
throw new InterpreterRuntimeError("Object.fromEntries expects an array of [key, value] pairs.", node)
}
const out: Record<string, unknown> = Object.create(null)
for (const pair of pairs) {
if (!Array.isArray(pair)) {
throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] pairs.", node)
}
guardedSet(out, String(pair[0]), pair[1])
}
return out
}
}
throw new InterpreterRuntimeError(`Object.${name} is not available in CodeMode.`, node)
}

View file

@ -0,0 +1,6 @@
import type { PromiseMethodName } from "../interpreter/model.js"
export const promiseStatics = new Set<PromiseMethodName>(["all", "allSettled", "race", "resolve", "reject"])
/** Maximum number of eagerly forked tool calls that may run concurrently. */
export const TOOL_CALL_CONCURRENCY = 8

View file

@ -0,0 +1,74 @@
export const regexpMethods = new Set(["test", "exec", "toString"])
export const regexpProperties = new Set([
"source",
"flags",
"lastIndex",
"global",
"ignoreCase",
"multiline",
"sticky",
"unicode",
"dotAll",
])
export const regexFailureReason = (error: unknown): string =>
(error instanceof Error ? error.message : String(error)).replace(/^Invalid regular expression:\s*/i, "")
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 (typeof arg === "string") {
try {
return new RegExp(arg, extraFlags)
} catch (error) {
throw new InterpreterRuntimeError(
`String.${method} received the string ${JSON.stringify(arg)}, which is not a valid regular expression pattern (${regexFailureReason(error)}). ${escapeRegexHint}`,
node,
).as("SyntaxError")
}
}
throw new InterpreterRuntimeError(
`String.${method} expects a regular expression (a /pattern/flags literal or new RegExp(...)) or a string pattern, not ${arg === null ? "null" : typeof arg}.`,
node,
)
}
export const matchToValue = (match: RegExpMatchArray): Array<unknown> => {
const result: Array<unknown> = Array.from(match, (group) => group)
if (match.index !== undefined) (result as Record<string, unknown> & Array<unknown>).index = match.index
if (match.groups) {
const groups: SafeObject = Object.create(null) as SafeObject
for (const [key, group] of Object.entries(match.groups)) {
if (!isBlockedMember(key)) groups[key] = group
}
;(result as Record<string, unknown> & Array<unknown>).groups = groups
}
return result
}
export const invokeRegExpMethod = (
value: SandboxRegExp,
name: string,
args: Array<unknown>,
node: AstNode,
): unknown => {
switch (name) {
case "test":
return value.regex.test(coerceToString(args[0]))
case "exec": {
const matched = value.regex.exec(coerceToString(args[0]))
return matched === null ? null : matchToValue(matched)
}
case "toString":
return coerceToString(value)
default:
throw new InterpreterRuntimeError(`RegExp method '${name}' is not available in CodeMode.`, node)
}
}
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { isBlockedMember, type SafeObject } from "../tool-runtime.js"
import { SandboxRegExp } from "../values.js"
import { coerceToString } from "./value.js"

View file

@ -0,0 +1,52 @@
export const stringMethods = new Set([
"toLowerCase",
"toUpperCase",
"trim",
"trimStart",
"trimEnd",
"trimLeft",
"trimRight",
"split",
"slice",
"substring",
"substr",
"includes",
"startsWith",
"endsWith",
"indexOf",
"lastIndexOf",
"replace",
"replaceAll",
"repeat",
"padStart",
"padEnd",
"charAt",
"charCodeAt",
"codePointAt",
"at",
"concat",
"toString",
"match",
"matchAll",
"search",
"localeCompare",
"normalize",
])
export const stringStatics = new Set(["fromCharCode", "fromCodePoint"])
export const invokeStringStatic = (name: string, args: Array<unknown>, node: AstNode): unknown => {
const codes = args.map((arg) => {
if (typeof arg !== "number") throw new InterpreterRuntimeError(`String.${name} expects number arguments.`, node)
return arg
})
switch (name) {
case "fromCharCode":
return String.fromCharCode(...codes)
case "fromCodePoint":
return String.fromCodePoint(...codes)
default:
throw new InterpreterRuntimeError(`String.${name} is not available in CodeMode.`, node)
}
}
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"

View file

@ -0,0 +1,90 @@
export const urlProperties = new Set([
"href",
"origin",
"protocol",
"username",
"password",
"host",
"hostname",
"port",
"pathname",
"search",
"hash",
])
export const urlWritableProperties = new Set([
"href",
"protocol",
"username",
"password",
"host",
"hostname",
"port",
"pathname",
"search",
"hash",
])
export const urlMethods = new Set(["toString", "toJSON"])
export const urlStatics = new Set(["canParse", "parse"])
export const urlSearchParamsMethods = new Set([
"append",
"delete",
"get",
"getAll",
"has",
"set",
"sort",
"forEach",
"keys",
"values",
"entries",
"toString",
])
export const uriArgument = (value: unknown, label: string): string => coerceToString(boundedData(value, label))
export const invokeUriFunction = (ref: UriFunction, args: Array<unknown>, node: AstNode): string => {
const value = uriArgument(args[0], `${ref.name} input`)
try {
switch (ref.name) {
case "encodeURI":
return encodeURI(value)
case "encodeURIComponent":
return encodeURIComponent(value)
case "decodeURI":
return decodeURI(value)
case "decodeURIComponent":
return decodeURIComponent(value)
}
} catch (error) {
throw new InterpreterRuntimeError(
`${ref.name} received malformed URI data: ${error instanceof Error ? error.message : String(error)}`,
node,
).as("URIError")
}
}
export const urlArgument = (value: unknown, label: string): string =>
value instanceof SandboxURL ? value.url.href : uriArgument(value, label)
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 (args.length === 0) throw new InterpreterRuntimeError(`URL.${name} requires a URL argument.`, node).as("TypeError")
const input = urlArgument(args[0], `URL.${name} input`)
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)
} catch {
return name === "canParse" ? false : null
}
}
export const invokeURLMethod = (value: SandboxURL, 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 { boundedData, coerceToString } from "./value.js"

View file

@ -0,0 +1,90 @@
export const errorConstructors = new Set([
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"EvalError",
"URIError",
])
export const valueConstructors = new Set(["Date", "RegExp", "Map", "Set", "URL", "URLSearchParams"])
export const compoundOperators = new Set(["+=", "-=", "*=", "/=", "%=", "**=", "&=", "|=", "^=", "<<=", ">>=", ">>>="])
const ErrorBrand: unique symbol = Symbol("codemode.error")
export const createErrorValue = (name: string, message: string): SafeObject => {
const value = Object.assign(Object.create(null) as SafeObject, { name, message })
Object.defineProperty(value, ErrorBrand, { value: name })
return value
}
export const errorBrandName = (value: unknown): string | undefined =>
value !== null && typeof value === "object"
? ((value as Record<PropertyKey, unknown>)[ErrorBrand] as string | undefined)
: undefined
export const boundedData = (value: unknown, label: string): unknown => copyIn(value, label, true)
export const coerceToString = (value: unknown): string => {
if (value === null) return "null"
if (value === undefined) return "undefined"
if (value instanceof SandboxDate)
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 (typeof value === "object") {
return Array.isArray(value)
? value.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",")
: "[object Object]"
}
return String(value)
}
export const coerceToNumber = (value: unknown): number => {
if (value instanceof SandboxDate) return value.time
if (isSandboxValue(value)) return Number.NaN
return value !== null && typeof value === "object" && !Array.isArray(value) ? Number.NaN : Number(value)
}
export const invokeCoercion = (ref: CoercionFunction, args: Array<unknown>, node: AstNode): unknown => {
const raw = args[0]
if (isSandboxValue(raw)) {
if (ref.name === "Boolean") return true
if (ref.name === "Number") return coerceToNumber(raw)
if (ref.name === "String") return coerceToString(raw)
if (ref.name === "parseInt") return parseInt(coerceToString(raw))
return parseFloat(coerceToString(raw))
}
const value = boundedData(args[0], `${ref.name} input`)
if (ref.name === "Number") return coerceToNumber(value)
if (ref.name === "Boolean") return Boolean(value)
if (ref.name === "parseInt") {
const radix = args[1]
if (radix !== undefined && typeof radix !== "number") {
throw new InterpreterRuntimeError("parseInt expects a numeric radix.", node)
}
return parseInt(coerceToString(value), radix)
}
if (ref.name === "parseFloat") return parseFloat(coerceToString(value))
return coerceToString(value)
}
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,
} from "../values.js"