UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

814 lines (784 loc) • 38.5 kB
import { n as parseCodeModeScriptSyntax } from "./code-mode-script-syntax-DZwdESO8.mjs"; import { n as ToolInputError } from "./tool-input-error-mjW74R8m.mjs"; import { Script } from "node:vm"; //#region src/agents/code-mode-console-source.ts /** Console is guest-only diagnostic text, not a host logging capability. */ const CODE_MODE_CONSOLE_SOURCE = String.raw` // The counter lives in the VM snapshot. JSON code units are a conservative // bound (at most three UTF-8 bytes each), including empty-message overhead. let consoleUnits = 0; const consoleLimit = 16_384; const consoleMarker = "[console output truncated]"; function consoleWrite(level, args) { if (consoleUnits >= consoleLimit) return; let nodes = 100; const seen = new Set(); const clip = (value, limit) => { if (value.length <= limit) return value; // Do not split a surrogate pair at a diagnostic boundary. let end = limit; const last = value.charCodeAt(end - 1); if (last >= 0xd800 && last <= 0xdbff) end--; return value.slice(0, end) + "…[truncated]"; }; const inspect = (value, depth = 0) => { if (--nodes < 0) return "[truncated]"; if (typeof value === "string") return clip(value, 512); if (value === null || typeof value === "boolean" || typeof value === "number") return value; if (typeof value === "undefined") return "undefined"; if (typeof value === "bigint") return clip(String(value), 512) + "n"; if (typeof value === "symbol") return clip(String(value), 512); if (typeof value === "function") return "[Function]"; if (seen.has(value)) return "[Circular]"; if (depth >= 4) return "[truncated]"; seen.add(value); try { if (value instanceof GuestPromise) return promiseOutput; const array = Array.isArray(value); const result = array ? [] : Object.create(null); let count = 0; for (const key in value) { if (!Object.prototype.hasOwnProperty.call(value, key)) continue; if (count++ >= 50 || nodes <= 0) { if (array) result.push("[truncated]"); else result["…"] = "[truncated]"; break; } const descriptor = Object.getOwnPropertyDescriptor(value, key); const item = descriptor && "value" in descriptor ? inspect(descriptor.value, depth + 1) : "[Accessor]"; if (array) result.push(item); else result[clip(key, 512)] = item; } if (value instanceof Error) { const message = Object.getOwnPropertyDescriptor(value, "message"); result.message = message && "value" in message ? inspect(message.value, depth + 1) : "[Accessor]"; } return result; } finally { seen.delete(value); } }; let message = level === "log" ? "" : "[" + level + "] "; for (let i = 0; i < args.length; i++) { if (i > 0) message += " "; if (nodes <= 0 || message.length >= 4096) { message += "[truncated]"; break; } try { const value = inspect(args[i]); message += typeof value === "string" ? value : JSON.stringify(value); } catch { message += "[Unserializable]"; } } const entry = { type: "text", text: clip(message, 4096) }; const units = JSON.stringify(entry).length; if (consoleUnits + units > consoleLimit - 100) { emitOutput({ type: "text", text: consoleMarker }); consoleUnits = consoleLimit; return; } consoleUnits += units; emitOutput(entry); } const guestConsole = Object.freeze(Object.fromEntries( ["log", "info", "warn", "error", "debug"].map((level) => [level, (...args) => consoleWrite(level, args)]), )); `; //#endregion //#region src/agents/code-mode-swarm-controller-source.ts /** Guest-side Swarm helpers injected into the Code Mode controller. */ const CODE_MODE_SWARM_CONTROLLER_SOURCE = String.raw` class SwarmAgentError extends Error { constructor(runId, status, detail) { super("Swarm agent " + runId + " " + status + ": " + detail); this.name = "SwarmAgentError"; this.runId = runId; this.status = status; } } function swarmNote(kind, value) { if (typeof value !== "string" || !value.trim()) { throw new TypeError(kind + " note must be a non-empty string"); } void request("swarmNote", [{ kind, text: value }], { queue: true }).catch(() => {}); } async function runAgent(prompt, options = {}) { if (typeof prompt !== "string" || !prompt.trim()) { throw new TypeError("agents.run prompt must be a non-empty string"); } if (options === null || typeof options !== "object" || Array.isArray(options)) { throw new TypeError("agents.run options must be an object"); } if (options.phase !== undefined && (typeof options.phase !== "string" || !options.phase.trim())) { throw new TypeError("agents.run phase must be a non-empty string"); } // Match the submitted contract even when callers reuse options while the child runs. const structured = options.schema !== undefined; if (options.phase !== undefined) swarmNote("phase", options.phase); const spawned = await request("agentSpawn", [prompt, options], { queue: true }); const completion = await request("agentWait", [spawned.runId], { queue: true }); if (!completion || completion.status !== "done") { const runId = completion?.runId ?? spawned.runId ?? "unknown"; const status = completion?.status ?? "failed"; const detail = [completion?.error, completion?.schemaError, completion?.result].find( (value) => typeof value === "string" && value.trim() ) || "collector returned no result"; throw new SwarmAgentError(runId, status, detail); } return structured ? completion.structured : completion.result; } `; //#endregion //#region src/agents/code-mode-controller-source.ts /** Guest globals and host bridge shared by Code Mode JavaScript executors. */ const CODE_MODE_CONTROLLER_SOURCE = String.raw` (() => { const output = []; const pending = new Map(); const queued = new Map(); // Only ordinary requests use this quota. Swarm retains its group/VM bounds // while sharing the same ordered queue and in-flight slots. const maxQueuedOrdinary = ${128}; let queuedOrdinaryCount = 0; let admissionError; const maxPending = globalThis.__openclawMaxPendingToolCalls; delete globalThis.__openclawMaxPendingToolCalls; const catalogBindings = Array.isArray(globalThis.__openclawCatalog) ? globalThis.__openclawCatalog : []; const apiFiles = Array.isArray(globalThis.__openclawApiFiles) ? globalThis.__openclawApiFiles : []; const namespaceDescriptors = Array.isArray(globalThis.__openclawNamespaces) ? globalThis.__openclawNamespaces : []; const hostRequest = globalThis.__openclawHostRequest; const hostCancelRequest = globalThis.__openclawHostCancelRequest; const hostObserveNetworkContent = globalThis.__openclawHostObserveNetworkContent; const hostOutput = globalThis.__openclawHostOutput; delete globalThis.__openclawHostOutput; delete globalThis.__openclawHostObserveNetworkContent; delete globalThis.__openclawHostRequest; delete globalThis.__openclawHostCancelRequest; delete globalThis.__openclawCatalog; delete globalThis.__openclawApiFiles; delete globalThis.__openclawNamespaces; const bridgeSequences = new Map(); const timers = new Map(); // Keep rejection ownership in the snapshot so a handler attached after wait // can clear it; an unawaited failure must not become a successful cell. const unhandledRejections = new Set(); const bridgeErrors = new WeakSet(); // Guest prototype changes must not replace the operations that own error provenance. const rememberBridgeError = bridgeErrors.add.bind(bridgeErrors); const isBridgeError = bridgeErrors.has.bind(bridgeErrors); let nextTimerId = 0; const GuestPromise = Promise; const GuestError = Error; const GuestTypeError = TypeError; const GuestRangeError = RangeError; const stringifyJson = JSON.stringify; function emitOutput(entry) { const count = output.push(entry); if (hostOutput) hostOutput(encodeFinalValue(entry)); return count; } const promiseOutput = "[Unawaited Promise: use await or Promise.all(...) before emitting or returning values.]"; let networkContentObserved = false; function observeNetworkContent() { if (networkContentObserved) return; networkContentObserved = true; hostObserveNetworkContent(); } ${CODE_MODE_CONSOLE_SOURCE} function safe(value, diagnosePromises = false) { if (value === undefined) return null; try { return JSON.parse(JSON.stringify(diagnosePromises ? serializeOutputValue(value) : value)); } catch { if (value instanceof Error) { return { name: value.name, message: value.message }; } if (value === null) return null; const type = typeof value; if (type === "string" || type === "number" || type === "boolean") return value; return String(value); } } function asText(value) { if (typeof value === "string") return value; const encoded = JSON.stringify(safe(value, true)); return typeof encoded === "string" ? encoded : String(value); } function assertQueueCapacity(queue) { if (!admissionError && !queue && pending.size >= maxPending && queuedOrdinaryCount >= maxQueuedOrdinary) { admissionError = "code mode bridge queue limit exceeded (" + maxQueuedOrdinary + " ordinary requests queued; " + maxPending + " in-flight slots). Await smaller batches before creating more tool calls or timers."; } // Sticky even if guest code catches the immediate error: the worker refuses // this entire synchronous frontier before dispatching any admitted prefix. if (admissionError) throw new Error(admissionError); } function beginRequest(method, args, { queue = false } = {}) { assertQueueCapacity(queue); const methodName = String(method); const sequence = (bridgeSequences.get(methodName) ?? 0) + 1; bridgeSequences.set(methodName, sequence); const id = "bridge:" + methodName + ":" + String(sequence); const argsJson = stringifyJson(args ?? []); // Guest toJSON/getters can create requests while serializing this input. assertQueueCapacity(queue); const callStack = new GuestError().stack; let callbacks; const promise = new Promise((resolve, reject) => { callbacks = { resolve, reject }; }); const admit = () => { callbacks.stack = callStack; callbacks.location = hostRequest(methodName, argsJson, id, callStack); pending.set(id, callbacks); }; if (queue || pending.size >= maxPending) { queued.set(id, { admit, callbacks, ordinary: !queue }); if (!queue) queuedOrdinaryCount++; } else admit(); return { id, promise }; } // Admission and cancellation release capacity through the same owner. function takeQueuedRequest(id) { const entry = queued.get(id); if (!entry) return; queued.delete(id); if (entry.ordinary) queuedOrdinaryCount--; return entry; } // Refill after guest continuations run so dependent ordinary calls retain // first use of released slots. Waiting requests refill in insertion order. // Closures, copied inputs, and stable IDs live only in the bounded VM snapshot. function drainQueuedRequests() { while (queued.size > 0 && pending.size < maxPending) { const id = queued.keys().next().value; takeQueuedRequest(id).admit(); } } function request(method, args, options) { return beginRequest(method, args, options).promise; } function scheduleTimer(callback, delay, args) { if (typeof callback !== "function") { throw new TypeError("setTimeout callback must be a function"); } const numericDelay = Number(delay); const delayMs = Number.isFinite(numericDelay) ? Math.max(0, Math.floor(numericDelay)) : 0; const timerId = ++nextTimerId; const timerRequest = beginRequest("sleep", [delayMs]); timers.set(timerId, timerRequest.id); void timerRequest.promise.then(() => { if (!timers.delete(timerId)) return; callback(...args); }); return timerId; } function cancelTimer(timerId) { const requestId = timers.get(Number(timerId)); if (!requestId) return; timers.delete(Number(timerId)); const queuedEntry = takeQueuedRequest(requestId); if (queuedEntry) { queuedEntry.callbacks.resolve(null); return; } hostCancelRequest(requestId); const entry = pending.get(requestId); if (!entry) return; pending.delete(requestId); entry.resolve(null); } ${CODE_MODE_SWARM_CONTROLLER_SOURCE} function namespaceFunction(namespaceId, path) { const callablePath = Object.freeze((Array.isArray(path) ? path : []).map((entry) => String(entry))); return (...args) => request("namespace", [namespaceId, callablePath, args]); } function deserializeNamespaceValue(namespaceId, value) { if (!value || typeof value !== "object") return null; if (value.kind === "function") { return namespaceFunction(namespaceId, Array.isArray(value.path) ? value.path.slice() : []); } if (value.kind === "array") { return Object.freeze((Array.isArray(value.items) ? value.items : []).map((item) => deserializeNamespaceValue(namespaceId, item))); } if (value.kind === "object") { const object = Object.create(null); for (const entry of Array.isArray(value.entries) ? value.entries : []) { const key = Array.isArray(entry) && typeof entry[0] === "string" ? entry[0] : ""; if (!key) continue; const projected = deserializeNamespaceValue(namespaceId, entry[1]); Object.defineProperty(object, key, namespaceId === "mcp" && entry[1]?.kind === "value" ? { get: () => { observeNetworkContent(); return projected; }, enumerable: true } : { value: projected, enumerable: true }); } return Object.freeze(object); } return safe(value.value); } function settle(id, ok, payload) { const entry = pending.get(String(id)); if (!entry) return false; pending.delete(String(id)); let parsed = null; try { parsed = JSON.parse(String(payload)); } catch { parsed = String(payload); } if (ok) { entry.resolve(parsed); } else { const error = new GuestError(typeof parsed === "string" ? parsed : parsed?.message ?? "nested tool failed"); if (entry.stack) Object.defineProperty(error, "stack", { value: entry.stack, writable: true, configurable: true }); if (entry.location) error.location = entry.location; if (parsed && typeof parsed === "object") { error.code = parsed.code; error.effectStatus = "unknown"; } rememberBridgeError(error); entry.reject(error); } return true; } function nodeHandle(descriptor) { const handle = Object.create(null); Object.defineProperties(handle, { id: { value: descriptor.id, enumerable: true }, name: { value: descriptor.name, enumerable: true }, invoke: { value: (command, params) => request("nodes", ["invoke", descriptor.id, command, params]), enumerable: true, }, }); if (typeof descriptor.listDirCommand === "string") { Object.defineProperty(handle, "listDir", { value: (path) => request("nodes", ["invoke", descriptor.id, descriptor.listDirCommand, { path }]), enumerable: true, }); } return Object.freeze(handle); } const nodes = Object.freeze({ list: () => request("nodes", ["list"]), get: async (idOrName) => nodeHandle(await request("nodes", ["get", idOrName])), }); const skills = Object.freeze({ list: (offset = 0) => request("skillsList", [offset]), search: (query, limit) => request("skillsSearch", limit === undefined ? [query] : [query, limit]), read: (name) => request("skillsRead", [name]), }); const results = Object.freeze({ save: (value) => request("resultSave", [value]), load: (id) => request("resultLoad", [id]), delete: (id) => request("resultDelete", [id]), }); // Session operations use the existing result bridge; buffers stay with the host cell. async function sessionResult(method, key, value) { if (typeof key !== "string") throw new GuestTypeError("Code Mode store key must be a string."); try { const result = await request(method, method === "resultSave" ? [value, "session", key] : [key, "session"]); return method === "resultLoad" ? result.value : undefined; } catch (error) { if (!(error instanceof GuestError)) throw error; const Constructor = error.code === "store_range" ? GuestRangeError : error.code === "store_type" ? GuestTypeError : undefined; if (!Constructor) throw error; const typed = new Constructor(error.message); typed.stack = error.stack; rememberBridgeError(typed); throw typed; } } if (globalThis.__openclawSwarmEnabled === true) { Object.defineProperties(globalThis, { agents: { value: Object.freeze({ run: runAgent }), enumerable: true, }, phase: { value: (title) => swarmNote("phase", title), enumerable: true }, log: { value: (message) => swarmNote("log", message), enumerable: true }, }); } function normalizeApiPath(value) { const text = String(value ?? "").trim().replace(/^\/+/, ""); if (!text || text.split("/").some((segment) => !segment || segment === "." || segment === "..")) { throw new Error("invalid API file path"); } return text; } const apiFileMap = new Map(); for (const file of apiFiles) { if (!file || typeof file !== "object") continue; const path = typeof file.path === "string" ? file.path : ""; const content = typeof file.content === "string" ? file.content : ""; if (!path || !content) continue; apiFileMap.set(path, Object.freeze({ path, content, description: typeof file.description === "string" ? file.description : undefined, bytes: file.bytes, })); } // Native declarations are generated by describe only when read, not injected // for every tool into every VM. Listing needs only the bounded callable metadata. const nativeApiFiles = new Map(catalogBindings.map(binding => [ "tools/" + binding.callableName + ".d.ts", binding.callableName, ])); const api = Object.freeze({ list: async (prefix = "") => { // list takes a directory prefix, so tolerate a trailing slash (API.list("mcp/")) // that read's exact-path normalizer would otherwise reject as an empty segment. const rawPrefix = prefix == null ? "" : String(prefix).trim().replace(/\/+$/, ""); const normalizedPrefix = rawPrefix === "" ? "" : normalizeApiPath(rawPrefix); const files = [...apiFileMap.values(), ...[...nativeApiFiles.keys()].map(path => ({ path }))] .filter((file) => !normalizedPrefix || file.path === normalizedPrefix || file.path.startsWith(normalizedPrefix.replace(/\/?$/, "/"))) .map((file) => Object.freeze({ path: file.path, description: file.description, bytes: file.bytes, })); if (files.some(file => file.path.startsWith("mcp/"))) observeNetworkContent(); return { files }; }, read: async (path) => { const normalizedPath = normalizeApiPath(path); const file = apiFileMap.get(normalizedPath); if (file) { if (normalizedPath.startsWith("mcp/")) observeNetworkContent(); return file; } const callableName = nativeApiFiles.get(normalizedPath); if (callableName) return request("describe", [callableName, "declaration"]); throw new Error("Unknown API file: " + normalizedPath); }, }); const callableHandles = new Map(); const callableMetadata = new WeakMap(); function callableHandle(binding) { const callableName = typeof binding?.callableName === "string" ? binding.callableName : ""; if (!callableName) return null; const existing = callableHandles.get(callableName); if (existing) return existing; const isMcp = binding.source === "mcp"; const path = isMcp ? Object.freeze(binding.path.slice()) : undefined; const handle = isMcp ? namespaceFunction(binding.namespaceId, path) : (input) => request("callValue", [callableName, input]); const metadata = Object.freeze({ callableName, toolName: typeof binding.name === "string" ? binding.name : callableName, label: typeof binding.label === "string" ? binding.label : undefined, description: typeof binding.description === "string" ? binding.description : "", source: binding.source, input: binding.input, output: binding.output, ...(isMcp ? { apiPath: binding.apiPath } : {}), }); for (const [key, value] of Object.entries(metadata)) { Object.defineProperty(handle, key, binding.source !== "openclaw" ? { get: () => { observeNetworkContent(); return value; }, enumerable: true } : { value, enumerable: true }); } Object.defineProperties(handle, { name: { value: callableName }, describe: { value: isMcp ? () => request("namespace", [binding.namespaceId, [path[0], "$api"], [path.slice(1).join("."), { schema: true }]]) : () => request("describe", [callableName]), enumerable: true, }, toJSON: { value: () => { if (binding.source !== "openclaw") observeNetworkContent(); return metadata; } }, }); const frozen = Object.freeze(handle); callableHandles.set(callableName, frozen); callableMetadata.set(frozen, metadata); return frozen; } // Project nested catalog handles before ordinary JSON conversion drops functions. function serializeOutputValue(value, seen = new Map(), finalState) { if (value instanceof GuestPromise) return promiseOutput; const metadata = callableMetadata.get(value); if (metadata) { if (metadata.source !== "openclaw") observeNetworkContent(); return finalState ? serializeOutputValue(metadata, seen, finalState) : metadata; } if (finalState) { if (typeof value === "function") return undefined; if (typeof value === "bigint") finalState.hasBigInt = true; } if (value === null || typeof value !== "object") return value; if (seen.has(value)) return seen.get(value); const proto = Object.getPrototypeOf(value); const isError = value instanceof Error; if (!finalState && !isError && !Array.isArray(value) && proto !== Object.prototype && proto !== null) return value; const plain = Array.isArray(value) ? new Array(value.length) : isError ? { name: value.name, message: value.message } : {}; // Project before JSON.stringify can invoke Error.toJSON, and preserve graph // identity so cyclic custom fields use the ordinary bounded JSON fallback. seen.set(value, plain); if (finalState) { // Final conversion never invokes inherited toJSON hooks. Expanding sparse // arrays here keeps their allocation inside the guest's memory/time limits. Object.setPrototypeOf(plain, null); if (Array.isArray(value)) { for (let index = 0; index < plain.length; index++) { plain[index] = serializeOutputValue(value[index], seen, finalState); } return plain; } if (isError) { plain.name = serializeOutputValue(plain.name, seen, finalState); plain.message = serializeOutputValue(plain.message, seen, finalState); } } for (const key of Object.keys(value)) { if (isError && key === "toJSON") continue; Object.defineProperty(plain, key, { value: serializeOutputValue(value[key], seen, finalState), enumerable: true, configurable: true, writable: true, }); } return plain; } function encodeFinalValue(value) { const state = { hasBigInt: false }; const plain = serializeOutputValue(value, new Map(), state); if (typeof plain === "bigint") return stringifyJson("" + plain); const fallback = Array.isArray(plain) ? '"[object Array]"' : '"[object Object]"'; // Detect BigInts before stringify can invoke a guest BigInt.toJSON hook. if (state.hasBigInt) return fallback; try { return stringifyJson(plain) ?? "null"; } catch (error) { if (error instanceof GuestTypeError) return fallback; throw error; } } const catalog = Object.freeze({ search: async (query, options) => { const matches = await request("search", [query, options]); return Object.freeze(matches.map((match) => { const handle = typeof match === "string" ? callableHandles.get(match) : callableHandle(match); if (!handle) throw new Error("Search result has no callable handle."); return handle; })); }, all: () => Object.freeze(catalogBindings.map(binding => callableHandles.get(binding.callableName))), }); const namespaceGlobals = Object.create(null); for (const descriptor of namespaceDescriptors) { const id = typeof descriptor?.id === "string" ? descriptor.id : ""; const globalName = typeof descriptor?.globalName === "string" ? descriptor.globalName : ""; if (!id || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(globalName)) continue; const scope = deserializeNamespaceValue(id, descriptor.scope); Object.defineProperty(namespaceGlobals, globalName, { value: scope, enumerable: true, }); const existingGlobal = Object.getOwnPropertyDescriptor(globalThis, globalName); if (existingGlobal && existingGlobal.configurable === false) continue; Object.defineProperty(globalThis, globalName, { value: scope, enumerable: true, configurable: true, }); } for (const binding of catalogBindings) { const handle = callableHandle(binding); const callableName = typeof binding?.callableName === "string" ? binding.callableName : ""; if (!handle || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(callableName)) continue; Object.defineProperty(globalThis, callableName, { value: handle, enumerable: true, configurable: true, }); } Object.defineProperties(globalThis, { API: { value: api, enumerable: true }, catalog: { value: catalog, enumerable: true }, nodes: { value: nodes, enumerable: true }, namespaces: { value: Object.freeze(namespaceGlobals), enumerable: true }, skills: { value: skills, enumerable: true }, results: { value: results, enumerable: true }, store: { value: (key, value) => sessionResult(value === undefined ? "resultDelete" : "resultSave", key, value), enumerable: true }, load: { value: (key) => sessionResult("resultLoad", key), enumerable: true }, setTimeout: { value: (callback, delay, ...args) => scheduleTimer(callback, delay, args), enumerable: true }, clearTimeout: { value: cancelTimer, enumerable: true }, console: { value: guestConsole, enumerable: true }, text: { value: (value) => emitOutput({ type: "text", text: asText(value) }), enumerable: true }, json: { value: (value) => emitOutput({ type: "json", value: safe(value, true) }), enumerable: true }, yield_control: { value: (reason) => request("yield", [reason]), enumerable: true }, __openclawSettleBridge: { value: settle }, __openclawIsBridgeError: { value: isBridgeError }, __openclawDrainQueuedRequests: { value: drainQueuedRequests }, __openclawAdmissionError: { value: () => admissionError }, // Final getters must run before the worker drains output and settles host work. __openclawRunCell: { value: async (run) => encodeFinalValue(await run()) }, __openclawTakeOutputJson: { value: () => encodeFinalValue(output.splice(0)) }, __openclawTrackRejection: { value: (promise, _reason, handled) => { if (handled) unhandledRejections.delete(promise); else unhandledRejections.add(promise); }, }, __openclawUnhandledRejection: { value: () => unhandledRejections.values().next().value }, }); })(); `; //#endregion //#region src/agents/code-mode-source-location.ts const USER_SOURCE_FILE = "openclaw-code-mode:user.js"; const SOURCE_LOCATION_KEY = "__openclawSourceLocation"; function sourceExtent(source, columnUnits) { let lines = 1; let lastLineStart = 0; for (const match of source.matchAll(/\r\n|[\r\n\u2028\u2029]/gu)) { lines += 1; lastLineStart = match.index + match[0].length; } const lastLine = source.slice(lastLineStart); return { lines, lastColumn: (columnUnits === "utf8" ? Buffer.byteLength(lastLine, "utf8") : lastLine.length) + 1 }; } function normalizeSourceStack(stack, location) { if (!stack || !location) return stack; return stack.replace(/^[^\S\r\n]+at [^\r\n]*openclaw-code-mode:(?:user|controller)\.js:\d+:\d+\)?(?:\r?\n|$)/gmu, (frame) => { const match = /openclaw-code-mode:user\.js:(\d+):(\d+)(?=\)?(?:\r?\n)?$)/u.exec(frame); if (!match) return ""; const line = Number(match[1]) - location.lineOffset; const originalColumn = Number(match[2]); const column = originalColumn - (line === 1 ? location.columnOffset : 0); if (line < 1 || line > location.lineCount || column < 1 || line === location.lineCount && originalColumn > location.endColumn) return ""; return frame.replace(match[0], location.file + ":" + line + ":" + column); }); } function buildUserSource(code, prelude = "", columnUnits = "utf8") { const prefix = `globalThis.__openclawResult = __openclawRunCell(async () => {\n${prelude}`; const before = sourceExtent(prefix, columnUnits); const body = sourceExtent(code, columnUnits); const columnOffset = before.lastColumn - 1; return { source: `${prefix}${code}\n})`, location: { file: USER_SOURCE_FILE, lineOffset: before.lines - 1, lineCount: body.lines, columnOffset, endColumn: body.lastColumn + (body.lines === 1 ? columnOffset : 0) } }; } //#endregion //#region src/agents/code-mode-shell-source.ts const JAVASCRIPT_EXPORT = /^export\s+(?:(?:abstract|as|async|class|const|declare|default|enum|function|import|interface|let|namespace|type|var)\b|[={*])/u; const JAVASCRIPT_KEYWORD = /^(?:abstract|as|async|await|break|case|catch|class|const|continue|debugger|declare|default|delete|do|else|enum|export|extends|false|finally|for|function|if|implements|import|in|instanceof|interface|let|namespace|new|null|of|private|protected|public|return|satisfies|static|super|switch|this|throw|true|try|typeof|undefined|var|void|while|with|yield)$/u; const JAVASCRIPT_GLOBAL = /^(?:API|MCP|AggregateError|Array|ArrayBuffer|BigInt|BigInt64Array|BigUint64Array|Boolean|DataView|Date|Error|EvalError|Float32Array|Float64Array|Function|Infinity|Int16Array|Int32Array|Int8Array|Intl|JSON|Map|Math|NaN|Number|Object|Promise|Proxy|RangeError|ReferenceError|Reflect|RegExp|Set|String|Symbol|SyntaxError|TypeError|URIError|Uint16Array|Uint32Array|Uint8Array|Uint8ClampedArray|WeakMap|WeakSet|catalog|clearTimeout|console|decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|eval|globalThis|isFinite|isNaN|json|nodes|parseFloat|parseInt|setTimeout|skills|text|yield_control)$/u; const SHELL_COMMAND = /^(?:\/(?:usr\/(?:local\/)?)?bin\/)?(alias|apt|awk|bash|bg|brew|builtin|bun|cargo|cat|cd|chmod|cmd|command|cp|curl|cut|date|declare|df|dir|docker|dotnet|du|echo|env|exec|exit|export|fg|file|find|getopts|git|go|gradle|grep|hash|head|help|hostname|id|java|javac|jobs|jq|kill|kubectl|ln|local|logout|ls|make|mkdir|mvn|mv|node|npm|npx|perl|php|pip|pip3|pnpm|poetry|popd|powershell|printf|ps|pushd|pwd|pwsh|pytest|python|python3|read|readonly|rg|rm|ruby|rustc|rustup|sed|set|sh|shift|sleep|sort|source|stat|sudo|swift|systemctl|tail|tar|tee|test|touch|trap|tree|type|ulimit|umask|uname|uniq|unset|unzip|uv|uvx|vitest|wait|wc|wget|which|whoami|xargs|yarn|zip|zsh)(?=$|[\s;&|<>])/u; const SHELL_IDENTIFIER = /^([A-Za-z_][\w-]*)(?=$|[\s;&|<>])/u; const SHELL_EXECUTABLE_PATH = /^(?:(?:\.{1,2}|~)[\\/]|\/|[A-Za-z]:[\\/])[^\s;|&()]+(?=$|[\t \r\n;&|])/u; const SHELL_ARGUMENT = /^(?:-{1,2}[a-z\d][\w-]*(?:[\t =;&|]|$)|(?:(?:\.{1,2}|~)[\\/]|\/|[A-Za-z]:[\\/])[^\s]+)/iu; const SHELL_CONTROL = /^(?:(?:if|elif|while|until)[\t ]+(?:\[{1,2}(?=[\t ]|$)|test\b|[A-Za-z_][\w-]*(?=[\t ;]))|for[\t ]+(?:[A-Za-z_]\w*[\t ]+in\b|\(\([^\r\n]*\)\)[\t ]*;[\t ]*do\b)|case[\t ]+\S+[\t ]+in\b|function[\t ]+[A-Za-z_][\w-]*[\t ]*\{)/u; const SHELL_REDIRECTION = /^(?:\d*(?:>>?|<<?)|&>>?)/u; const LEADING_SOURCE_COMMENTS = /^(?:(?:\/\/[^\r\n]*(?:\r?\n|$)|\/\*[\s\S]*?\*\/|#[^\r\n]*(?:\r?\n|$))[\t \r\n]*)+/u; function parsesAsGuestJavaScript(source, declaration = "") { try { return new Script(`(async () => {\n${declaration}${source}\n})`) instanceof Script; } catch { return false; } } function hasHoistedGuestBinding(source, name) { return !parsesAsGuestJavaScript(source, `let ${name};\n`) && parsesAsGuestJavaScript(source, `var ${name};\n`); } function stripShellEnvAssignments(source) { const name = /[A-Za-z_]\w*=/uy; if (!name.test(source)) return source; const value = new Uint32Array(source.length + 3); const doubleQuoted = new Uint32Array(source.length + 3); const singleQuoted = new Uint32Array(source.length + 3); let blanksEnd = 0; let newlineEnd = 0; for (let index = source.length - 1; index >= name.lastIndex; index -= 1) { const character = source[index]; const blank = character === " " || character === " "; const whitespace = /\s/u.test(character); const separator = blank ? blanksEnd || newlineEnd : character === "\n" ? blanksEnd : character === "\r" && source[index + 1] === "\n" ? newlineEnd : 0; if (!blank) { blanksEnd = whitespace ? 0 : index; newlineEnd = separator; } const escaped = character === "\\" && index + 1 < source.length && !/[\r\n\u2028\u2029]/u.test(source[index + 1]); const afterEscape = index + (escaped && source.codePointAt(index + 1) > 65535 ? 3 : 2); doubleQuoted[index] = (escaped ? doubleQuoted[afterEscape] : 0) || (character === "\"" ? value[index + 1] : doubleQuoted[index + 1]); singleQuoted[index] = character === "'" ? value[index + 1] : singleQuoted[index + 1]; value[index] = (character === "\"" ? doubleQuoted[index + 1] : 0) || (character === "'" ? singleQuoted[index + 1] : 0) || (escaped ? value[afterEscape] : 0) || (!whitespace && character !== ";" && character !== "&" && character !== "|" ? value[index + 1] : 0) || separator; } let commandStart = 0; do { const end = value[name.lastIndex]; if (!end) break; commandStart = end; name.lastIndex = end; } while (name.test(source)); return source.slice(commandStart); } /** Reject recognizable shell commands without guessing at JavaScript expressions. */ function isShellLikeCodeModeSource(source) { const trimmed = source.trim(); if (trimmed.startsWith("#!")) return true; const uncommented = trimmed.replace(LEADING_SOURCE_COMMENTS, ""); if (!uncommented || JAVASCRIPT_EXPORT.test(uncommented)) return false; if (SHELL_CONTROL.test(uncommented)) return true; const commandSource = stripShellEnvAssignments(uncommented); const knownCommand = SHELL_COMMAND.exec(commandSource); const unknownCommand = SHELL_IDENTIFIER.exec(commandSource); const command = knownCommand ?? (unknownCommand && !JAVASCRIPT_KEYWORD.test(unknownCommand[1] ?? "") && !JAVASCRIPT_GLOBAL.test(unknownCommand[1] ?? "") ? unknownCommand : null); if (!command && !SHELL_EXECUTABLE_PATH.test(commandSource)) return false; const commandTail = command ? commandSource.slice(command[0].length) : ""; const remainder = commandTail.trimStart(); if (command && !remainder) return knownCommand !== null; if (!parsesAsGuestJavaScript(source)) return true; const commandName = command?.[1]; if (commandName && hasHoistedGuestBinding(source, commandName)) return false; if (/^[\t ]*(?:[;\r\n]|&&?|\|{1,2})/u.test(commandTail)) return knownCommand !== null; if (!command || !SHELL_ARGUMENT.test(remainder) && !(knownCommand !== null && SHELL_REDIRECTION.test(remainder)) && !(commandName === "jq" && remainder.startsWith("."))) return false; return true; } const CODE_MODE_SHELL_SOURCE_ERROR = "code-mode exec runs JavaScript, not shell commands. Call an enabled async tool global from guest JavaScript; use catalog.search(query) when the bounded quick index omits it. Do not retry the same shell command as code."; //#endregion //#region src/agents/code-mode-source.ts /** Validate guest JavaScript before execution. */ function isModuleLoaderCallee(callee) { if (callee.type === "ParenthesizedExpression") return isModuleLoaderCallee(callee.expression); if (callee.type === "ChainExpression") return isModuleLoaderCallee(callee.expression); if (callee.type === "SequenceExpression") { const expression = callee.expressions[callee.expressions.length - 1]; return expression !== void 0 && isModuleLoaderCallee(expression); } return callee.type === "Identifier" && callee.name === "require"; } function containsModuleAccess(node) { if (node.type === "ImportExpression" || node.type === "CallExpression" && isModuleLoaderCallee(node.callee)) return true; for (const value of Object.values(node)) { if (Array.isArray(value)) { for (const child of value) if (child !== null && typeof child === "object" && "type" in child && typeof child.type === "string" && containsModuleAccess(child)) return true; continue; } if (value !== null && typeof value === "object" && "type" in value && typeof value.type === "string" && containsModuleAccess(value)) return true; } return false; } function prepareSource(code) { const parsed = parseCodeModeScriptSyntax(code); if (isShellLikeCodeModeSource(code)) throw new ToolInputError(CODE_MODE_SHELL_SOURCE_ERROR); if (!parsed.ok) { const message = parsed.message.slice(0, 240); throw new ToolInputError(`SyntaxError at openclaw-code-mode:user.js:${parsed.line}:${parsed.column + 1}: ${message}. No tools were dispatched; correct the JavaScript source and submit it again.`); } if ((code.includes("import") || code.includes("require") || code.includes("\\u")) && containsModuleAccess(parsed.program)) throw new ToolInputError("code mode module access is disabled."); return code; } //#endregion export { normalizeSourceStack as a, buildUserSource as i, SOURCE_LOCATION_KEY as n, CODE_MODE_CONTROLLER_SOURCE as o, USER_SOURCE_FILE as r, prepareSource as t };