mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 17:41:37 +00:00
* fix(code-mode): bound and simplify worker lifecycles * fix(code-mode): keep worker input internal * fix(code-mode): retry failed wasm initialization safely * docs(code-mode): explain live node tool budget invariant * test(code-mode): prove hostless promises fail without worker churn
239 lines
9.2 KiB
TypeScript
239 lines
9.2 KiB
TypeScript
/** Sandboxed guest globals and host bridge for Code Mode QuickJS cells. */
|
|
import { CODE_MODE_SWARM_CONTROLLER_SOURCE } from "./code-mode-swarm-controller-source.js";
|
|
|
|
export const CODE_MODE_CONTROLLER_SOURCE = String.raw`
|
|
(() => {
|
|
const output = [];
|
|
const pending = new Map();
|
|
const catalog = 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;
|
|
delete globalThis.__openclawHostRequest;
|
|
const bridgeSequences = new Map();
|
|
|
|
function safe(value) {
|
|
if (value === undefined) return null;
|
|
try {
|
|
return JSON.parse(JSON.stringify(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));
|
|
return typeof encoded === "string" ? encoded : String(value);
|
|
}
|
|
|
|
function request(method, args) {
|
|
const methodName = String(method);
|
|
const sequence = (bridgeSequences.get(methodName) ?? 0) + 1;
|
|
bridgeSequences.set(methodName, sequence);
|
|
const bridgeId = "bridge:" + methodName + ":" + String(sequence);
|
|
const id = String(hostRequest(methodName, JSON.stringify(safe(args ?? [])), bridgeId));
|
|
return new Promise((resolve, reject) => {
|
|
pending.set(id, { resolve, reject });
|
|
});
|
|
}
|
|
|
|
${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;
|
|
Object.defineProperty(object, key, {
|
|
value: deserializeNamespaceValue(namespaceId, entry[1]),
|
|
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 Error(typeof parsed === "string" ? parsed : parsed?.message ?? "nested tool failed");
|
|
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 baseTools = Object.create(null);
|
|
Object.defineProperties(baseTools, {
|
|
search: { value: (query, options) => request("search", [query, options]), enumerable: true },
|
|
describe: { value: (id) => request("describe", [id]), enumerable: true },
|
|
call: { value: (id, input) => request("call", [id, input]), enumerable: true },
|
|
callValue: { value: (id, input) => request("callValue", [id, input]), enumerable: true },
|
|
});
|
|
const skills = Object.freeze({
|
|
list: () => request("skillsList", []),
|
|
read: (name) => request("skillsRead", [name]),
|
|
});
|
|
|
|
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,
|
|
}));
|
|
}
|
|
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()]
|
|
.filter((file) => !normalizedPrefix || file.path === normalizedPrefix || file.path.startsWith(normalizedPrefix.replace(/\/?$/, "/")))
|
|
.map((file) => Object.freeze({
|
|
path: file.path,
|
|
description: file.description,
|
|
bytes: file.bytes,
|
|
}));
|
|
return { files };
|
|
},
|
|
read: async (path) => {
|
|
const normalizedPath = normalizeApiPath(path);
|
|
const file = apiFileMap.get(normalizedPath);
|
|
if (!file) throw new Error("Unknown API file: " + normalizedPath);
|
|
return file;
|
|
},
|
|
});
|
|
|
|
const safeNameCounts = new Map();
|
|
for (const tool of catalog) {
|
|
const name = typeof tool?.name === "string" ? tool.name : "";
|
|
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) continue;
|
|
safeNameCounts.set(name, (safeNameCounts.get(name) ?? 0) + 1);
|
|
}
|
|
for (const tool of catalog) {
|
|
const name = typeof tool?.name === "string" ? tool.name : "";
|
|
const id = typeof tool?.id === "string" ? tool.id : "";
|
|
if (!id || safeNameCounts.get(name) !== 1 || Object.prototype.hasOwnProperty.call(baseTools, name)) {
|
|
continue;
|
|
}
|
|
Object.defineProperty(baseTools, name, {
|
|
value: (input) => request("callValue", [id, input]),
|
|
enumerable: true,
|
|
});
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|
|
|
|
Object.defineProperties(globalThis, {
|
|
ALL_TOOLS: { value: Object.freeze(catalog.slice()), enumerable: true },
|
|
API: { value: api, enumerable: true },
|
|
nodes: { value: nodes, enumerable: true },
|
|
namespaces: { value: Object.freeze(namespaceGlobals), enumerable: true },
|
|
skills: { value: skills, enumerable: true },
|
|
tools: { value: Object.freeze(baseTools), enumerable: true },
|
|
text: { value: (value) => output.push({ type: "text", text: asText(value) }), enumerable: true },
|
|
json: { value: (value) => output.push({ type: "json", value: safe(value) }), enumerable: true },
|
|
yield_control: { value: (reason) => request("yield", [reason]), enumerable: true },
|
|
__openclawSettleBridge: { value: settle },
|
|
__openclawTakeOutput: { value: () => output.splice(0) },
|
|
});
|
|
})();
|
|
`;
|