pulse-sandbox 0.0.1-alpha.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/dist/index.d.ts +56 -0
- package/dist/index.js +313 -0
- package/dist/index.js.map +1 -0
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +188 -0
- package/dist/runner.js.map +1 -0
- package/package.json +35 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
|
|
3
|
+
type JsExecutionErrorCode = 'TIMEOUT' | 'OOM' | 'RUNTIME_ERROR' | 'POLICY_BLOCKED' | 'INTERNAL';
|
|
4
|
+
interface JsExecutionError {
|
|
5
|
+
code: JsExecutionErrorCode;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
interface JsExecutionRequest {
|
|
9
|
+
code: string;
|
|
10
|
+
input?: unknown;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
interface JsExecutionResult {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
result?: unknown;
|
|
16
|
+
stdout: string;
|
|
17
|
+
stderr: string;
|
|
18
|
+
error?: JsExecutionError;
|
|
19
|
+
durationMs: number;
|
|
20
|
+
outputTruncated: boolean;
|
|
21
|
+
}
|
|
22
|
+
interface JsExecutor {
|
|
23
|
+
execute(request: JsExecutionRequest): Promise<JsExecutionResult>;
|
|
24
|
+
}
|
|
25
|
+
interface JsExecutorOptions {
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
memoryLimitMb?: number;
|
|
28
|
+
maxOutputChars?: number;
|
|
29
|
+
maxCodeLength?: number;
|
|
30
|
+
}
|
|
31
|
+
interface RunJsToolInput {
|
|
32
|
+
code: string;
|
|
33
|
+
input?: unknown;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
}
|
|
36
|
+
type RunJsToolOutput = JsExecutionResult;
|
|
37
|
+
interface RunJsToolOptions {
|
|
38
|
+
executor: JsExecutor;
|
|
39
|
+
name?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare function createJsExecutor(options?: JsExecutorOptions): JsExecutor;
|
|
44
|
+
|
|
45
|
+
declare function createRunJsTool(options: RunJsToolOptions): {
|
|
46
|
+
name: string;
|
|
47
|
+
description: string;
|
|
48
|
+
inputSchema: z.ZodObject<{
|
|
49
|
+
code: z.ZodString;
|
|
50
|
+
input: z.ZodOptional<z.ZodAny>;
|
|
51
|
+
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
execute: (input: RunJsToolInput) => Promise<RunJsToolOutput>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export { type JsExecutionError, type JsExecutionErrorCode, type JsExecutionRequest, type JsExecutionResult, type JsExecutor, type JsExecutorOptions, type RunJsToolInput, type RunJsToolOptions, type RunJsToolOutput, createJsExecutor, createRunJsTool };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// src/executor.ts
|
|
2
|
+
import { fork } from "child_process";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
8
|
+
var DEFAULT_MEMORY_LIMIT_MB = 64;
|
|
9
|
+
var DEFAULT_MAX_OUTPUT_CHARS = 2e4;
|
|
10
|
+
var DEFAULT_MAX_CODE_LENGTH = 2e4;
|
|
11
|
+
function toErrorMessage(error) {
|
|
12
|
+
if (error instanceof Error) {
|
|
13
|
+
return error.message;
|
|
14
|
+
}
|
|
15
|
+
return String(error);
|
|
16
|
+
}
|
|
17
|
+
function appendWithLimit(buffer, chunk, maxChars) {
|
|
18
|
+
const merged = buffer + chunk;
|
|
19
|
+
if (merged.length <= maxChars) {
|
|
20
|
+
return { value: merged, truncated: false };
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
value: merged.slice(0, maxChars),
|
|
24
|
+
truncated: true
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function clampText(value, maxChars) {
|
|
28
|
+
if (value.length <= maxChars) {
|
|
29
|
+
return { value, truncated: false };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
value: value.slice(0, maxChars),
|
|
33
|
+
truncated: true
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function mergeText(first, second) {
|
|
37
|
+
if (!first) {
|
|
38
|
+
return second;
|
|
39
|
+
}
|
|
40
|
+
if (!second) {
|
|
41
|
+
return first;
|
|
42
|
+
}
|
|
43
|
+
return `${first}
|
|
44
|
+
${second}`;
|
|
45
|
+
}
|
|
46
|
+
function inferExitErrorCode(signal, stderr) {
|
|
47
|
+
if (signal === "SIGABRT" || /heap out of memory/i.test(stderr)) {
|
|
48
|
+
return "OOM";
|
|
49
|
+
}
|
|
50
|
+
return "INTERNAL";
|
|
51
|
+
}
|
|
52
|
+
function createErrorResult(startedAt, stdout, stderr, errorCode, errorMessage, outputTruncated) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
stdout,
|
|
56
|
+
stderr,
|
|
57
|
+
durationMs: Date.now() - startedAt,
|
|
58
|
+
outputTruncated,
|
|
59
|
+
error: {
|
|
60
|
+
code: errorCode,
|
|
61
|
+
message: errorMessage
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function resolveRunnerPath() {
|
|
66
|
+
const requireForResolve = createRequire(import.meta.url);
|
|
67
|
+
const localDir = path.dirname(fileURLToPath(import.meta.url));
|
|
68
|
+
const localCandidates = [path.join(localDir, "runner.js"), path.join(localDir, "runner.cjs")];
|
|
69
|
+
for (const candidate of localCandidates) {
|
|
70
|
+
if (existsSync(candidate)) {
|
|
71
|
+
return candidate;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const packageEntryPath = requireForResolve.resolve("pulse-sandbox");
|
|
76
|
+
const packageDir = path.dirname(packageEntryPath);
|
|
77
|
+
const packageCandidates = [path.join(packageDir, "runner.js"), path.join(packageDir, "runner.cjs")];
|
|
78
|
+
for (const candidate of packageCandidates) {
|
|
79
|
+
if (existsSync(candidate)) {
|
|
80
|
+
return candidate;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
return localCandidates[0];
|
|
86
|
+
}
|
|
87
|
+
function createJsExecutor(options = {}) {
|
|
88
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
89
|
+
const memoryLimitMb = options.memoryLimitMb ?? DEFAULT_MEMORY_LIMIT_MB;
|
|
90
|
+
const maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
91
|
+
const maxCodeLength = options.maxCodeLength ?? DEFAULT_MAX_CODE_LENGTH;
|
|
92
|
+
return {
|
|
93
|
+
async execute(request) {
|
|
94
|
+
const startedAt = Date.now();
|
|
95
|
+
const effectiveTimeoutMs = request.timeoutMs ?? timeoutMs;
|
|
96
|
+
if (!Number.isInteger(effectiveTimeoutMs) || effectiveTimeoutMs <= 0) {
|
|
97
|
+
return createErrorResult(
|
|
98
|
+
startedAt,
|
|
99
|
+
"",
|
|
100
|
+
"",
|
|
101
|
+
"POLICY_BLOCKED",
|
|
102
|
+
"timeoutMs must be a positive integer.",
|
|
103
|
+
false
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (!request.code || request.code.trim().length === 0) {
|
|
107
|
+
return createErrorResult(
|
|
108
|
+
startedAt,
|
|
109
|
+
"",
|
|
110
|
+
"",
|
|
111
|
+
"POLICY_BLOCKED",
|
|
112
|
+
"code must be a non-empty string.",
|
|
113
|
+
false
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (request.code.length > maxCodeLength) {
|
|
117
|
+
return createErrorResult(
|
|
118
|
+
startedAt,
|
|
119
|
+
"",
|
|
120
|
+
"",
|
|
121
|
+
"POLICY_BLOCKED",
|
|
122
|
+
`code exceeds maxCodeLength (${maxCodeLength}).`,
|
|
123
|
+
false
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const runnerPath = resolveRunnerPath();
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
let child;
|
|
129
|
+
let finished = false;
|
|
130
|
+
let timedOut = false;
|
|
131
|
+
let stdoutBuffer = "";
|
|
132
|
+
let stderrBuffer = "";
|
|
133
|
+
let streamTruncated = false;
|
|
134
|
+
const finish = (result) => {
|
|
135
|
+
if (finished) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
finished = true;
|
|
139
|
+
resolve(result);
|
|
140
|
+
};
|
|
141
|
+
try {
|
|
142
|
+
child = fork(runnerPath, {
|
|
143
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
144
|
+
serialization: "advanced",
|
|
145
|
+
execArgv: [`--max-old-space-size=${memoryLimitMb}`]
|
|
146
|
+
});
|
|
147
|
+
} catch (error) {
|
|
148
|
+
finish(
|
|
149
|
+
createErrorResult(
|
|
150
|
+
startedAt,
|
|
151
|
+
"",
|
|
152
|
+
"",
|
|
153
|
+
"INTERNAL",
|
|
154
|
+
`Failed to start sandbox process: ${toErrorMessage(error)}`,
|
|
155
|
+
false
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const timeoutHandle = setTimeout(() => {
|
|
161
|
+
if (finished) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
timedOut = true;
|
|
165
|
+
child?.kill("SIGKILL");
|
|
166
|
+
}, effectiveTimeoutMs);
|
|
167
|
+
const collectStreamChunk = (source, chunk) => {
|
|
168
|
+
const currentBuffer = source === "stdout" ? stdoutBuffer : stderrBuffer;
|
|
169
|
+
const next = appendWithLimit(currentBuffer, chunk, maxOutputChars);
|
|
170
|
+
if (next.truncated) {
|
|
171
|
+
streamTruncated = true;
|
|
172
|
+
}
|
|
173
|
+
if (source === "stdout") {
|
|
174
|
+
stdoutBuffer = next.value;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
stderrBuffer = next.value;
|
|
178
|
+
};
|
|
179
|
+
child.stdout?.on("data", (chunk) => {
|
|
180
|
+
collectStreamChunk("stdout", String(chunk));
|
|
181
|
+
});
|
|
182
|
+
child.stderr?.on("data", (chunk) => {
|
|
183
|
+
collectStreamChunk("stderr", String(chunk));
|
|
184
|
+
});
|
|
185
|
+
child.once("error", (error) => {
|
|
186
|
+
clearTimeout(timeoutHandle);
|
|
187
|
+
finish(
|
|
188
|
+
createErrorResult(
|
|
189
|
+
startedAt,
|
|
190
|
+
stdoutBuffer,
|
|
191
|
+
stderrBuffer,
|
|
192
|
+
"INTERNAL",
|
|
193
|
+
`Sandbox process error: ${toErrorMessage(error)}`,
|
|
194
|
+
streamTruncated
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
child.once("message", (message) => {
|
|
199
|
+
clearTimeout(timeoutHandle);
|
|
200
|
+
const mergedStdout = mergeText(message.stdout, stdoutBuffer);
|
|
201
|
+
const mergedStderr = mergeText(message.stderr, stderrBuffer);
|
|
202
|
+
const clampedStdout = clampText(mergedStdout, maxOutputChars);
|
|
203
|
+
const clampedStderr = clampText(mergedStderr, maxOutputChars);
|
|
204
|
+
const outputTruncated = message.outputTruncated || streamTruncated || clampedStdout.truncated || clampedStderr.truncated;
|
|
205
|
+
if (message.type === "success") {
|
|
206
|
+
finish({
|
|
207
|
+
ok: true,
|
|
208
|
+
result: message.result,
|
|
209
|
+
stdout: clampedStdout.value,
|
|
210
|
+
stderr: clampedStderr.value,
|
|
211
|
+
durationMs: Date.now() - startedAt,
|
|
212
|
+
outputTruncated
|
|
213
|
+
});
|
|
214
|
+
if (child.connected) {
|
|
215
|
+
child.disconnect();
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
finish(
|
|
220
|
+
createErrorResult(
|
|
221
|
+
startedAt,
|
|
222
|
+
clampedStdout.value,
|
|
223
|
+
clampedStderr.value,
|
|
224
|
+
message.errorCode,
|
|
225
|
+
message.errorMessage,
|
|
226
|
+
outputTruncated
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
if (child.connected) {
|
|
230
|
+
child.disconnect();
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
child.once("exit", (_code, signal) => {
|
|
234
|
+
clearTimeout(timeoutHandle);
|
|
235
|
+
if (finished) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (timedOut) {
|
|
239
|
+
finish(
|
|
240
|
+
createErrorResult(
|
|
241
|
+
startedAt,
|
|
242
|
+
stdoutBuffer,
|
|
243
|
+
stderrBuffer,
|
|
244
|
+
"TIMEOUT",
|
|
245
|
+
`Execution timed out after ${effectiveTimeoutMs}ms.`,
|
|
246
|
+
streamTruncated
|
|
247
|
+
)
|
|
248
|
+
);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const errorCode = inferExitErrorCode(signal, stderrBuffer);
|
|
252
|
+
finish(
|
|
253
|
+
createErrorResult(
|
|
254
|
+
startedAt,
|
|
255
|
+
stdoutBuffer,
|
|
256
|
+
stderrBuffer,
|
|
257
|
+
errorCode,
|
|
258
|
+
"Sandbox process exited unexpectedly.",
|
|
259
|
+
streamTruncated
|
|
260
|
+
)
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
const payload = {
|
|
264
|
+
code: request.code,
|
|
265
|
+
input: request.input,
|
|
266
|
+
maxOutputChars
|
|
267
|
+
};
|
|
268
|
+
try {
|
|
269
|
+
child.send(payload);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
clearTimeout(timeoutHandle);
|
|
272
|
+
child.kill("SIGKILL");
|
|
273
|
+
finish(
|
|
274
|
+
createErrorResult(
|
|
275
|
+
startedAt,
|
|
276
|
+
stdoutBuffer,
|
|
277
|
+
stderrBuffer,
|
|
278
|
+
"INTERNAL",
|
|
279
|
+
`Failed to send payload to sandbox: ${toErrorMessage(error)}`,
|
|
280
|
+
streamTruncated
|
|
281
|
+
)
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/tool.ts
|
|
290
|
+
import z from "zod";
|
|
291
|
+
var DEFAULT_TOOL_NAME = "run_js";
|
|
292
|
+
var DEFAULT_TOOL_DESCRIPTION = "Execute JavaScript in an isolated sandbox process. Returns result, stdout, stderr, and execution metadata.";
|
|
293
|
+
function createRunJsTool(options) {
|
|
294
|
+
const name = options.name ?? DEFAULT_TOOL_NAME;
|
|
295
|
+
const description = options.description ?? DEFAULT_TOOL_DESCRIPTION;
|
|
296
|
+
return {
|
|
297
|
+
name,
|
|
298
|
+
description,
|
|
299
|
+
inputSchema: z.object({
|
|
300
|
+
code: z.string().describe("JavaScript source code to execute in sandbox."),
|
|
301
|
+
input: z.any().optional().describe("Optional input object exposed as global `input` in sandbox."),
|
|
302
|
+
timeoutMs: z.number().int().positive().optional().describe("Optional timeout in milliseconds. Defaults to executor timeout.")
|
|
303
|
+
}),
|
|
304
|
+
execute: async (input) => {
|
|
305
|
+
return options.executor.execute(input);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
export {
|
|
310
|
+
createJsExecutor,
|
|
311
|
+
createRunJsTool
|
|
312
|
+
};
|
|
313
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/executor.ts","../src/tool.ts"],"sourcesContent":["import { fork, type ChildProcess } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport type {\n JsExecutionErrorCode,\n JsExecutionRequest,\n JsExecutionResult,\n JsExecutor,\n JsExecutorOptions\n} from './types.js';\n\ninterface RunnerRequest {\n code: string;\n input: unknown;\n maxOutputChars: number;\n}\n\ninterface RunnerSuccessMessage {\n type: 'success';\n result: unknown;\n stdout: string;\n stderr: string;\n outputTruncated: boolean;\n}\n\ninterface RunnerErrorMessage {\n type: 'error';\n errorCode: 'RUNTIME_ERROR' | 'INTERNAL';\n errorMessage: string;\n stdout: string;\n stderr: string;\n outputTruncated: boolean;\n}\n\ntype RunnerMessage = RunnerSuccessMessage | RunnerErrorMessage;\n\nconst DEFAULT_TIMEOUT_MS = 2_000;\nconst DEFAULT_MEMORY_LIMIT_MB = 64;\nconst DEFAULT_MAX_OUTPUT_CHARS = 20_000;\nconst DEFAULT_MAX_CODE_LENGTH = 20_000;\n\nfunction toErrorMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n\n return String(error);\n}\n\nfunction appendWithLimit(buffer: string, chunk: string, maxChars: number): { value: string; truncated: boolean } {\n const merged = buffer + chunk;\n\n if (merged.length <= maxChars) {\n return { value: merged, truncated: false };\n }\n\n return {\n value: merged.slice(0, maxChars),\n truncated: true\n };\n}\n\nfunction clampText(value: string, maxChars: number): { value: string; truncated: boolean } {\n if (value.length <= maxChars) {\n return { value, truncated: false };\n }\n\n return {\n value: value.slice(0, maxChars),\n truncated: true\n };\n}\n\nfunction mergeText(first: string, second: string): string {\n if (!first) {\n return second;\n }\n\n if (!second) {\n return first;\n }\n\n return `${first}\\n${second}`;\n}\n\nfunction inferExitErrorCode(signal: NodeJS.Signals | null, stderr: string): JsExecutionErrorCode {\n if (signal === 'SIGABRT' || /heap out of memory/i.test(stderr)) {\n return 'OOM';\n }\n\n return 'INTERNAL';\n}\n\nfunction createErrorResult(\n startedAt: number,\n stdout: string,\n stderr: string,\n errorCode: JsExecutionErrorCode,\n errorMessage: string,\n outputTruncated: boolean\n): JsExecutionResult {\n return {\n ok: false,\n stdout,\n stderr,\n durationMs: Date.now() - startedAt,\n outputTruncated,\n error: {\n code: errorCode,\n message: errorMessage\n }\n };\n}\n\nfunction resolveRunnerPath(): string {\n const requireForResolve = createRequire(import.meta.url);\n const localDir = path.dirname(fileURLToPath(import.meta.url));\n const localCandidates = [path.join(localDir, 'runner.js'), path.join(localDir, 'runner.cjs')];\n\n for (const candidate of localCandidates) {\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n try {\n const packageEntryPath = requireForResolve.resolve('pulse-sandbox');\n const packageDir = path.dirname(packageEntryPath);\n const packageCandidates = [path.join(packageDir, 'runner.js'), path.join(packageDir, 'runner.cjs')];\n\n for (const candidate of packageCandidates) {\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n } catch {\n // Ignore and fall through to final fallback.\n }\n\n return localCandidates[0];\n}\n\nexport function createJsExecutor(options: JsExecutorOptions = {}): JsExecutor {\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const memoryLimitMb = options.memoryLimitMb ?? DEFAULT_MEMORY_LIMIT_MB;\n const maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;\n const maxCodeLength = options.maxCodeLength ?? DEFAULT_MAX_CODE_LENGTH;\n\n return {\n async execute(request: JsExecutionRequest): Promise<JsExecutionResult> {\n const startedAt = Date.now();\n const effectiveTimeoutMs = request.timeoutMs ?? timeoutMs;\n\n if (!Number.isInteger(effectiveTimeoutMs) || effectiveTimeoutMs <= 0) {\n return createErrorResult(\n startedAt,\n '',\n '',\n 'POLICY_BLOCKED',\n 'timeoutMs must be a positive integer.',\n false\n );\n }\n\n if (!request.code || request.code.trim().length === 0) {\n return createErrorResult(\n startedAt,\n '',\n '',\n 'POLICY_BLOCKED',\n 'code must be a non-empty string.',\n false\n );\n }\n\n if (request.code.length > maxCodeLength) {\n return createErrorResult(\n startedAt,\n '',\n '',\n 'POLICY_BLOCKED',\n `code exceeds maxCodeLength (${maxCodeLength}).`,\n false\n );\n }\n\n const runnerPath = resolveRunnerPath();\n\n return new Promise<JsExecutionResult>((resolve) => {\n let child: ChildProcess | undefined;\n let finished = false;\n let timedOut = false;\n let stdoutBuffer = '';\n let stderrBuffer = '';\n let streamTruncated = false;\n\n const finish = (result: JsExecutionResult): void => {\n if (finished) {\n return;\n }\n\n finished = true;\n resolve(result);\n };\n\n try {\n child = fork(runnerPath, {\n stdio: ['ignore', 'pipe', 'pipe', 'ipc'],\n serialization: 'advanced',\n execArgv: [`--max-old-space-size=${memoryLimitMb}`]\n });\n } catch (error) {\n finish(\n createErrorResult(\n startedAt,\n '',\n '',\n 'INTERNAL',\n `Failed to start sandbox process: ${toErrorMessage(error)}`,\n false\n )\n );\n return;\n }\n\n const timeoutHandle = setTimeout(() => {\n if (finished) {\n return;\n }\n\n timedOut = true;\n child?.kill('SIGKILL');\n }, effectiveTimeoutMs);\n\n const collectStreamChunk = (source: 'stdout' | 'stderr', chunk: string): void => {\n const currentBuffer = source === 'stdout' ? stdoutBuffer : stderrBuffer;\n const next = appendWithLimit(currentBuffer, chunk, maxOutputChars);\n\n if (next.truncated) {\n streamTruncated = true;\n }\n\n if (source === 'stdout') {\n stdoutBuffer = next.value;\n return;\n }\n\n stderrBuffer = next.value;\n };\n\n child.stdout?.on('data', (chunk: Buffer | string) => {\n collectStreamChunk('stdout', String(chunk));\n });\n\n child.stderr?.on('data', (chunk: Buffer | string) => {\n collectStreamChunk('stderr', String(chunk));\n });\n\n child.once('error', (error) => {\n clearTimeout(timeoutHandle);\n finish(\n createErrorResult(\n startedAt,\n stdoutBuffer,\n stderrBuffer,\n 'INTERNAL',\n `Sandbox process error: ${toErrorMessage(error)}`,\n streamTruncated\n )\n );\n });\n\n child.once('message', (message: RunnerMessage) => {\n clearTimeout(timeoutHandle);\n\n const mergedStdout = mergeText(message.stdout, stdoutBuffer);\n const mergedStderr = mergeText(message.stderr, stderrBuffer);\n\n const clampedStdout = clampText(mergedStdout, maxOutputChars);\n const clampedStderr = clampText(mergedStderr, maxOutputChars);\n const outputTruncated =\n message.outputTruncated ||\n streamTruncated ||\n clampedStdout.truncated ||\n clampedStderr.truncated;\n\n if (message.type === 'success') {\n finish({\n ok: true,\n result: message.result,\n stdout: clampedStdout.value,\n stderr: clampedStderr.value,\n durationMs: Date.now() - startedAt,\n outputTruncated\n });\n if (child.connected) {\n child.disconnect();\n }\n return;\n }\n\n finish(\n createErrorResult(\n startedAt,\n clampedStdout.value,\n clampedStderr.value,\n message.errorCode,\n message.errorMessage,\n outputTruncated\n )\n );\n if (child.connected) {\n child.disconnect();\n }\n });\n\n child.once('exit', (_code, signal) => {\n clearTimeout(timeoutHandle);\n\n if (finished) {\n return;\n }\n\n if (timedOut) {\n finish(\n createErrorResult(\n startedAt,\n stdoutBuffer,\n stderrBuffer,\n 'TIMEOUT',\n `Execution timed out after ${effectiveTimeoutMs}ms.`,\n streamTruncated\n )\n );\n return;\n }\n\n const errorCode = inferExitErrorCode(signal, stderrBuffer);\n finish(\n createErrorResult(\n startedAt,\n stdoutBuffer,\n stderrBuffer,\n errorCode,\n 'Sandbox process exited unexpectedly.',\n streamTruncated\n )\n );\n });\n\n const payload: RunnerRequest = {\n code: request.code,\n input: request.input,\n maxOutputChars\n };\n\n try {\n child.send(payload);\n } catch (error) {\n clearTimeout(timeoutHandle);\n child.kill('SIGKILL');\n finish(\n createErrorResult(\n startedAt,\n stdoutBuffer,\n stderrBuffer,\n 'INTERNAL',\n `Failed to send payload to sandbox: ${toErrorMessage(error)}`,\n streamTruncated\n )\n );\n }\n });\n }\n };\n}\n","import z from 'zod';\n\nimport type { RunJsToolInput, RunJsToolOptions, RunJsToolOutput } from './types.js';\n\nconst DEFAULT_TOOL_NAME = 'run_js';\nconst DEFAULT_TOOL_DESCRIPTION =\n 'Execute JavaScript in an isolated sandbox process. Returns result, stdout, stderr, and execution metadata.';\n\nexport function createRunJsTool(options: RunJsToolOptions) {\n const name = options.name ?? DEFAULT_TOOL_NAME;\n const description = options.description ?? DEFAULT_TOOL_DESCRIPTION;\n\n return {\n name,\n description,\n inputSchema: z.object({\n code: z.string().describe('JavaScript source code to execute in sandbox.'),\n input: z.any().optional().describe('Optional input object exposed as global `input` in sandbox.'),\n timeoutMs: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('Optional timeout in milliseconds. Defaults to executor timeout.')\n }),\n execute: async (input: RunJsToolInput): Promise<RunJsToolOutput> => {\n return options.executor.execute(input);\n }\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/runner.d.ts
ADDED
package/dist/runner.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// src/runner.ts
|
|
2
|
+
import vm from "vm";
|
|
3
|
+
import { inspect } from "util";
|
|
4
|
+
function toErrorMessage(error) {
|
|
5
|
+
if (error instanceof Error) {
|
|
6
|
+
return error.stack ?? error.message;
|
|
7
|
+
}
|
|
8
|
+
return String(error);
|
|
9
|
+
}
|
|
10
|
+
function appendWithLimit(buffer, chunk, maxChars) {
|
|
11
|
+
const merged = buffer + chunk;
|
|
12
|
+
if (merged.length <= maxChars) {
|
|
13
|
+
return { value: merged, truncated: false };
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
value: merged.slice(0, maxChars),
|
|
17
|
+
truncated: true
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function formatLogArgs(args) {
|
|
21
|
+
return args.map((arg) => {
|
|
22
|
+
if (typeof arg === "string") {
|
|
23
|
+
return arg;
|
|
24
|
+
}
|
|
25
|
+
return inspect(arg, {
|
|
26
|
+
depth: 4,
|
|
27
|
+
breakLength: 120,
|
|
28
|
+
compact: true,
|
|
29
|
+
maxArrayLength: 100
|
|
30
|
+
});
|
|
31
|
+
}).join(" ");
|
|
32
|
+
}
|
|
33
|
+
function createConsoleProxy(append) {
|
|
34
|
+
return {
|
|
35
|
+
log: (...args) => append("stdout", `${formatLogArgs(args)}
|
|
36
|
+
`),
|
|
37
|
+
info: (...args) => append("stdout", `${formatLogArgs(args)}
|
|
38
|
+
`),
|
|
39
|
+
warn: (...args) => append("stderr", `${formatLogArgs(args)}
|
|
40
|
+
`),
|
|
41
|
+
error: (...args) => append("stderr", `${formatLogArgs(args)}
|
|
42
|
+
`),
|
|
43
|
+
debug: (...args) => append("stdout", `${formatLogArgs(args)}
|
|
44
|
+
`),
|
|
45
|
+
trace: (...args) => append("stderr", `${formatLogArgs(args)}
|
|
46
|
+
`),
|
|
47
|
+
dir: (item) => append("stdout", `${inspect(item)}
|
|
48
|
+
`),
|
|
49
|
+
dirxml: (item) => append("stdout", `${inspect(item)}
|
|
50
|
+
`),
|
|
51
|
+
assert: (condition, ...args) => {
|
|
52
|
+
if (!condition) {
|
|
53
|
+
append("stderr", `${formatLogArgs(args.length ? args : ["Assertion failed"])}
|
|
54
|
+
`);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
clear: () => {
|
|
58
|
+
},
|
|
59
|
+
count: () => {
|
|
60
|
+
},
|
|
61
|
+
countReset: () => {
|
|
62
|
+
},
|
|
63
|
+
group: (...args) => append("stdout", `${formatLogArgs(args)}
|
|
64
|
+
`),
|
|
65
|
+
groupCollapsed: (...args) => append("stdout", `${formatLogArgs(args)}
|
|
66
|
+
`),
|
|
67
|
+
groupEnd: () => {
|
|
68
|
+
},
|
|
69
|
+
table: (tabularData) => append("stdout", `${inspect(tabularData, { depth: 3 })}
|
|
70
|
+
`),
|
|
71
|
+
time: () => {
|
|
72
|
+
},
|
|
73
|
+
timeLog: (...args) => append("stdout", `${formatLogArgs(args)}
|
|
74
|
+
`),
|
|
75
|
+
timeEnd: (...args) => append("stdout", `${formatLogArgs(args)}
|
|
76
|
+
`),
|
|
77
|
+
timeStamp: () => {
|
|
78
|
+
},
|
|
79
|
+
profile: () => {
|
|
80
|
+
},
|
|
81
|
+
profileEnd: () => {
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function buildSandbox(input, consoleProxy) {
|
|
86
|
+
const sandbox = {
|
|
87
|
+
input,
|
|
88
|
+
console: consoleProxy,
|
|
89
|
+
fetch: void 0,
|
|
90
|
+
WebSocket: void 0,
|
|
91
|
+
EventSource: void 0,
|
|
92
|
+
require: void 0,
|
|
93
|
+
process: void 0,
|
|
94
|
+
module: void 0,
|
|
95
|
+
exports: void 0,
|
|
96
|
+
Buffer: void 0,
|
|
97
|
+
setTimeout,
|
|
98
|
+
clearTimeout,
|
|
99
|
+
setInterval,
|
|
100
|
+
clearInterval
|
|
101
|
+
};
|
|
102
|
+
sandbox.globalThis = sandbox;
|
|
103
|
+
sandbox.global = sandbox;
|
|
104
|
+
return vm.createContext(sandbox, {
|
|
105
|
+
codeGeneration: {
|
|
106
|
+
strings: false,
|
|
107
|
+
wasm: false
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function executeInSandbox(payload) {
|
|
112
|
+
let stdout = "";
|
|
113
|
+
let stderr = "";
|
|
114
|
+
let outputTruncated = false;
|
|
115
|
+
const append = (stream, text) => {
|
|
116
|
+
if (stream === "stdout") {
|
|
117
|
+
const next2 = appendWithLimit(stdout, text, payload.maxOutputChars);
|
|
118
|
+
stdout = next2.value;
|
|
119
|
+
if (next2.truncated) {
|
|
120
|
+
outputTruncated = true;
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const next = appendWithLimit(stderr, text, payload.maxOutputChars);
|
|
125
|
+
stderr = next.value;
|
|
126
|
+
if (next.truncated) {
|
|
127
|
+
outputTruncated = true;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
const consoleProxy = createConsoleProxy(append);
|
|
132
|
+
const context = buildSandbox(payload.input, consoleProxy);
|
|
133
|
+
const wrappedCode = `'use strict';
|
|
134
|
+
(async () => {
|
|
135
|
+
${payload.code}
|
|
136
|
+
})()`;
|
|
137
|
+
const script = new vm.Script(wrappedCode, { filename: "pulse-sandbox-user-code.js" });
|
|
138
|
+
const result = await script.runInContext(context);
|
|
139
|
+
return {
|
|
140
|
+
type: "success",
|
|
141
|
+
result,
|
|
142
|
+
stdout,
|
|
143
|
+
stderr,
|
|
144
|
+
outputTruncated
|
|
145
|
+
};
|
|
146
|
+
} catch (error) {
|
|
147
|
+
append("stderr", `${toErrorMessage(error)}
|
|
148
|
+
`);
|
|
149
|
+
return {
|
|
150
|
+
type: "error",
|
|
151
|
+
errorCode: "RUNTIME_ERROR",
|
|
152
|
+
errorMessage: toErrorMessage(error),
|
|
153
|
+
stdout,
|
|
154
|
+
stderr,
|
|
155
|
+
outputTruncated
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function sendAndExit(message, exitCode) {
|
|
160
|
+
if (!process.send) {
|
|
161
|
+
process.exit(exitCode);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
process.send(message, (error) => {
|
|
165
|
+
if (error) {
|
|
166
|
+
process.exit(1);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
process.exit(exitCode);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
process.once("message", async (payload) => {
|
|
173
|
+
try {
|
|
174
|
+
const result = await executeInSandbox(payload);
|
|
175
|
+
sendAndExit(result, 0);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const fallback = {
|
|
178
|
+
type: "error",
|
|
179
|
+
errorCode: "INTERNAL",
|
|
180
|
+
errorMessage: toErrorMessage(error),
|
|
181
|
+
stdout: "",
|
|
182
|
+
stderr: toErrorMessage(error),
|
|
183
|
+
outputTruncated: false
|
|
184
|
+
};
|
|
185
|
+
sendAndExit(fallback, 1);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/runner.ts"],"sourcesContent":["import vm from 'vm';\nimport { inspect } from 'util';\n\ninterface RunnerRequest {\n code: string;\n input: unknown;\n maxOutputChars: number;\n}\n\ninterface RunnerSuccessMessage {\n type: 'success';\n result: unknown;\n stdout: string;\n stderr: string;\n outputTruncated: boolean;\n}\n\ninterface RunnerErrorMessage {\n type: 'error';\n errorCode: 'RUNTIME_ERROR' | 'INTERNAL';\n errorMessage: string;\n stdout: string;\n stderr: string;\n outputTruncated: boolean;\n}\n\ntype RunnerMessage = RunnerSuccessMessage | RunnerErrorMessage;\n\nfunction toErrorMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.stack ?? error.message;\n }\n\n return String(error);\n}\n\nfunction appendWithLimit(buffer: string, chunk: string, maxChars: number): { value: string; truncated: boolean } {\n const merged = buffer + chunk;\n\n if (merged.length <= maxChars) {\n return { value: merged, truncated: false };\n }\n\n return {\n value: merged.slice(0, maxChars),\n truncated: true\n };\n}\n\nfunction formatLogArgs(args: unknown[]): string {\n return args\n .map((arg) => {\n if (typeof arg === 'string') {\n return arg;\n }\n\n return inspect(arg, {\n depth: 4,\n breakLength: 120,\n compact: true,\n maxArrayLength: 100\n });\n })\n .join(' ');\n}\n\nfunction createConsoleProxy(\n append: (stream: 'stdout' | 'stderr', text: string) => void\n): Console {\n return {\n log: (...args: unknown[]) => append('stdout', `${formatLogArgs(args)}\\n`),\n info: (...args: unknown[]) => append('stdout', `${formatLogArgs(args)}\\n`),\n warn: (...args: unknown[]) => append('stderr', `${formatLogArgs(args)}\\n`),\n error: (...args: unknown[]) => append('stderr', `${formatLogArgs(args)}\\n`),\n debug: (...args: unknown[]) => append('stdout', `${formatLogArgs(args)}\\n`),\n trace: (...args: unknown[]) => append('stderr', `${formatLogArgs(args)}\\n`),\n dir: (item: unknown) => append('stdout', `${inspect(item)}\\n`),\n dirxml: (item: unknown) => append('stdout', `${inspect(item)}\\n`),\n assert: (condition: unknown, ...args: unknown[]) => {\n if (!condition) {\n append('stderr', `${formatLogArgs(args.length ? args : ['Assertion failed'])}\\n`);\n }\n },\n clear: () => {},\n count: () => {},\n countReset: () => {},\n group: (...args: unknown[]) => append('stdout', `${formatLogArgs(args)}\\n`),\n groupCollapsed: (...args: unknown[]) => append('stdout', `${formatLogArgs(args)}\\n`),\n groupEnd: () => {},\n table: (tabularData: unknown) => append('stdout', `${inspect(tabularData, { depth: 3 })}\\n`),\n time: () => {},\n timeLog: (...args: unknown[]) => append('stdout', `${formatLogArgs(args)}\\n`),\n timeEnd: (...args: unknown[]) => append('stdout', `${formatLogArgs(args)}\\n`),\n timeStamp: () => {},\n profile: () => {},\n profileEnd: () => {}\n } as unknown as Console;\n}\n\nfunction buildSandbox(input: unknown, consoleProxy: Console): vm.Context {\n const sandbox: Record<string, unknown> = {\n input,\n console: consoleProxy,\n fetch: undefined,\n WebSocket: undefined,\n EventSource: undefined,\n require: undefined,\n process: undefined,\n module: undefined,\n exports: undefined,\n Buffer: undefined,\n setTimeout,\n clearTimeout,\n setInterval,\n clearInterval\n };\n\n sandbox.globalThis = sandbox;\n sandbox.global = sandbox;\n\n return vm.createContext(sandbox, {\n codeGeneration: {\n strings: false,\n wasm: false\n }\n });\n}\n\nasync function executeInSandbox(payload: RunnerRequest): Promise<RunnerMessage> {\n let stdout = '';\n let stderr = '';\n let outputTruncated = false;\n\n const append = (stream: 'stdout' | 'stderr', text: string): void => {\n if (stream === 'stdout') {\n const next = appendWithLimit(stdout, text, payload.maxOutputChars);\n stdout = next.value;\n if (next.truncated) {\n outputTruncated = true;\n }\n return;\n }\n\n const next = appendWithLimit(stderr, text, payload.maxOutputChars);\n stderr = next.value;\n if (next.truncated) {\n outputTruncated = true;\n }\n };\n\n try {\n const consoleProxy = createConsoleProxy(append);\n const context = buildSandbox(payload.input, consoleProxy);\n const wrappedCode = `'use strict';\\n(async () => {\\n${payload.code}\\n})()`;\n const script = new vm.Script(wrappedCode, { filename: 'pulse-sandbox-user-code.js' });\n const result = await script.runInContext(context);\n\n return {\n type: 'success',\n result,\n stdout,\n stderr,\n outputTruncated\n };\n } catch (error) {\n append('stderr', `${toErrorMessage(error)}\\n`);\n\n return {\n type: 'error',\n errorCode: 'RUNTIME_ERROR',\n errorMessage: toErrorMessage(error),\n stdout,\n stderr,\n outputTruncated\n };\n }\n}\n\nfunction sendAndExit(message: RunnerMessage, exitCode: number): void {\n if (!process.send) {\n process.exit(exitCode);\n return;\n }\n\n process.send(message, (error) => {\n if (error) {\n process.exit(1);\n return;\n }\n\n process.exit(exitCode);\n });\n}\n\nprocess.once('message', async (payload: RunnerRequest) => {\n try {\n const result = await executeInSandbox(payload);\n sendAndExit(result, 0);\n } catch (error) {\n const fallback: RunnerErrorMessage = {\n type: 'error',\n errorCode: 'INTERNAL',\n errorMessage: toErrorMessage(error),\n stdout: '',\n stderr: toErrorMessage(error),\n outputTruncated: false\n };\n\n sendAndExit(fallback, 1);\n }\n});\n"],"mappings":"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","names":["next"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pulse-sandbox",
|
|
3
|
+
"version": "0.0.1-alpha.0",
|
|
4
|
+
"description": "Sandboxed JavaScript runtime and engine tool adapter for Pulse Coder",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch",
|
|
20
|
+
"test": "vitest",
|
|
21
|
+
"typecheck": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"zod": "^4.3.6"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^25.0.10",
|
|
31
|
+
"tsup": "^8.0.0",
|
|
32
|
+
"typescript": "^5.0.0",
|
|
33
|
+
"vitest": "^1.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|