@mcploom/codexec-isolated-vm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/index.cjs +340 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +38 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +340 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mouaad Aallam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @mcploom/codexec-isolated-vm
|
|
2
|
+
|
|
3
|
+
`isolated-vm` executor package for `@mcploom/codexec`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @mcploom/codexec @mcploom/codexec-isolated-vm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
It implements the shared `Executor` contract from `@mcploom/codexec`, so it can be used anywhere the QuickJS package can be used.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Node 20+ must run with `--no-node-snapshot`
|
|
16
|
+
- the optional `isolated-vm` native dependency must install successfully in the host environment
|
|
17
|
+
- native-addon failures are surfaced only when `IsolatedVmExecutor` is used
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { resolveProvider } from "@mcploom/codexec";
|
|
23
|
+
import { IsolatedVmExecutor } from "@mcploom/codexec-isolated-vm";
|
|
24
|
+
|
|
25
|
+
const provider = resolveProvider({
|
|
26
|
+
tools: {
|
|
27
|
+
echo: {
|
|
28
|
+
execute: async (input) => input,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const executor = new IsolatedVmExecutor();
|
|
34
|
+
const result = await executor.execute("await codemode.echo({ ok: true })", [
|
|
35
|
+
provider,
|
|
36
|
+
]);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This package is verified through the opt-in workspace flow:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm run verify:isolated-vm
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`isolated-vm` is not documented here as a hard security boundary. If the workload is hostile or process stability matters more than in-process performance, prefer process isolation around the executor.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
let __mcploom_codexec = require("@mcploom/codexec");
|
|
2
|
+
|
|
3
|
+
//#region src/isolatedVmExecutor.ts
|
|
4
|
+
const DEFAULT_MEMORY_LIMIT_BYTES = 64 * 1024 * 1024;
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 5e3;
|
|
6
|
+
const DEFAULT_MAX_LOG_LINES = 100;
|
|
7
|
+
const DEFAULT_MAX_LOG_CHARS = 64e3;
|
|
8
|
+
const HOST_ERROR_PREFIX = "__MCP_CODE_EXEC_HOST_ERROR__";
|
|
9
|
+
let cachedModulePromise;
|
|
10
|
+
function createExecutionContext(signal, providerName, safeToolName, originalToolName) {
|
|
11
|
+
return {
|
|
12
|
+
originalToolName,
|
|
13
|
+
providerName,
|
|
14
|
+
safeToolName,
|
|
15
|
+
signal
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function hasRequiredNodeFlag() {
|
|
19
|
+
const execArgv = process.execArgv.join(" ");
|
|
20
|
+
const nodeOptions = process.env.NODE_OPTIONS ?? "";
|
|
21
|
+
return execArgv.includes("--no-node-snapshot") || nodeOptions.includes("--no-node-snapshot");
|
|
22
|
+
}
|
|
23
|
+
function isKnownErrorCode(value) {
|
|
24
|
+
return value === "timeout" || value === "memory_limit" || value === "validation_error" || value === "tool_error" || value === "runtime_error" || value === "serialization_error" || value === "internal_error";
|
|
25
|
+
}
|
|
26
|
+
function normalizeThrownMessage(error) {
|
|
27
|
+
if (error instanceof Error) return error.message;
|
|
28
|
+
if (typeof error === "object" && error !== null && "message" in error) {
|
|
29
|
+
const message = error.message;
|
|
30
|
+
if (typeof message === "string") return message;
|
|
31
|
+
}
|
|
32
|
+
return String(error);
|
|
33
|
+
}
|
|
34
|
+
function truncateLogs(logs, maxLogLines, maxLogChars) {
|
|
35
|
+
const limitedLines = logs.slice(0, maxLogLines);
|
|
36
|
+
let remainingChars = maxLogChars;
|
|
37
|
+
const truncated = [];
|
|
38
|
+
for (const line of limitedLines) {
|
|
39
|
+
if (remainingChars <= 0) break;
|
|
40
|
+
if (line.length <= remainingChars) {
|
|
41
|
+
truncated.push(line);
|
|
42
|
+
remainingChars -= line.length;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
truncated.push(line.slice(0, remainingChars));
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
return truncated;
|
|
49
|
+
}
|
|
50
|
+
function formatLogValue(value) {
|
|
51
|
+
if (typeof value === "string") return value;
|
|
52
|
+
if (value === void 0) return "undefined";
|
|
53
|
+
try {
|
|
54
|
+
return JSON.stringify(value);
|
|
55
|
+
} catch {
|
|
56
|
+
return String(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function formatConsoleLine(values) {
|
|
60
|
+
return values.map((value) => formatLogValue(value)).join(" ");
|
|
61
|
+
}
|
|
62
|
+
function toJsonValue(value, message) {
|
|
63
|
+
if (value === void 0) return;
|
|
64
|
+
const jsonValue = JSON.stringify(value);
|
|
65
|
+
if (jsonValue === void 0) throw new __mcploom_codexec.ExecuteFailure("serialization_error", message);
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(jsonValue);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new __mcploom_codexec.ExecuteFailure("serialization_error", message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function toTransferableValue(ivm, value) {
|
|
73
|
+
const normalizedValue = toJsonValue(value, "Host value is not JSON-serializable");
|
|
74
|
+
if (normalizedValue === null || normalizedValue === void 0 || typeof normalizedValue === "string" || typeof normalizedValue === "number" || typeof normalizedValue === "boolean") return normalizedValue;
|
|
75
|
+
return new ivm.ExternalCopy(normalizedValue).copyInto({ release: true });
|
|
76
|
+
}
|
|
77
|
+
function toExecuteError(error, deadline) {
|
|
78
|
+
if (typeof error === "object" && error !== null && "code" in error && "message" in error && isKnownErrorCode(error.code) && typeof error.message === "string") return {
|
|
79
|
+
code: error.code,
|
|
80
|
+
message: error.message
|
|
81
|
+
};
|
|
82
|
+
if ((0, __mcploom_codexec.isExecuteFailure)(error)) return {
|
|
83
|
+
code: error.code,
|
|
84
|
+
message: error.message
|
|
85
|
+
};
|
|
86
|
+
const message = normalizeThrownMessage(error);
|
|
87
|
+
const normalizedMessage = message.toLowerCase();
|
|
88
|
+
if (Date.now() > deadline || normalizedMessage.includes("timed out") || normalizedMessage.includes("time limit")) return {
|
|
89
|
+
code: "timeout",
|
|
90
|
+
message: "Execution timed out"
|
|
91
|
+
};
|
|
92
|
+
if (normalizedMessage.includes("memory limit") || normalizedMessage.includes("out of memory")) return {
|
|
93
|
+
code: "memory_limit",
|
|
94
|
+
message
|
|
95
|
+
};
|
|
96
|
+
if (normalizedMessage.includes("could not be cloned") || normalizedMessage.includes("non-transferable") || normalizedMessage.includes("not json-serializable")) return {
|
|
97
|
+
code: "serialization_error",
|
|
98
|
+
message
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
code: "runtime_error",
|
|
102
|
+
message
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function toMemoryLimitMb(memoryLimitBytes) {
|
|
106
|
+
return Math.max(8, Math.ceil(memoryLimitBytes / (1024 * 1024)));
|
|
107
|
+
}
|
|
108
|
+
function remainingTime(deadline) {
|
|
109
|
+
return Math.max(0, deadline - Date.now());
|
|
110
|
+
}
|
|
111
|
+
async function runWithDeadline(operation, deadline, signal) {
|
|
112
|
+
if (signal.aborted || Date.now() > deadline) throw new __mcploom_codexec.ExecuteFailure("timeout", "Execution timed out");
|
|
113
|
+
const timeoutMs = remainingTime(deadline);
|
|
114
|
+
if (timeoutMs <= 0) throw new __mcploom_codexec.ExecuteFailure("timeout", "Execution timed out");
|
|
115
|
+
let timeoutId;
|
|
116
|
+
return await new Promise((resolve, reject) => {
|
|
117
|
+
const onAbort = () => {
|
|
118
|
+
reject(new __mcploom_codexec.ExecuteFailure("timeout", "Execution timed out"));
|
|
119
|
+
};
|
|
120
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
121
|
+
timeoutId = setTimeout(() => {
|
|
122
|
+
signal.removeEventListener("abort", onAbort);
|
|
123
|
+
reject(new __mcploom_codexec.ExecuteFailure("timeout", "Execution timed out"));
|
|
124
|
+
}, timeoutMs);
|
|
125
|
+
operation.then((value) => {
|
|
126
|
+
signal.removeEventListener("abort", onAbort);
|
|
127
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
128
|
+
resolve(value);
|
|
129
|
+
}, (error) => {
|
|
130
|
+
signal.removeEventListener("abort", onAbort);
|
|
131
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
132
|
+
reject(error);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function loadDefaultModule() {
|
|
137
|
+
cachedModulePromise ??= import("isolated-vm").then((loaded) => {
|
|
138
|
+
return "default" in loaded ? loaded.default : loaded;
|
|
139
|
+
});
|
|
140
|
+
return cachedModulePromise;
|
|
141
|
+
}
|
|
142
|
+
function createBootstrapSource(providers, timeoutMs) {
|
|
143
|
+
const lines = [
|
|
144
|
+
`const __MCP_HOST_ERROR_PREFIX = ${JSON.stringify(HOST_ERROR_PREFIX)};`,
|
|
145
|
+
"const __mcpNormalizeError = (error) => {",
|
|
146
|
+
" if (error && typeof error === 'object' && typeof error.code === 'string' && typeof error.message === 'string') {",
|
|
147
|
+
" return { code: error.code, message: error.message };",
|
|
148
|
+
" }",
|
|
149
|
+
" if (error && typeof error.message === 'string' && error.message.startsWith(__MCP_HOST_ERROR_PREFIX)) {",
|
|
150
|
+
" try {",
|
|
151
|
+
" const payload = JSON.parse(error.message.slice(__MCP_HOST_ERROR_PREFIX.length));",
|
|
152
|
+
" if (payload && typeof payload.code === 'string' && typeof payload.message === 'string') {",
|
|
153
|
+
" return payload;",
|
|
154
|
+
" }",
|
|
155
|
+
" } catch {}",
|
|
156
|
+
" }",
|
|
157
|
+
" if (error && typeof error.message === 'string') {",
|
|
158
|
+
" return { code: 'runtime_error', message: error.message };",
|
|
159
|
+
" }",
|
|
160
|
+
" return { code: 'runtime_error', message: String(error) };",
|
|
161
|
+
"};",
|
|
162
|
+
"const __mcpRaiseNormalizedError = (error) => {",
|
|
163
|
+
" const normalized = __mcpNormalizeError(error);",
|
|
164
|
+
" const wrapped = new Error(normalized.message);",
|
|
165
|
+
" wrapped.code = normalized.code;",
|
|
166
|
+
" throw wrapped;",
|
|
167
|
+
"};",
|
|
168
|
+
"const __mcpToJsonValue = (value) => {",
|
|
169
|
+
" if (typeof value === 'undefined') {",
|
|
170
|
+
" return undefined;",
|
|
171
|
+
" }",
|
|
172
|
+
" const json = JSON.stringify(value);",
|
|
173
|
+
" if (typeof json === 'undefined') {",
|
|
174
|
+
" const error = new Error('Guest code returned a non-serializable value');",
|
|
175
|
+
" error.code = 'serialization_error';",
|
|
176
|
+
" throw error;",
|
|
177
|
+
" }",
|
|
178
|
+
" return JSON.parse(json);",
|
|
179
|
+
"};",
|
|
180
|
+
"globalThis.console = {",
|
|
181
|
+
" log: (...args) => __mcp_console_log(...args),",
|
|
182
|
+
" info: (...args) => __mcp_console_info(...args),",
|
|
183
|
+
" warn: (...args) => __mcp_console_warn(...args),",
|
|
184
|
+
" error: (...args) => __mcp_console_error(...args),",
|
|
185
|
+
"};"
|
|
186
|
+
];
|
|
187
|
+
for (const provider of providers) {
|
|
188
|
+
lines.push(`globalThis.${provider.name} = {};`);
|
|
189
|
+
for (const safeToolName of Object.keys(provider.tools)) {
|
|
190
|
+
const hostReferenceName = `__mcp_tool_${provider.name}_${safeToolName}`;
|
|
191
|
+
lines.push(`globalThis.${provider.name}.${safeToolName} = async (input) => {`, " try {", ` return await ${hostReferenceName}.applySyncPromise(undefined, [input], { arguments: { copy: true }, timeout: ${timeoutMs} });`, " } catch (error) {", " __mcpRaiseNormalizedError(error);", " }", "};");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return lines.join("\n");
|
|
195
|
+
}
|
|
196
|
+
function createExecutionSource(code) {
|
|
197
|
+
return [
|
|
198
|
+
`const __mcpUserFunction = (${(0, __mcploom_codexec.normalizeCode)(code)});`,
|
|
199
|
+
"return (async () => {",
|
|
200
|
+
" try {",
|
|
201
|
+
" const value = await __mcpUserFunction();",
|
|
202
|
+
" return { ok: true, value: __mcpToJsonValue(value) };",
|
|
203
|
+
" } catch (error) {",
|
|
204
|
+
" return { ok: false, error: __mcpNormalizeError(error) };",
|
|
205
|
+
" }",
|
|
206
|
+
"})();"
|
|
207
|
+
].join("\n");
|
|
208
|
+
}
|
|
209
|
+
function setConsoleBindings(context, logs) {
|
|
210
|
+
const jail = context.global;
|
|
211
|
+
jail.setSync("__mcp_console_log", (...args) => {
|
|
212
|
+
logs.push(formatConsoleLine(args));
|
|
213
|
+
});
|
|
214
|
+
jail.setSync("__mcp_console_info", (...args) => {
|
|
215
|
+
logs.push(formatConsoleLine(args));
|
|
216
|
+
});
|
|
217
|
+
jail.setSync("__mcp_console_warn", (...args) => {
|
|
218
|
+
logs.push(formatConsoleLine(args));
|
|
219
|
+
});
|
|
220
|
+
jail.setSync("__mcp_console_error", (...args) => {
|
|
221
|
+
logs.push(formatConsoleLine(args));
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function setProviderBindings(ivm, context, providers, signal, deadline) {
|
|
225
|
+
const jail = context.global;
|
|
226
|
+
for (const provider of providers) for (const [safeToolName, descriptor] of Object.entries(provider.tools)) {
|
|
227
|
+
const hostReferenceName = `__mcp_tool_${provider.name}_${safeToolName}`;
|
|
228
|
+
jail.setSync(hostReferenceName, async (input) => {
|
|
229
|
+
const executionContext = createExecutionContext(signal, provider.name, safeToolName, descriptor.originalName);
|
|
230
|
+
try {
|
|
231
|
+
const normalizedInput = input === void 0 ? void 0 : toJsonValue(input, "Guest code passed a non-serializable tool input");
|
|
232
|
+
return toTransferableValue(ivm, await runWithDeadline(Promise.resolve(descriptor.execute(normalizedInput, executionContext)), deadline, signal));
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const executeError = toExecuteError(error, deadline);
|
|
235
|
+
throw new Error(`${HOST_ERROR_PREFIX}${JSON.stringify(executeError)}`, { cause: error });
|
|
236
|
+
}
|
|
237
|
+
}, { reference: true });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* isolated-vm-backed executor for one-shot sandboxed JavaScript runs.
|
|
242
|
+
*/
|
|
243
|
+
var IsolatedVmExecutor = class {
|
|
244
|
+
loadModule;
|
|
245
|
+
maxLogChars;
|
|
246
|
+
maxLogLines;
|
|
247
|
+
memoryLimitBytes;
|
|
248
|
+
timeoutMs;
|
|
249
|
+
constructor(options = {}) {
|
|
250
|
+
this.loadModule = async () => {
|
|
251
|
+
return options.loadModule ? await options.loadModule() : await loadDefaultModule();
|
|
252
|
+
};
|
|
253
|
+
this.maxLogChars = options.maxLogChars ?? DEFAULT_MAX_LOG_CHARS;
|
|
254
|
+
this.maxLogLines = options.maxLogLines ?? DEFAULT_MAX_LOG_LINES;
|
|
255
|
+
this.memoryLimitBytes = options.memoryLimitBytes ?? DEFAULT_MEMORY_LIMIT_BYTES;
|
|
256
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Executes JavaScript against the provided tool namespaces in a fresh isolated-vm context.
|
|
260
|
+
*/
|
|
261
|
+
async execute(code, providers) {
|
|
262
|
+
const startedAt = Date.now();
|
|
263
|
+
const deadline = startedAt + this.timeoutMs;
|
|
264
|
+
const logs = [];
|
|
265
|
+
const abortController = new AbortController();
|
|
266
|
+
if (Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10) >= 20 && !hasRequiredNodeFlag()) return {
|
|
267
|
+
durationMs: Date.now() - startedAt,
|
|
268
|
+
error: {
|
|
269
|
+
code: "internal_error",
|
|
270
|
+
message: "isolated-vm requires Node to run with --no-node-snapshot on Node 20+"
|
|
271
|
+
},
|
|
272
|
+
logs,
|
|
273
|
+
ok: false
|
|
274
|
+
};
|
|
275
|
+
let catastrophicErrorMessage;
|
|
276
|
+
let isolate;
|
|
277
|
+
let context;
|
|
278
|
+
try {
|
|
279
|
+
const ivm = await this.loadModule();
|
|
280
|
+
isolate = new ivm.Isolate({
|
|
281
|
+
memoryLimit: toMemoryLimitMb(this.memoryLimitBytes),
|
|
282
|
+
onCatastrophicError: (message) => {
|
|
283
|
+
catastrophicErrorMessage = message;
|
|
284
|
+
abortController.abort();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
context = await isolate.createContext();
|
|
288
|
+
setConsoleBindings(context, logs);
|
|
289
|
+
setProviderBindings(ivm, context, providers, abortController.signal, deadline);
|
|
290
|
+
await context.eval(createBootstrapSource(providers, this.timeoutMs), { timeout: this.timeoutMs });
|
|
291
|
+
const execution = await context.evalClosure(createExecutionSource(code), [], {
|
|
292
|
+
timeout: this.timeoutMs,
|
|
293
|
+
result: {
|
|
294
|
+
copy: true,
|
|
295
|
+
promise: true
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
if (catastrophicErrorMessage) return {
|
|
299
|
+
durationMs: Date.now() - startedAt,
|
|
300
|
+
error: {
|
|
301
|
+
code: "internal_error",
|
|
302
|
+
message: `isolated-vm catastrophic error: ${catastrophicErrorMessage}`
|
|
303
|
+
},
|
|
304
|
+
logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),
|
|
305
|
+
ok: false
|
|
306
|
+
};
|
|
307
|
+
if (!execution.ok) return {
|
|
308
|
+
durationMs: Date.now() - startedAt,
|
|
309
|
+
error: toExecuteError(execution.error, deadline),
|
|
310
|
+
logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),
|
|
311
|
+
ok: false
|
|
312
|
+
};
|
|
313
|
+
return {
|
|
314
|
+
durationMs: Date.now() - startedAt,
|
|
315
|
+
logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),
|
|
316
|
+
ok: true,
|
|
317
|
+
result: execution.value
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
const executeError = catastrophicErrorMessage !== void 0 ? {
|
|
321
|
+
code: "internal_error",
|
|
322
|
+
message: `isolated-vm catastrophic error: ${catastrophicErrorMessage}`
|
|
323
|
+
} : toExecuteError(error, deadline);
|
|
324
|
+
return {
|
|
325
|
+
durationMs: Date.now() - startedAt,
|
|
326
|
+
error: executeError,
|
|
327
|
+
logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),
|
|
328
|
+
ok: false
|
|
329
|
+
};
|
|
330
|
+
} finally {
|
|
331
|
+
abortController.abort();
|
|
332
|
+
context?.release?.();
|
|
333
|
+
isolate?.dispose();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
//#endregion
|
|
339
|
+
exports.IsolatedVmExecutor = IsolatedVmExecutor;
|
|
340
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["cachedModulePromise: Promise<IsolatedVmModule> | undefined","truncated: string[]","ExecuteFailure","timeoutId: ReturnType<typeof setTimeout> | undefined","logs: string[]","catastrophicErrorMessage: string | undefined","isolate: IsolatedVmIsolate | undefined","context: IsolatedVmContext | undefined"],"sources":["../src/isolatedVmExecutor.ts"],"sourcesContent":["import {\n ExecuteFailure,\n isExecuteFailure,\n normalizeCode,\n} from \"@mcploom/codexec\";\nimport type {\n ExecuteError,\n ExecuteResult,\n Executor,\n ResolvedToolProvider,\n ToolExecutionContext,\n} from \"@mcploom/codexec\";\n\nimport type { IsolatedVmExecutorOptions } from \"./types\";\n\ntype IsolatedVmExternalCopyInstance = {\n copyInto: (options?: { release?: boolean; transferIn?: boolean }) => unknown;\n release?: () => void;\n};\n\ntype IsolatedVmReferenceInstance = {\n setSync: (\n property: string,\n value: unknown,\n options?: Record<string, unknown>,\n ) => void;\n};\n\ntype IsolatedVmContext = {\n eval: (code: string, options?: Record<string, unknown>) => Promise<unknown>;\n evalClosure: (\n code: string,\n args?: unknown[],\n options?: Record<string, unknown>,\n ) => Promise<unknown>;\n global: IsolatedVmReferenceInstance;\n release?: () => void;\n};\n\ntype IsolatedVmIsolate = {\n createContext: () => Promise<IsolatedVmContext>;\n dispose: () => void;\n};\n\ntype IsolatedVmModule = {\n ExternalCopy: new (\n value: unknown,\n options?: Record<string, unknown>,\n ) => IsolatedVmExternalCopyInstance;\n Isolate: new (options?: {\n inspector?: boolean;\n memoryLimit?: number;\n onCatastrophicError?: (message: string) => void;\n }) => IsolatedVmIsolate;\n};\n\ntype GuestExecutionEnvelope =\n | {\n ok: true;\n value: unknown;\n }\n | {\n error: {\n code?: string;\n message?: string;\n };\n ok: false;\n };\n\nconst DEFAULT_MEMORY_LIMIT_BYTES = 64 * 1024 * 1024;\nconst DEFAULT_TIMEOUT_MS = 5000;\nconst DEFAULT_MAX_LOG_LINES = 100;\nconst DEFAULT_MAX_LOG_CHARS = 64_000;\nconst HOST_ERROR_PREFIX = \"__MCP_CODE_EXEC_HOST_ERROR__\";\n\nlet cachedModulePromise: Promise<IsolatedVmModule> | undefined;\n\nfunction createExecutionContext(\n signal: AbortSignal,\n providerName: string,\n safeToolName: string,\n originalToolName: string,\n): ToolExecutionContext {\n return {\n originalToolName,\n providerName,\n safeToolName,\n signal,\n };\n}\n\nfunction hasRequiredNodeFlag(): boolean {\n const execArgv = process.execArgv.join(\" \");\n const nodeOptions = process.env.NODE_OPTIONS ?? \"\";\n return (\n execArgv.includes(\"--no-node-snapshot\") ||\n nodeOptions.includes(\"--no-node-snapshot\")\n );\n}\n\nfunction isKnownErrorCode(value: unknown): value is ExecuteError[\"code\"] {\n return (\n value === \"timeout\" ||\n value === \"memory_limit\" ||\n value === \"validation_error\" ||\n value === \"tool_error\" ||\n value === \"runtime_error\" ||\n value === \"serialization_error\" ||\n value === \"internal_error\"\n );\n}\n\nfunction normalizeThrownMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n\n if (typeof error === \"object\" && error !== null && \"message\" in error) {\n const message = (error as { message?: unknown }).message;\n if (typeof message === \"string\") {\n return message;\n }\n }\n\n return String(error);\n}\n\nfunction truncateLogs(\n logs: string[],\n maxLogLines: number,\n maxLogChars: number,\n): string[] {\n const limitedLines = logs.slice(0, maxLogLines);\n let remainingChars = maxLogChars;\n const truncated: string[] = [];\n\n for (const line of limitedLines) {\n if (remainingChars <= 0) {\n break;\n }\n\n if (line.length <= remainingChars) {\n truncated.push(line);\n remainingChars -= line.length;\n continue;\n }\n\n truncated.push(line.slice(0, remainingChars));\n break;\n }\n\n return truncated;\n}\n\nfunction formatLogValue(value: unknown): string {\n if (typeof value === \"string\") {\n return value;\n }\n\n if (value === undefined) {\n return \"undefined\";\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\nfunction formatConsoleLine(values: unknown[]): string {\n return values.map((value) => formatLogValue(value)).join(\" \");\n}\n\nfunction toJsonValue(value: unknown, message: string): unknown {\n if (value === undefined) {\n return undefined;\n }\n\n const jsonValue = JSON.stringify(value);\n\n if (jsonValue === undefined) {\n throw new ExecuteFailure(\"serialization_error\", message);\n }\n\n try {\n return JSON.parse(jsonValue) as unknown;\n } catch {\n throw new ExecuteFailure(\"serialization_error\", message);\n }\n}\n\nfunction toTransferableValue(ivm: IsolatedVmModule, value: unknown): unknown {\n const normalizedValue = toJsonValue(\n value,\n \"Host value is not JSON-serializable\",\n );\n\n if (\n normalizedValue === null ||\n normalizedValue === undefined ||\n typeof normalizedValue === \"string\" ||\n typeof normalizedValue === \"number\" ||\n typeof normalizedValue === \"boolean\"\n ) {\n return normalizedValue;\n }\n\n const copy = new ivm.ExternalCopy(normalizedValue);\n return copy.copyInto({ release: true });\n}\n\nfunction toExecuteError(error: unknown, deadline: number): ExecuteError {\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n \"message\" in error &&\n isKnownErrorCode((error as { code?: unknown }).code) &&\n typeof (error as { message?: unknown }).message === \"string\"\n ) {\n return {\n code: (error as { code: ExecuteError[\"code\"] }).code,\n message: (error as { message: string }).message,\n };\n }\n\n if (isExecuteFailure(error)) {\n return {\n code: error.code,\n message: error.message,\n };\n }\n\n const message = normalizeThrownMessage(error);\n const normalizedMessage = message.toLowerCase();\n\n if (\n Date.now() > deadline ||\n normalizedMessage.includes(\"timed out\") ||\n normalizedMessage.includes(\"time limit\")\n ) {\n return {\n code: \"timeout\",\n message: \"Execution timed out\",\n };\n }\n\n if (\n normalizedMessage.includes(\"memory limit\") ||\n normalizedMessage.includes(\"out of memory\")\n ) {\n return {\n code: \"memory_limit\",\n message,\n };\n }\n\n if (\n normalizedMessage.includes(\"could not be cloned\") ||\n normalizedMessage.includes(\"non-transferable\") ||\n normalizedMessage.includes(\"not json-serializable\")\n ) {\n return {\n code: \"serialization_error\",\n message,\n };\n }\n\n return {\n code: \"runtime_error\",\n message,\n };\n}\n\nfunction toMemoryLimitMb(memoryLimitBytes: number): number {\n return Math.max(8, Math.ceil(memoryLimitBytes / (1024 * 1024)));\n}\n\nfunction remainingTime(deadline: number): number {\n return Math.max(0, deadline - Date.now());\n}\n\nasync function runWithDeadline<T>(\n operation: Promise<T>,\n deadline: number,\n signal: AbortSignal,\n): Promise<T> {\n if (signal.aborted || Date.now() > deadline) {\n throw new ExecuteFailure(\"timeout\", \"Execution timed out\");\n }\n\n const timeoutMs = remainingTime(deadline);\n if (timeoutMs <= 0) {\n throw new ExecuteFailure(\"timeout\", \"Execution timed out\");\n }\n\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n return await new Promise<T>((resolve, reject) => {\n const onAbort = () => {\n reject(new ExecuteFailure(\"timeout\", \"Execution timed out\"));\n };\n\n signal.addEventListener(\"abort\", onAbort, { once: true });\n timeoutId = setTimeout(() => {\n signal.removeEventListener(\"abort\", onAbort);\n reject(new ExecuteFailure(\"timeout\", \"Execution timed out\"));\n }, timeoutMs);\n\n void operation.then(\n (value) => {\n signal.removeEventListener(\"abort\", onAbort);\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n resolve(value);\n },\n (error) => {\n signal.removeEventListener(\"abort\", onAbort);\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n reject(error);\n },\n );\n });\n}\n\nasync function loadDefaultModule(): Promise<IsolatedVmModule> {\n cachedModulePromise ??= import(\"isolated-vm\").then((loaded) => {\n const candidate = (\n \"default\" in loaded ? loaded.default : loaded\n ) as unknown;\n return candidate as IsolatedVmModule;\n });\n\n return cachedModulePromise;\n}\n\nfunction createBootstrapSource(\n providers: ResolvedToolProvider[],\n timeoutMs: number,\n): string {\n const lines = [\n `const __MCP_HOST_ERROR_PREFIX = ${JSON.stringify(HOST_ERROR_PREFIX)};`,\n \"const __mcpNormalizeError = (error) => {\",\n \" if (error && typeof error === 'object' && typeof error.code === 'string' && typeof error.message === 'string') {\",\n \" return { code: error.code, message: error.message };\",\n \" }\",\n \" if (error && typeof error.message === 'string' && error.message.startsWith(__MCP_HOST_ERROR_PREFIX)) {\",\n \" try {\",\n \" const payload = JSON.parse(error.message.slice(__MCP_HOST_ERROR_PREFIX.length));\",\n \" if (payload && typeof payload.code === 'string' && typeof payload.message === 'string') {\",\n \" return payload;\",\n \" }\",\n \" } catch {}\",\n \" }\",\n \" if (error && typeof error.message === 'string') {\",\n \" return { code: 'runtime_error', message: error.message };\",\n \" }\",\n \" return { code: 'runtime_error', message: String(error) };\",\n \"};\",\n \"const __mcpRaiseNormalizedError = (error) => {\",\n \" const normalized = __mcpNormalizeError(error);\",\n \" const wrapped = new Error(normalized.message);\",\n \" wrapped.code = normalized.code;\",\n \" throw wrapped;\",\n \"};\",\n \"const __mcpToJsonValue = (value) => {\",\n \" if (typeof value === 'undefined') {\",\n \" return undefined;\",\n \" }\",\n \" const json = JSON.stringify(value);\",\n \" if (typeof json === 'undefined') {\",\n \" const error = new Error('Guest code returned a non-serializable value');\",\n \" error.code = 'serialization_error';\",\n \" throw error;\",\n \" }\",\n \" return JSON.parse(json);\",\n \"};\",\n \"globalThis.console = {\",\n \" log: (...args) => __mcp_console_log(...args),\",\n \" info: (...args) => __mcp_console_info(...args),\",\n \" warn: (...args) => __mcp_console_warn(...args),\",\n \" error: (...args) => __mcp_console_error(...args),\",\n \"};\",\n ];\n\n for (const provider of providers) {\n lines.push(`globalThis.${provider.name} = {};`);\n\n for (const safeToolName of Object.keys(provider.tools)) {\n const hostReferenceName = `__mcp_tool_${provider.name}_${safeToolName}`;\n lines.push(\n `globalThis.${provider.name}.${safeToolName} = async (input) => {`,\n \" try {\",\n ` return await ${hostReferenceName}.applySyncPromise(undefined, [input], { arguments: { copy: true }, timeout: ${timeoutMs} });`,\n \" } catch (error) {\",\n \" __mcpRaiseNormalizedError(error);\",\n \" }\",\n \"};\",\n );\n }\n }\n\n return lines.join(\"\\n\");\n}\n\nfunction createExecutionSource(code: string): string {\n const executableSource = normalizeCode(code);\n\n return [\n `const __mcpUserFunction = (${executableSource});`,\n \"return (async () => {\",\n \" try {\",\n \" const value = await __mcpUserFunction();\",\n \" return { ok: true, value: __mcpToJsonValue(value) };\",\n \" } catch (error) {\",\n \" return { ok: false, error: __mcpNormalizeError(error) };\",\n \" }\",\n \"})();\",\n ].join(\"\\n\");\n}\n\nfunction setConsoleBindings(context: IsolatedVmContext, logs: string[]): void {\n const jail = context.global;\n jail.setSync(\"__mcp_console_log\", (...args: unknown[]) => {\n logs.push(formatConsoleLine(args));\n });\n jail.setSync(\"__mcp_console_info\", (...args: unknown[]) => {\n logs.push(formatConsoleLine(args));\n });\n jail.setSync(\"__mcp_console_warn\", (...args: unknown[]) => {\n logs.push(formatConsoleLine(args));\n });\n jail.setSync(\"__mcp_console_error\", (...args: unknown[]) => {\n logs.push(formatConsoleLine(args));\n });\n}\n\nfunction setProviderBindings(\n ivm: IsolatedVmModule,\n context: IsolatedVmContext,\n providers: ResolvedToolProvider[],\n signal: AbortSignal,\n deadline: number,\n): void {\n const jail = context.global;\n\n for (const provider of providers) {\n for (const [safeToolName, descriptor] of Object.entries(provider.tools)) {\n const hostReferenceName = `__mcp_tool_${provider.name}_${safeToolName}`;\n\n jail.setSync(\n hostReferenceName,\n async (input: unknown) => {\n const executionContext = createExecutionContext(\n signal,\n provider.name,\n safeToolName,\n descriptor.originalName,\n );\n\n try {\n const normalizedInput =\n input === undefined\n ? undefined\n : toJsonValue(\n input,\n \"Guest code passed a non-serializable tool input\",\n );\n const result = await runWithDeadline(\n Promise.resolve(\n descriptor.execute(normalizedInput, executionContext),\n ),\n deadline,\n signal,\n );\n\n return toTransferableValue(ivm, result);\n } catch (error) {\n const executeError = toExecuteError(error, deadline);\n throw new Error(\n `${HOST_ERROR_PREFIX}${JSON.stringify(executeError)}`,\n {\n cause: error,\n },\n );\n }\n },\n { reference: true },\n );\n }\n }\n}\n\n/**\n * isolated-vm-backed executor for one-shot sandboxed JavaScript runs.\n */\nexport class IsolatedVmExecutor implements Executor {\n private readonly loadModule: () => Promise<IsolatedVmModule>;\n private readonly maxLogChars: number;\n private readonly maxLogLines: number;\n private readonly memoryLimitBytes: number;\n private readonly timeoutMs: number;\n\n constructor(options: IsolatedVmExecutorOptions = {}) {\n this.loadModule = async () => {\n const loaded = options.loadModule\n ? await options.loadModule()\n : await loadDefaultModule();\n return loaded as IsolatedVmModule;\n };\n this.maxLogChars = options.maxLogChars ?? DEFAULT_MAX_LOG_CHARS;\n this.maxLogLines = options.maxLogLines ?? DEFAULT_MAX_LOG_LINES;\n this.memoryLimitBytes =\n options.memoryLimitBytes ?? DEFAULT_MEMORY_LIMIT_BYTES;\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n }\n\n /**\n * Executes JavaScript against the provided tool namespaces in a fresh isolated-vm context.\n */\n async execute(\n code: string,\n providers: ResolvedToolProvider[],\n ): Promise<ExecuteResult> {\n const startedAt = Date.now();\n const deadline = startedAt + this.timeoutMs;\n const logs: string[] = [];\n const abortController = new AbortController();\n const nodeMajorVersion = Number.parseInt(\n process.versions.node.split(\".\")[0] ?? \"0\",\n 10,\n );\n\n if (nodeMajorVersion >= 20 && !hasRequiredNodeFlag()) {\n return {\n durationMs: Date.now() - startedAt,\n error: {\n code: \"internal_error\",\n message:\n \"isolated-vm requires Node to run with --no-node-snapshot on Node 20+\",\n },\n logs,\n ok: false,\n };\n }\n\n let catastrophicErrorMessage: string | undefined;\n let isolate: IsolatedVmIsolate | undefined;\n let context: IsolatedVmContext | undefined;\n\n try {\n const ivm = await this.loadModule();\n isolate = new ivm.Isolate({\n memoryLimit: toMemoryLimitMb(this.memoryLimitBytes),\n onCatastrophicError: (message) => {\n catastrophicErrorMessage = message;\n abortController.abort();\n },\n });\n context = await isolate.createContext();\n\n setConsoleBindings(context, logs);\n setProviderBindings(\n ivm,\n context,\n providers,\n abortController.signal,\n deadline,\n );\n await context.eval(createBootstrapSource(providers, this.timeoutMs), {\n timeout: this.timeoutMs,\n });\n\n const execution = (await context.evalClosure(\n createExecutionSource(code),\n [],\n {\n timeout: this.timeoutMs,\n result: { copy: true, promise: true },\n },\n )) as GuestExecutionEnvelope;\n\n if (catastrophicErrorMessage) {\n return {\n durationMs: Date.now() - startedAt,\n error: {\n code: \"internal_error\",\n message: `isolated-vm catastrophic error: ${catastrophicErrorMessage}`,\n },\n logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),\n ok: false,\n };\n }\n\n if (!execution.ok) {\n return {\n durationMs: Date.now() - startedAt,\n error: toExecuteError(execution.error, deadline),\n logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),\n ok: false,\n };\n }\n\n return {\n durationMs: Date.now() - startedAt,\n logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),\n ok: true,\n result: execution.value,\n };\n } catch (error) {\n const executeError =\n catastrophicErrorMessage !== undefined\n ? {\n code: \"internal_error\" as const,\n message: `isolated-vm catastrophic error: ${catastrophicErrorMessage}`,\n }\n : toExecuteError(error, deadline);\n\n return {\n durationMs: Date.now() - startedAt,\n error: executeError,\n logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),\n ok: false,\n };\n } finally {\n abortController.abort();\n context?.release?.();\n isolate?.dispose();\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ExecuteResult, Executor, ResolvedToolProvider } from "@mcploom/codexec";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Options for constructing an {@link IsolatedVmExecutor}.
|
|
6
|
+
*/
|
|
7
|
+
interface IsolatedVmExecutorOptions {
|
|
8
|
+
/** Optional isolated-vm module loader override for tests or custom builds. */
|
|
9
|
+
loadModule?: () => Promise<unknown> | unknown;
|
|
10
|
+
/** Maximum total characters preserved across captured log lines. */
|
|
11
|
+
maxLogChars?: number;
|
|
12
|
+
/** Maximum number of captured log lines returned in the result. */
|
|
13
|
+
maxLogLines?: number;
|
|
14
|
+
/** Guest memory limit in bytes. */
|
|
15
|
+
memoryLimitBytes?: number;
|
|
16
|
+
/** Wall-clock execution timeout in milliseconds. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/isolatedVmExecutor.d.ts
|
|
21
|
+
/**
|
|
22
|
+
* isolated-vm-backed executor for one-shot sandboxed JavaScript runs.
|
|
23
|
+
*/
|
|
24
|
+
declare class IsolatedVmExecutor implements Executor {
|
|
25
|
+
private readonly loadModule;
|
|
26
|
+
private readonly maxLogChars;
|
|
27
|
+
private readonly maxLogLines;
|
|
28
|
+
private readonly memoryLimitBytes;
|
|
29
|
+
private readonly timeoutMs;
|
|
30
|
+
constructor(options?: IsolatedVmExecutorOptions);
|
|
31
|
+
/**
|
|
32
|
+
* Executes JavaScript against the provided tool namespaces in a fresh isolated-vm context.
|
|
33
|
+
*/
|
|
34
|
+
execute(code: string, providers: ResolvedToolProvider[]): Promise<ExecuteResult>;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { IsolatedVmExecutor, type IsolatedVmExecutorOptions };
|
|
38
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/isolatedVmExecutor.ts"],"sourcesContent":[],"mappings":";;;;;;AAGiB,UAAA,yBAAA,CAEI;;qBAAA;;EC+eR,WAAA,CAAA,EAAA,MAAA;EAOU;EAmBR,WAAA,CAAA,EAAA,MAAA;EACF;EAAR,gBAAA,CAAA,EAAA,MAAA;EA3BsC;EAAQ,SAAA,CAAA,EAAA,MAAA;;;;;ADjfnD;;cCifa,kBAAA,YAA8B;;EAA9B,iBAAA,WAAmB;EAOT,iBAAA,WAAA;EAmBR,iBAAA,gBAAA;EACF,iBAAA,SAAA;EAAR,WAAA,CAAA,OAAA,CAAA,EApBkB,yBAoBlB;EA3BsC;;;mCA0B5B,yBACV,QAAQ"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ExecuteResult, Executor, ResolvedToolProvider } from "@mcploom/codexec";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Options for constructing an {@link IsolatedVmExecutor}.
|
|
6
|
+
*/
|
|
7
|
+
interface IsolatedVmExecutorOptions {
|
|
8
|
+
/** Optional isolated-vm module loader override for tests or custom builds. */
|
|
9
|
+
loadModule?: () => Promise<unknown> | unknown;
|
|
10
|
+
/** Maximum total characters preserved across captured log lines. */
|
|
11
|
+
maxLogChars?: number;
|
|
12
|
+
/** Maximum number of captured log lines returned in the result. */
|
|
13
|
+
maxLogLines?: number;
|
|
14
|
+
/** Guest memory limit in bytes. */
|
|
15
|
+
memoryLimitBytes?: number;
|
|
16
|
+
/** Wall-clock execution timeout in milliseconds. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/isolatedVmExecutor.d.ts
|
|
21
|
+
/**
|
|
22
|
+
* isolated-vm-backed executor for one-shot sandboxed JavaScript runs.
|
|
23
|
+
*/
|
|
24
|
+
declare class IsolatedVmExecutor implements Executor {
|
|
25
|
+
private readonly loadModule;
|
|
26
|
+
private readonly maxLogChars;
|
|
27
|
+
private readonly maxLogLines;
|
|
28
|
+
private readonly memoryLimitBytes;
|
|
29
|
+
private readonly timeoutMs;
|
|
30
|
+
constructor(options?: IsolatedVmExecutorOptions);
|
|
31
|
+
/**
|
|
32
|
+
* Executes JavaScript against the provided tool namespaces in a fresh isolated-vm context.
|
|
33
|
+
*/
|
|
34
|
+
execute(code: string, providers: ResolvedToolProvider[]): Promise<ExecuteResult>;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { IsolatedVmExecutor, type IsolatedVmExecutorOptions };
|
|
38
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/isolatedVmExecutor.ts"],"sourcesContent":[],"mappings":";;;;;;AAGiB,UAAA,yBAAA,CAEI;;qBAAA;;EC+eR,WAAA,CAAA,EAAA,MAAA;EAOU;EAmBR,WAAA,CAAA,EAAA,MAAA;EACF;EAAR,gBAAA,CAAA,EAAA,MAAA;EA3BsC;EAAQ,SAAA,CAAA,EAAA,MAAA;;;;;ADjfnD;;cCifa,kBAAA,YAA8B;;EAA9B,iBAAA,WAAmB;EAOT,iBAAA,WAAA;EAmBR,iBAAA,gBAAA;EACF,iBAAA,SAAA;EAAR,WAAA,CAAA,OAAA,CAAA,EApBkB,yBAoBlB;EA3BsC;;;mCA0B5B,yBACV,QAAQ"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { ExecuteFailure, isExecuteFailure, normalizeCode } from "@mcploom/codexec";
|
|
2
|
+
|
|
3
|
+
//#region src/isolatedVmExecutor.ts
|
|
4
|
+
const DEFAULT_MEMORY_LIMIT_BYTES = 64 * 1024 * 1024;
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 5e3;
|
|
6
|
+
const DEFAULT_MAX_LOG_LINES = 100;
|
|
7
|
+
const DEFAULT_MAX_LOG_CHARS = 64e3;
|
|
8
|
+
const HOST_ERROR_PREFIX = "__MCP_CODE_EXEC_HOST_ERROR__";
|
|
9
|
+
let cachedModulePromise;
|
|
10
|
+
function createExecutionContext(signal, providerName, safeToolName, originalToolName) {
|
|
11
|
+
return {
|
|
12
|
+
originalToolName,
|
|
13
|
+
providerName,
|
|
14
|
+
safeToolName,
|
|
15
|
+
signal
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function hasRequiredNodeFlag() {
|
|
19
|
+
const execArgv = process.execArgv.join(" ");
|
|
20
|
+
const nodeOptions = process.env.NODE_OPTIONS ?? "";
|
|
21
|
+
return execArgv.includes("--no-node-snapshot") || nodeOptions.includes("--no-node-snapshot");
|
|
22
|
+
}
|
|
23
|
+
function isKnownErrorCode(value) {
|
|
24
|
+
return value === "timeout" || value === "memory_limit" || value === "validation_error" || value === "tool_error" || value === "runtime_error" || value === "serialization_error" || value === "internal_error";
|
|
25
|
+
}
|
|
26
|
+
function normalizeThrownMessage(error) {
|
|
27
|
+
if (error instanceof Error) return error.message;
|
|
28
|
+
if (typeof error === "object" && error !== null && "message" in error) {
|
|
29
|
+
const message = error.message;
|
|
30
|
+
if (typeof message === "string") return message;
|
|
31
|
+
}
|
|
32
|
+
return String(error);
|
|
33
|
+
}
|
|
34
|
+
function truncateLogs(logs, maxLogLines, maxLogChars) {
|
|
35
|
+
const limitedLines = logs.slice(0, maxLogLines);
|
|
36
|
+
let remainingChars = maxLogChars;
|
|
37
|
+
const truncated = [];
|
|
38
|
+
for (const line of limitedLines) {
|
|
39
|
+
if (remainingChars <= 0) break;
|
|
40
|
+
if (line.length <= remainingChars) {
|
|
41
|
+
truncated.push(line);
|
|
42
|
+
remainingChars -= line.length;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
truncated.push(line.slice(0, remainingChars));
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
return truncated;
|
|
49
|
+
}
|
|
50
|
+
function formatLogValue(value) {
|
|
51
|
+
if (typeof value === "string") return value;
|
|
52
|
+
if (value === void 0) return "undefined";
|
|
53
|
+
try {
|
|
54
|
+
return JSON.stringify(value);
|
|
55
|
+
} catch {
|
|
56
|
+
return String(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function formatConsoleLine(values) {
|
|
60
|
+
return values.map((value) => formatLogValue(value)).join(" ");
|
|
61
|
+
}
|
|
62
|
+
function toJsonValue(value, message) {
|
|
63
|
+
if (value === void 0) return;
|
|
64
|
+
const jsonValue = JSON.stringify(value);
|
|
65
|
+
if (jsonValue === void 0) throw new ExecuteFailure("serialization_error", message);
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(jsonValue);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new ExecuteFailure("serialization_error", message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function toTransferableValue(ivm, value) {
|
|
73
|
+
const normalizedValue = toJsonValue(value, "Host value is not JSON-serializable");
|
|
74
|
+
if (normalizedValue === null || normalizedValue === void 0 || typeof normalizedValue === "string" || typeof normalizedValue === "number" || typeof normalizedValue === "boolean") return normalizedValue;
|
|
75
|
+
return new ivm.ExternalCopy(normalizedValue).copyInto({ release: true });
|
|
76
|
+
}
|
|
77
|
+
function toExecuteError(error, deadline) {
|
|
78
|
+
if (typeof error === "object" && error !== null && "code" in error && "message" in error && isKnownErrorCode(error.code) && typeof error.message === "string") return {
|
|
79
|
+
code: error.code,
|
|
80
|
+
message: error.message
|
|
81
|
+
};
|
|
82
|
+
if (isExecuteFailure(error)) return {
|
|
83
|
+
code: error.code,
|
|
84
|
+
message: error.message
|
|
85
|
+
};
|
|
86
|
+
const message = normalizeThrownMessage(error);
|
|
87
|
+
const normalizedMessage = message.toLowerCase();
|
|
88
|
+
if (Date.now() > deadline || normalizedMessage.includes("timed out") || normalizedMessage.includes("time limit")) return {
|
|
89
|
+
code: "timeout",
|
|
90
|
+
message: "Execution timed out"
|
|
91
|
+
};
|
|
92
|
+
if (normalizedMessage.includes("memory limit") || normalizedMessage.includes("out of memory")) return {
|
|
93
|
+
code: "memory_limit",
|
|
94
|
+
message
|
|
95
|
+
};
|
|
96
|
+
if (normalizedMessage.includes("could not be cloned") || normalizedMessage.includes("non-transferable") || normalizedMessage.includes("not json-serializable")) return {
|
|
97
|
+
code: "serialization_error",
|
|
98
|
+
message
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
code: "runtime_error",
|
|
102
|
+
message
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function toMemoryLimitMb(memoryLimitBytes) {
|
|
106
|
+
return Math.max(8, Math.ceil(memoryLimitBytes / (1024 * 1024)));
|
|
107
|
+
}
|
|
108
|
+
function remainingTime(deadline) {
|
|
109
|
+
return Math.max(0, deadline - Date.now());
|
|
110
|
+
}
|
|
111
|
+
async function runWithDeadline(operation, deadline, signal) {
|
|
112
|
+
if (signal.aborted || Date.now() > deadline) throw new ExecuteFailure("timeout", "Execution timed out");
|
|
113
|
+
const timeoutMs = remainingTime(deadline);
|
|
114
|
+
if (timeoutMs <= 0) throw new ExecuteFailure("timeout", "Execution timed out");
|
|
115
|
+
let timeoutId;
|
|
116
|
+
return await new Promise((resolve, reject) => {
|
|
117
|
+
const onAbort = () => {
|
|
118
|
+
reject(new ExecuteFailure("timeout", "Execution timed out"));
|
|
119
|
+
};
|
|
120
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
121
|
+
timeoutId = setTimeout(() => {
|
|
122
|
+
signal.removeEventListener("abort", onAbort);
|
|
123
|
+
reject(new ExecuteFailure("timeout", "Execution timed out"));
|
|
124
|
+
}, timeoutMs);
|
|
125
|
+
operation.then((value) => {
|
|
126
|
+
signal.removeEventListener("abort", onAbort);
|
|
127
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
128
|
+
resolve(value);
|
|
129
|
+
}, (error) => {
|
|
130
|
+
signal.removeEventListener("abort", onAbort);
|
|
131
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
132
|
+
reject(error);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function loadDefaultModule() {
|
|
137
|
+
cachedModulePromise ??= import("isolated-vm").then((loaded) => {
|
|
138
|
+
return "default" in loaded ? loaded.default : loaded;
|
|
139
|
+
});
|
|
140
|
+
return cachedModulePromise;
|
|
141
|
+
}
|
|
142
|
+
function createBootstrapSource(providers, timeoutMs) {
|
|
143
|
+
const lines = [
|
|
144
|
+
`const __MCP_HOST_ERROR_PREFIX = ${JSON.stringify(HOST_ERROR_PREFIX)};`,
|
|
145
|
+
"const __mcpNormalizeError = (error) => {",
|
|
146
|
+
" if (error && typeof error === 'object' && typeof error.code === 'string' && typeof error.message === 'string') {",
|
|
147
|
+
" return { code: error.code, message: error.message };",
|
|
148
|
+
" }",
|
|
149
|
+
" if (error && typeof error.message === 'string' && error.message.startsWith(__MCP_HOST_ERROR_PREFIX)) {",
|
|
150
|
+
" try {",
|
|
151
|
+
" const payload = JSON.parse(error.message.slice(__MCP_HOST_ERROR_PREFIX.length));",
|
|
152
|
+
" if (payload && typeof payload.code === 'string' && typeof payload.message === 'string') {",
|
|
153
|
+
" return payload;",
|
|
154
|
+
" }",
|
|
155
|
+
" } catch {}",
|
|
156
|
+
" }",
|
|
157
|
+
" if (error && typeof error.message === 'string') {",
|
|
158
|
+
" return { code: 'runtime_error', message: error.message };",
|
|
159
|
+
" }",
|
|
160
|
+
" return { code: 'runtime_error', message: String(error) };",
|
|
161
|
+
"};",
|
|
162
|
+
"const __mcpRaiseNormalizedError = (error) => {",
|
|
163
|
+
" const normalized = __mcpNormalizeError(error);",
|
|
164
|
+
" const wrapped = new Error(normalized.message);",
|
|
165
|
+
" wrapped.code = normalized.code;",
|
|
166
|
+
" throw wrapped;",
|
|
167
|
+
"};",
|
|
168
|
+
"const __mcpToJsonValue = (value) => {",
|
|
169
|
+
" if (typeof value === 'undefined') {",
|
|
170
|
+
" return undefined;",
|
|
171
|
+
" }",
|
|
172
|
+
" const json = JSON.stringify(value);",
|
|
173
|
+
" if (typeof json === 'undefined') {",
|
|
174
|
+
" const error = new Error('Guest code returned a non-serializable value');",
|
|
175
|
+
" error.code = 'serialization_error';",
|
|
176
|
+
" throw error;",
|
|
177
|
+
" }",
|
|
178
|
+
" return JSON.parse(json);",
|
|
179
|
+
"};",
|
|
180
|
+
"globalThis.console = {",
|
|
181
|
+
" log: (...args) => __mcp_console_log(...args),",
|
|
182
|
+
" info: (...args) => __mcp_console_info(...args),",
|
|
183
|
+
" warn: (...args) => __mcp_console_warn(...args),",
|
|
184
|
+
" error: (...args) => __mcp_console_error(...args),",
|
|
185
|
+
"};"
|
|
186
|
+
];
|
|
187
|
+
for (const provider of providers) {
|
|
188
|
+
lines.push(`globalThis.${provider.name} = {};`);
|
|
189
|
+
for (const safeToolName of Object.keys(provider.tools)) {
|
|
190
|
+
const hostReferenceName = `__mcp_tool_${provider.name}_${safeToolName}`;
|
|
191
|
+
lines.push(`globalThis.${provider.name}.${safeToolName} = async (input) => {`, " try {", ` return await ${hostReferenceName}.applySyncPromise(undefined, [input], { arguments: { copy: true }, timeout: ${timeoutMs} });`, " } catch (error) {", " __mcpRaiseNormalizedError(error);", " }", "};");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return lines.join("\n");
|
|
195
|
+
}
|
|
196
|
+
function createExecutionSource(code) {
|
|
197
|
+
return [
|
|
198
|
+
`const __mcpUserFunction = (${normalizeCode(code)});`,
|
|
199
|
+
"return (async () => {",
|
|
200
|
+
" try {",
|
|
201
|
+
" const value = await __mcpUserFunction();",
|
|
202
|
+
" return { ok: true, value: __mcpToJsonValue(value) };",
|
|
203
|
+
" } catch (error) {",
|
|
204
|
+
" return { ok: false, error: __mcpNormalizeError(error) };",
|
|
205
|
+
" }",
|
|
206
|
+
"})();"
|
|
207
|
+
].join("\n");
|
|
208
|
+
}
|
|
209
|
+
function setConsoleBindings(context, logs) {
|
|
210
|
+
const jail = context.global;
|
|
211
|
+
jail.setSync("__mcp_console_log", (...args) => {
|
|
212
|
+
logs.push(formatConsoleLine(args));
|
|
213
|
+
});
|
|
214
|
+
jail.setSync("__mcp_console_info", (...args) => {
|
|
215
|
+
logs.push(formatConsoleLine(args));
|
|
216
|
+
});
|
|
217
|
+
jail.setSync("__mcp_console_warn", (...args) => {
|
|
218
|
+
logs.push(formatConsoleLine(args));
|
|
219
|
+
});
|
|
220
|
+
jail.setSync("__mcp_console_error", (...args) => {
|
|
221
|
+
logs.push(formatConsoleLine(args));
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function setProviderBindings(ivm, context, providers, signal, deadline) {
|
|
225
|
+
const jail = context.global;
|
|
226
|
+
for (const provider of providers) for (const [safeToolName, descriptor] of Object.entries(provider.tools)) {
|
|
227
|
+
const hostReferenceName = `__mcp_tool_${provider.name}_${safeToolName}`;
|
|
228
|
+
jail.setSync(hostReferenceName, async (input) => {
|
|
229
|
+
const executionContext = createExecutionContext(signal, provider.name, safeToolName, descriptor.originalName);
|
|
230
|
+
try {
|
|
231
|
+
const normalizedInput = input === void 0 ? void 0 : toJsonValue(input, "Guest code passed a non-serializable tool input");
|
|
232
|
+
return toTransferableValue(ivm, await runWithDeadline(Promise.resolve(descriptor.execute(normalizedInput, executionContext)), deadline, signal));
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const executeError = toExecuteError(error, deadline);
|
|
235
|
+
throw new Error(`${HOST_ERROR_PREFIX}${JSON.stringify(executeError)}`, { cause: error });
|
|
236
|
+
}
|
|
237
|
+
}, { reference: true });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* isolated-vm-backed executor for one-shot sandboxed JavaScript runs.
|
|
242
|
+
*/
|
|
243
|
+
var IsolatedVmExecutor = class {
|
|
244
|
+
loadModule;
|
|
245
|
+
maxLogChars;
|
|
246
|
+
maxLogLines;
|
|
247
|
+
memoryLimitBytes;
|
|
248
|
+
timeoutMs;
|
|
249
|
+
constructor(options = {}) {
|
|
250
|
+
this.loadModule = async () => {
|
|
251
|
+
return options.loadModule ? await options.loadModule() : await loadDefaultModule();
|
|
252
|
+
};
|
|
253
|
+
this.maxLogChars = options.maxLogChars ?? DEFAULT_MAX_LOG_CHARS;
|
|
254
|
+
this.maxLogLines = options.maxLogLines ?? DEFAULT_MAX_LOG_LINES;
|
|
255
|
+
this.memoryLimitBytes = options.memoryLimitBytes ?? DEFAULT_MEMORY_LIMIT_BYTES;
|
|
256
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Executes JavaScript against the provided tool namespaces in a fresh isolated-vm context.
|
|
260
|
+
*/
|
|
261
|
+
async execute(code, providers) {
|
|
262
|
+
const startedAt = Date.now();
|
|
263
|
+
const deadline = startedAt + this.timeoutMs;
|
|
264
|
+
const logs = [];
|
|
265
|
+
const abortController = new AbortController();
|
|
266
|
+
if (Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10) >= 20 && !hasRequiredNodeFlag()) return {
|
|
267
|
+
durationMs: Date.now() - startedAt,
|
|
268
|
+
error: {
|
|
269
|
+
code: "internal_error",
|
|
270
|
+
message: "isolated-vm requires Node to run with --no-node-snapshot on Node 20+"
|
|
271
|
+
},
|
|
272
|
+
logs,
|
|
273
|
+
ok: false
|
|
274
|
+
};
|
|
275
|
+
let catastrophicErrorMessage;
|
|
276
|
+
let isolate;
|
|
277
|
+
let context;
|
|
278
|
+
try {
|
|
279
|
+
const ivm = await this.loadModule();
|
|
280
|
+
isolate = new ivm.Isolate({
|
|
281
|
+
memoryLimit: toMemoryLimitMb(this.memoryLimitBytes),
|
|
282
|
+
onCatastrophicError: (message) => {
|
|
283
|
+
catastrophicErrorMessage = message;
|
|
284
|
+
abortController.abort();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
context = await isolate.createContext();
|
|
288
|
+
setConsoleBindings(context, logs);
|
|
289
|
+
setProviderBindings(ivm, context, providers, abortController.signal, deadline);
|
|
290
|
+
await context.eval(createBootstrapSource(providers, this.timeoutMs), { timeout: this.timeoutMs });
|
|
291
|
+
const execution = await context.evalClosure(createExecutionSource(code), [], {
|
|
292
|
+
timeout: this.timeoutMs,
|
|
293
|
+
result: {
|
|
294
|
+
copy: true,
|
|
295
|
+
promise: true
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
if (catastrophicErrorMessage) return {
|
|
299
|
+
durationMs: Date.now() - startedAt,
|
|
300
|
+
error: {
|
|
301
|
+
code: "internal_error",
|
|
302
|
+
message: `isolated-vm catastrophic error: ${catastrophicErrorMessage}`
|
|
303
|
+
},
|
|
304
|
+
logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),
|
|
305
|
+
ok: false
|
|
306
|
+
};
|
|
307
|
+
if (!execution.ok) return {
|
|
308
|
+
durationMs: Date.now() - startedAt,
|
|
309
|
+
error: toExecuteError(execution.error, deadline),
|
|
310
|
+
logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),
|
|
311
|
+
ok: false
|
|
312
|
+
};
|
|
313
|
+
return {
|
|
314
|
+
durationMs: Date.now() - startedAt,
|
|
315
|
+
logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),
|
|
316
|
+
ok: true,
|
|
317
|
+
result: execution.value
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
const executeError = catastrophicErrorMessage !== void 0 ? {
|
|
321
|
+
code: "internal_error",
|
|
322
|
+
message: `isolated-vm catastrophic error: ${catastrophicErrorMessage}`
|
|
323
|
+
} : toExecuteError(error, deadline);
|
|
324
|
+
return {
|
|
325
|
+
durationMs: Date.now() - startedAt,
|
|
326
|
+
error: executeError,
|
|
327
|
+
logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),
|
|
328
|
+
ok: false
|
|
329
|
+
};
|
|
330
|
+
} finally {
|
|
331
|
+
abortController.abort();
|
|
332
|
+
context?.release?.();
|
|
333
|
+
isolate?.dispose();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
//#endregion
|
|
339
|
+
export { IsolatedVmExecutor };
|
|
340
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["cachedModulePromise: Promise<IsolatedVmModule> | undefined","truncated: string[]","timeoutId: ReturnType<typeof setTimeout> | undefined","logs: string[]","catastrophicErrorMessage: string | undefined","isolate: IsolatedVmIsolate | undefined","context: IsolatedVmContext | undefined"],"sources":["../src/isolatedVmExecutor.ts"],"sourcesContent":["import {\n ExecuteFailure,\n isExecuteFailure,\n normalizeCode,\n} from \"@mcploom/codexec\";\nimport type {\n ExecuteError,\n ExecuteResult,\n Executor,\n ResolvedToolProvider,\n ToolExecutionContext,\n} from \"@mcploom/codexec\";\n\nimport type { IsolatedVmExecutorOptions } from \"./types\";\n\ntype IsolatedVmExternalCopyInstance = {\n copyInto: (options?: { release?: boolean; transferIn?: boolean }) => unknown;\n release?: () => void;\n};\n\ntype IsolatedVmReferenceInstance = {\n setSync: (\n property: string,\n value: unknown,\n options?: Record<string, unknown>,\n ) => void;\n};\n\ntype IsolatedVmContext = {\n eval: (code: string, options?: Record<string, unknown>) => Promise<unknown>;\n evalClosure: (\n code: string,\n args?: unknown[],\n options?: Record<string, unknown>,\n ) => Promise<unknown>;\n global: IsolatedVmReferenceInstance;\n release?: () => void;\n};\n\ntype IsolatedVmIsolate = {\n createContext: () => Promise<IsolatedVmContext>;\n dispose: () => void;\n};\n\ntype IsolatedVmModule = {\n ExternalCopy: new (\n value: unknown,\n options?: Record<string, unknown>,\n ) => IsolatedVmExternalCopyInstance;\n Isolate: new (options?: {\n inspector?: boolean;\n memoryLimit?: number;\n onCatastrophicError?: (message: string) => void;\n }) => IsolatedVmIsolate;\n};\n\ntype GuestExecutionEnvelope =\n | {\n ok: true;\n value: unknown;\n }\n | {\n error: {\n code?: string;\n message?: string;\n };\n ok: false;\n };\n\nconst DEFAULT_MEMORY_LIMIT_BYTES = 64 * 1024 * 1024;\nconst DEFAULT_TIMEOUT_MS = 5000;\nconst DEFAULT_MAX_LOG_LINES = 100;\nconst DEFAULT_MAX_LOG_CHARS = 64_000;\nconst HOST_ERROR_PREFIX = \"__MCP_CODE_EXEC_HOST_ERROR__\";\n\nlet cachedModulePromise: Promise<IsolatedVmModule> | undefined;\n\nfunction createExecutionContext(\n signal: AbortSignal,\n providerName: string,\n safeToolName: string,\n originalToolName: string,\n): ToolExecutionContext {\n return {\n originalToolName,\n providerName,\n safeToolName,\n signal,\n };\n}\n\nfunction hasRequiredNodeFlag(): boolean {\n const execArgv = process.execArgv.join(\" \");\n const nodeOptions = process.env.NODE_OPTIONS ?? \"\";\n return (\n execArgv.includes(\"--no-node-snapshot\") ||\n nodeOptions.includes(\"--no-node-snapshot\")\n );\n}\n\nfunction isKnownErrorCode(value: unknown): value is ExecuteError[\"code\"] {\n return (\n value === \"timeout\" ||\n value === \"memory_limit\" ||\n value === \"validation_error\" ||\n value === \"tool_error\" ||\n value === \"runtime_error\" ||\n value === \"serialization_error\" ||\n value === \"internal_error\"\n );\n}\n\nfunction normalizeThrownMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n\n if (typeof error === \"object\" && error !== null && \"message\" in error) {\n const message = (error as { message?: unknown }).message;\n if (typeof message === \"string\") {\n return message;\n }\n }\n\n return String(error);\n}\n\nfunction truncateLogs(\n logs: string[],\n maxLogLines: number,\n maxLogChars: number,\n): string[] {\n const limitedLines = logs.slice(0, maxLogLines);\n let remainingChars = maxLogChars;\n const truncated: string[] = [];\n\n for (const line of limitedLines) {\n if (remainingChars <= 0) {\n break;\n }\n\n if (line.length <= remainingChars) {\n truncated.push(line);\n remainingChars -= line.length;\n continue;\n }\n\n truncated.push(line.slice(0, remainingChars));\n break;\n }\n\n return truncated;\n}\n\nfunction formatLogValue(value: unknown): string {\n if (typeof value === \"string\") {\n return value;\n }\n\n if (value === undefined) {\n return \"undefined\";\n }\n\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\nfunction formatConsoleLine(values: unknown[]): string {\n return values.map((value) => formatLogValue(value)).join(\" \");\n}\n\nfunction toJsonValue(value: unknown, message: string): unknown {\n if (value === undefined) {\n return undefined;\n }\n\n const jsonValue = JSON.stringify(value);\n\n if (jsonValue === undefined) {\n throw new ExecuteFailure(\"serialization_error\", message);\n }\n\n try {\n return JSON.parse(jsonValue) as unknown;\n } catch {\n throw new ExecuteFailure(\"serialization_error\", message);\n }\n}\n\nfunction toTransferableValue(ivm: IsolatedVmModule, value: unknown): unknown {\n const normalizedValue = toJsonValue(\n value,\n \"Host value is not JSON-serializable\",\n );\n\n if (\n normalizedValue === null ||\n normalizedValue === undefined ||\n typeof normalizedValue === \"string\" ||\n typeof normalizedValue === \"number\" ||\n typeof normalizedValue === \"boolean\"\n ) {\n return normalizedValue;\n }\n\n const copy = new ivm.ExternalCopy(normalizedValue);\n return copy.copyInto({ release: true });\n}\n\nfunction toExecuteError(error: unknown, deadline: number): ExecuteError {\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n \"message\" in error &&\n isKnownErrorCode((error as { code?: unknown }).code) &&\n typeof (error as { message?: unknown }).message === \"string\"\n ) {\n return {\n code: (error as { code: ExecuteError[\"code\"] }).code,\n message: (error as { message: string }).message,\n };\n }\n\n if (isExecuteFailure(error)) {\n return {\n code: error.code,\n message: error.message,\n };\n }\n\n const message = normalizeThrownMessage(error);\n const normalizedMessage = message.toLowerCase();\n\n if (\n Date.now() > deadline ||\n normalizedMessage.includes(\"timed out\") ||\n normalizedMessage.includes(\"time limit\")\n ) {\n return {\n code: \"timeout\",\n message: \"Execution timed out\",\n };\n }\n\n if (\n normalizedMessage.includes(\"memory limit\") ||\n normalizedMessage.includes(\"out of memory\")\n ) {\n return {\n code: \"memory_limit\",\n message,\n };\n }\n\n if (\n normalizedMessage.includes(\"could not be cloned\") ||\n normalizedMessage.includes(\"non-transferable\") ||\n normalizedMessage.includes(\"not json-serializable\")\n ) {\n return {\n code: \"serialization_error\",\n message,\n };\n }\n\n return {\n code: \"runtime_error\",\n message,\n };\n}\n\nfunction toMemoryLimitMb(memoryLimitBytes: number): number {\n return Math.max(8, Math.ceil(memoryLimitBytes / (1024 * 1024)));\n}\n\nfunction remainingTime(deadline: number): number {\n return Math.max(0, deadline - Date.now());\n}\n\nasync function runWithDeadline<T>(\n operation: Promise<T>,\n deadline: number,\n signal: AbortSignal,\n): Promise<T> {\n if (signal.aborted || Date.now() > deadline) {\n throw new ExecuteFailure(\"timeout\", \"Execution timed out\");\n }\n\n const timeoutMs = remainingTime(deadline);\n if (timeoutMs <= 0) {\n throw new ExecuteFailure(\"timeout\", \"Execution timed out\");\n }\n\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n return await new Promise<T>((resolve, reject) => {\n const onAbort = () => {\n reject(new ExecuteFailure(\"timeout\", \"Execution timed out\"));\n };\n\n signal.addEventListener(\"abort\", onAbort, { once: true });\n timeoutId = setTimeout(() => {\n signal.removeEventListener(\"abort\", onAbort);\n reject(new ExecuteFailure(\"timeout\", \"Execution timed out\"));\n }, timeoutMs);\n\n void operation.then(\n (value) => {\n signal.removeEventListener(\"abort\", onAbort);\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n resolve(value);\n },\n (error) => {\n signal.removeEventListener(\"abort\", onAbort);\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n reject(error);\n },\n );\n });\n}\n\nasync function loadDefaultModule(): Promise<IsolatedVmModule> {\n cachedModulePromise ??= import(\"isolated-vm\").then((loaded) => {\n const candidate = (\n \"default\" in loaded ? loaded.default : loaded\n ) as unknown;\n return candidate as IsolatedVmModule;\n });\n\n return cachedModulePromise;\n}\n\nfunction createBootstrapSource(\n providers: ResolvedToolProvider[],\n timeoutMs: number,\n): string {\n const lines = [\n `const __MCP_HOST_ERROR_PREFIX = ${JSON.stringify(HOST_ERROR_PREFIX)};`,\n \"const __mcpNormalizeError = (error) => {\",\n \" if (error && typeof error === 'object' && typeof error.code === 'string' && typeof error.message === 'string') {\",\n \" return { code: error.code, message: error.message };\",\n \" }\",\n \" if (error && typeof error.message === 'string' && error.message.startsWith(__MCP_HOST_ERROR_PREFIX)) {\",\n \" try {\",\n \" const payload = JSON.parse(error.message.slice(__MCP_HOST_ERROR_PREFIX.length));\",\n \" if (payload && typeof payload.code === 'string' && typeof payload.message === 'string') {\",\n \" return payload;\",\n \" }\",\n \" } catch {}\",\n \" }\",\n \" if (error && typeof error.message === 'string') {\",\n \" return { code: 'runtime_error', message: error.message };\",\n \" }\",\n \" return { code: 'runtime_error', message: String(error) };\",\n \"};\",\n \"const __mcpRaiseNormalizedError = (error) => {\",\n \" const normalized = __mcpNormalizeError(error);\",\n \" const wrapped = new Error(normalized.message);\",\n \" wrapped.code = normalized.code;\",\n \" throw wrapped;\",\n \"};\",\n \"const __mcpToJsonValue = (value) => {\",\n \" if (typeof value === 'undefined') {\",\n \" return undefined;\",\n \" }\",\n \" const json = JSON.stringify(value);\",\n \" if (typeof json === 'undefined') {\",\n \" const error = new Error('Guest code returned a non-serializable value');\",\n \" error.code = 'serialization_error';\",\n \" throw error;\",\n \" }\",\n \" return JSON.parse(json);\",\n \"};\",\n \"globalThis.console = {\",\n \" log: (...args) => __mcp_console_log(...args),\",\n \" info: (...args) => __mcp_console_info(...args),\",\n \" warn: (...args) => __mcp_console_warn(...args),\",\n \" error: (...args) => __mcp_console_error(...args),\",\n \"};\",\n ];\n\n for (const provider of providers) {\n lines.push(`globalThis.${provider.name} = {};`);\n\n for (const safeToolName of Object.keys(provider.tools)) {\n const hostReferenceName = `__mcp_tool_${provider.name}_${safeToolName}`;\n lines.push(\n `globalThis.${provider.name}.${safeToolName} = async (input) => {`,\n \" try {\",\n ` return await ${hostReferenceName}.applySyncPromise(undefined, [input], { arguments: { copy: true }, timeout: ${timeoutMs} });`,\n \" } catch (error) {\",\n \" __mcpRaiseNormalizedError(error);\",\n \" }\",\n \"};\",\n );\n }\n }\n\n return lines.join(\"\\n\");\n}\n\nfunction createExecutionSource(code: string): string {\n const executableSource = normalizeCode(code);\n\n return [\n `const __mcpUserFunction = (${executableSource});`,\n \"return (async () => {\",\n \" try {\",\n \" const value = await __mcpUserFunction();\",\n \" return { ok: true, value: __mcpToJsonValue(value) };\",\n \" } catch (error) {\",\n \" return { ok: false, error: __mcpNormalizeError(error) };\",\n \" }\",\n \"})();\",\n ].join(\"\\n\");\n}\n\nfunction setConsoleBindings(context: IsolatedVmContext, logs: string[]): void {\n const jail = context.global;\n jail.setSync(\"__mcp_console_log\", (...args: unknown[]) => {\n logs.push(formatConsoleLine(args));\n });\n jail.setSync(\"__mcp_console_info\", (...args: unknown[]) => {\n logs.push(formatConsoleLine(args));\n });\n jail.setSync(\"__mcp_console_warn\", (...args: unknown[]) => {\n logs.push(formatConsoleLine(args));\n });\n jail.setSync(\"__mcp_console_error\", (...args: unknown[]) => {\n logs.push(formatConsoleLine(args));\n });\n}\n\nfunction setProviderBindings(\n ivm: IsolatedVmModule,\n context: IsolatedVmContext,\n providers: ResolvedToolProvider[],\n signal: AbortSignal,\n deadline: number,\n): void {\n const jail = context.global;\n\n for (const provider of providers) {\n for (const [safeToolName, descriptor] of Object.entries(provider.tools)) {\n const hostReferenceName = `__mcp_tool_${provider.name}_${safeToolName}`;\n\n jail.setSync(\n hostReferenceName,\n async (input: unknown) => {\n const executionContext = createExecutionContext(\n signal,\n provider.name,\n safeToolName,\n descriptor.originalName,\n );\n\n try {\n const normalizedInput =\n input === undefined\n ? undefined\n : toJsonValue(\n input,\n \"Guest code passed a non-serializable tool input\",\n );\n const result = await runWithDeadline(\n Promise.resolve(\n descriptor.execute(normalizedInput, executionContext),\n ),\n deadline,\n signal,\n );\n\n return toTransferableValue(ivm, result);\n } catch (error) {\n const executeError = toExecuteError(error, deadline);\n throw new Error(\n `${HOST_ERROR_PREFIX}${JSON.stringify(executeError)}`,\n {\n cause: error,\n },\n );\n }\n },\n { reference: true },\n );\n }\n }\n}\n\n/**\n * isolated-vm-backed executor for one-shot sandboxed JavaScript runs.\n */\nexport class IsolatedVmExecutor implements Executor {\n private readonly loadModule: () => Promise<IsolatedVmModule>;\n private readonly maxLogChars: number;\n private readonly maxLogLines: number;\n private readonly memoryLimitBytes: number;\n private readonly timeoutMs: number;\n\n constructor(options: IsolatedVmExecutorOptions = {}) {\n this.loadModule = async () => {\n const loaded = options.loadModule\n ? await options.loadModule()\n : await loadDefaultModule();\n return loaded as IsolatedVmModule;\n };\n this.maxLogChars = options.maxLogChars ?? DEFAULT_MAX_LOG_CHARS;\n this.maxLogLines = options.maxLogLines ?? DEFAULT_MAX_LOG_LINES;\n this.memoryLimitBytes =\n options.memoryLimitBytes ?? DEFAULT_MEMORY_LIMIT_BYTES;\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n }\n\n /**\n * Executes JavaScript against the provided tool namespaces in a fresh isolated-vm context.\n */\n async execute(\n code: string,\n providers: ResolvedToolProvider[],\n ): Promise<ExecuteResult> {\n const startedAt = Date.now();\n const deadline = startedAt + this.timeoutMs;\n const logs: string[] = [];\n const abortController = new AbortController();\n const nodeMajorVersion = Number.parseInt(\n process.versions.node.split(\".\")[0] ?? \"0\",\n 10,\n );\n\n if (nodeMajorVersion >= 20 && !hasRequiredNodeFlag()) {\n return {\n durationMs: Date.now() - startedAt,\n error: {\n code: \"internal_error\",\n message:\n \"isolated-vm requires Node to run with --no-node-snapshot on Node 20+\",\n },\n logs,\n ok: false,\n };\n }\n\n let catastrophicErrorMessage: string | undefined;\n let isolate: IsolatedVmIsolate | undefined;\n let context: IsolatedVmContext | undefined;\n\n try {\n const ivm = await this.loadModule();\n isolate = new ivm.Isolate({\n memoryLimit: toMemoryLimitMb(this.memoryLimitBytes),\n onCatastrophicError: (message) => {\n catastrophicErrorMessage = message;\n abortController.abort();\n },\n });\n context = await isolate.createContext();\n\n setConsoleBindings(context, logs);\n setProviderBindings(\n ivm,\n context,\n providers,\n abortController.signal,\n deadline,\n );\n await context.eval(createBootstrapSource(providers, this.timeoutMs), {\n timeout: this.timeoutMs,\n });\n\n const execution = (await context.evalClosure(\n createExecutionSource(code),\n [],\n {\n timeout: this.timeoutMs,\n result: { copy: true, promise: true },\n },\n )) as GuestExecutionEnvelope;\n\n if (catastrophicErrorMessage) {\n return {\n durationMs: Date.now() - startedAt,\n error: {\n code: \"internal_error\",\n message: `isolated-vm catastrophic error: ${catastrophicErrorMessage}`,\n },\n logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),\n ok: false,\n };\n }\n\n if (!execution.ok) {\n return {\n durationMs: Date.now() - startedAt,\n error: toExecuteError(execution.error, deadline),\n logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),\n ok: false,\n };\n }\n\n return {\n durationMs: Date.now() - startedAt,\n logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),\n ok: true,\n result: execution.value,\n };\n } catch (error) {\n const executeError =\n catastrophicErrorMessage !== undefined\n ? {\n code: \"internal_error\" as const,\n message: `isolated-vm catastrophic error: ${catastrophicErrorMessage}`,\n }\n : toExecuteError(error, deadline);\n\n return {\n durationMs: Date.now() - startedAt,\n error: executeError,\n logs: truncateLogs(logs, this.maxLogLines, this.maxLogChars),\n ok: false,\n };\n } finally {\n abortController.abort();\n context?.release?.();\n isolate?.dispose();\n }\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcploom/codexec-isolated-vm",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "isolated-vm executor for the mcploom codexec core package.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.cjs",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"require": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsdown"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"mcp",
|
|
31
|
+
"model-context-protocol",
|
|
32
|
+
"isolated-vm",
|
|
33
|
+
"sandbox",
|
|
34
|
+
"code-execution"
|
|
35
|
+
],
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/aallam/mcploom.git",
|
|
39
|
+
"directory": "packages/codexec-isolated-vm"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/aallam/mcploom/tree/main/packages/codexec-isolated-vm#readme",
|
|
42
|
+
"bugs": "https://github.com/aallam/mcploom/issues",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@mcploom/codexec": "^0.1.0"
|
|
45
|
+
},
|
|
46
|
+
"optionalDependencies": {
|
|
47
|
+
"isolated-vm": "^6.0.1"
|
|
48
|
+
}
|
|
49
|
+
}
|