@poncho-ai/harness 0.35.0 → 0.36.1
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/.turbo/turbo-build.log +12 -11
- package/CHANGELOG.md +25 -0
- package/dist/index.d.ts +485 -29
- package/dist/index.js +2839 -2114
- package/dist/isolate-TCWTUVG4.js +1532 -0
- package/package.json +23 -4
- package/scripts/migrate-to-engine.mjs +556 -0
- package/src/config.ts +106 -1
- package/src/harness.ts +226 -91
- package/src/index.ts +5 -0
- package/src/isolate/bindings.ts +206 -0
- package/src/isolate/bundler.ts +179 -0
- package/src/isolate/index.ts +10 -0
- package/src/isolate/polyfills.ts +796 -0
- package/src/isolate/run-code-tool.ts +220 -0
- package/src/isolate/runtime.ts +286 -0
- package/src/isolate/type-stubs.ts +196 -0
- package/src/memory.ts +129 -198
- package/src/reminder-store.ts +3 -237
- package/src/secrets-store.ts +2 -91
- package/src/state.ts +11 -1302
- package/src/storage/engine.ts +106 -0
- package/src/storage/index.ts +59 -0
- package/src/storage/memory-engine.ts +588 -0
- package/src/storage/postgres-engine.ts +139 -0
- package/src/storage/schema.ts +145 -0
- package/src/storage/sql-dialect.ts +963 -0
- package/src/storage/sqlite-engine.ts +99 -0
- package/src/storage/store-adapters.ts +100 -0
- package/src/todo-tools.ts +1 -136
- package/src/upload-store.ts +1 -0
- package/src/vfs/bash-manager.ts +120 -0
- package/src/vfs/bash-tool.ts +59 -0
- package/src/vfs/create-bash-fs.ts +32 -0
- package/src/vfs/edit-file-tool.ts +72 -0
- package/src/vfs/index.ts +5 -0
- package/src/vfs/poncho-fs-adapter.ts +267 -0
- package/src/vfs/protected-fs.ts +177 -0
- package/src/vfs/read-file-tool.ts +103 -0
- package/src/vfs/write-file-tool.ts +49 -0
- package/test/harness.test.ts +30 -36
- package/test/isolate-vfs.test.ts +453 -0
- package/test/isolate.test.ts +252 -0
- package/test/state.test.ts +4 -27
- package/test/storage-engine.test.ts +250 -0
- package/test/vfs.test.ts +242 -0
- package/.turbo/turbo-lint.log +0 -6
- package/.turbo/turbo-test.log +0 -11931
- package/src/kv-store.ts +0 -216
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// run_code tool – sandboxed JavaScript/TypeScript execution in V8 isolates.
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { defineTool, type ToolDefinition } from "@poncho-ai/sdk";
|
|
6
|
+
import type { IsolateConfig, IsolateBinding, NetworkConfig } from "../config.js";
|
|
7
|
+
import type { BashEnvironmentManager } from "../vfs/bash-manager.js";
|
|
8
|
+
import { createIsolateRuntime, type IsolateRuntime } from "./runtime.js";
|
|
9
|
+
import { createVfsBindings, createFetchBinding, mergeBuilderBindings } from "./bindings.js";
|
|
10
|
+
import { buildPolyfillPreamble } from "./polyfills.js";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// TS stripping via esbuild (dynamic import)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
let esbuildTransform: typeof import("esbuild").transform | undefined;
|
|
17
|
+
|
|
18
|
+
async function loadEsbuild(): Promise<typeof import("esbuild").transform> {
|
|
19
|
+
if (esbuildTransform) return esbuildTransform;
|
|
20
|
+
try {
|
|
21
|
+
const mod = await import("esbuild");
|
|
22
|
+
esbuildTransform = mod.transform;
|
|
23
|
+
return esbuildTransform;
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Code execution requires esbuild for TypeScript stripping. Run: pnpm add esbuild",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function stripTypeScript(code: string): Promise<string> {
|
|
32
|
+
const transform = await loadEsbuild();
|
|
33
|
+
// Wrap in an async function before transforming so that top-level
|
|
34
|
+
// `await` + `return` don't trigger esbuild's ESM detection
|
|
35
|
+
// (ESM forbids top-level return).
|
|
36
|
+
const wrapped = "async function __poncho_wrapper__() {\n" + code + "\n}";
|
|
37
|
+
const result = await transform(wrapped, { loader: "ts" });
|
|
38
|
+
// Unwrap: remove the function declaration and closing brace
|
|
39
|
+
const stripped = result.code
|
|
40
|
+
.replace(/^async function __poncho_wrapper__\(\)\s*\{\n?/, "")
|
|
41
|
+
.replace(/\n?\}\s*$/, "");
|
|
42
|
+
return stripped;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Allowed file extensions for VFS file execution
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const ALLOWED_EXTENSIONS = new Set([".js", ".ts", ".mjs", ".mts"]);
|
|
50
|
+
|
|
51
|
+
function hasAllowedExtension(path: string): boolean {
|
|
52
|
+
const dot = path.lastIndexOf(".");
|
|
53
|
+
if (dot === -1) return false;
|
|
54
|
+
return ALLOWED_EXTENSIONS.has(path.slice(dot).toLowerCase());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Factory
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
export interface CreateRunCodeToolOptions {
|
|
62
|
+
config: IsolateConfig;
|
|
63
|
+
bashManager: BashEnvironmentManager;
|
|
64
|
+
/** Pre-built library preamble (from bundler). null if no libraries configured. */
|
|
65
|
+
libraryPreamble: string | null;
|
|
66
|
+
/** Dynamic tool description built from config. */
|
|
67
|
+
description: string;
|
|
68
|
+
/** Top-level network config — auto-registers fetch binding when set. */
|
|
69
|
+
network?: NetworkConfig;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createRunCodeTool(opts: CreateRunCodeToolOptions): ToolDefinition {
|
|
73
|
+
const { config, bashManager, libraryPreamble, description } = opts;
|
|
74
|
+
|
|
75
|
+
const memoryLimit = config.memoryLimit ?? 128;
|
|
76
|
+
const timeout = config.timeLimit ?? 10_000;
|
|
77
|
+
const outputLimit = config.outputLimit ?? 65_536;
|
|
78
|
+
const codeLimit = config.codeLimit ?? 102_400;
|
|
79
|
+
|
|
80
|
+
const runtime: IsolateRuntime = createIsolateRuntime({
|
|
81
|
+
memoryLimit,
|
|
82
|
+
timeout,
|
|
83
|
+
outputLimit,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Static bindings (created once, reused across calls)
|
|
87
|
+
const staticBindings: Record<string, IsolateBinding> = {};
|
|
88
|
+
|
|
89
|
+
// Explicit isolate.apis.fetch takes precedence, then top-level network config
|
|
90
|
+
if (config.apis?.fetch) {
|
|
91
|
+
staticBindings.__poncho_fetch = createFetchBinding(config.apis.fetch.allowedDomains);
|
|
92
|
+
} else if (opts.network) {
|
|
93
|
+
const net = opts.network;
|
|
94
|
+
if (net.dangerouslyAllowAll) {
|
|
95
|
+
staticBindings.__poncho_fetch = createFetchBinding([], net);
|
|
96
|
+
} else if (net.allowedUrls?.length) {
|
|
97
|
+
const domains: string[] = [];
|
|
98
|
+
for (const entry of net.allowedUrls) {
|
|
99
|
+
const urlStr = typeof entry === "string" ? entry : entry.url;
|
|
100
|
+
try { domains.push(new URL(urlStr).hostname); } catch { /* skip invalid */ }
|
|
101
|
+
}
|
|
102
|
+
if (domains.length > 0) {
|
|
103
|
+
staticBindings.__poncho_fetch = createFetchBinding(domains, net);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (config.bindings) {
|
|
109
|
+
Object.assign(staticBindings, mergeBuilderBindings(config.bindings));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const hasNetwork = "__poncho_fetch" in staticBindings;
|
|
113
|
+
const polyfillPreamble = buildPolyfillPreamble(hasNetwork);
|
|
114
|
+
|
|
115
|
+
return defineTool({
|
|
116
|
+
name: "run_code",
|
|
117
|
+
description,
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
code: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "JavaScript or TypeScript code to execute",
|
|
124
|
+
},
|
|
125
|
+
file: {
|
|
126
|
+
type: "string",
|
|
127
|
+
description: "Path to a .js/.ts file in the VFS to execute instead of inline code",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
additionalProperties: false,
|
|
131
|
+
},
|
|
132
|
+
handler: async (input, context) => {
|
|
133
|
+
const code = input.code as string | undefined;
|
|
134
|
+
const file = input.file as string | undefined;
|
|
135
|
+
|
|
136
|
+
// Validate exactly one of code/file
|
|
137
|
+
if (code && file) {
|
|
138
|
+
return { error: "Provide either `code` or `file`, not both." };
|
|
139
|
+
}
|
|
140
|
+
if (!code && !file) {
|
|
141
|
+
return { error: "Provide either `code` (inline) or `file` (VFS path)." };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Resolve source code
|
|
145
|
+
let source: string;
|
|
146
|
+
if (file) {
|
|
147
|
+
if (!hasAllowedExtension(file)) {
|
|
148
|
+
return {
|
|
149
|
+
error: `File must have a .js, .ts, .mjs, or .mts extension. Got: "${file}"`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const tenantId = context.tenantId ?? "__default__";
|
|
153
|
+
const adapter = bashManager.getAdapter(tenantId);
|
|
154
|
+
try {
|
|
155
|
+
source = await adapter.readFile(file);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
return {
|
|
158
|
+
error: `Failed to read file "${file}": ${err instanceof Error ? err.message : String(err)}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
source = code!;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Code size limit
|
|
166
|
+
if (source.length > codeLimit) {
|
|
167
|
+
return {
|
|
168
|
+
error: `Code exceeds size limit: ${source.length} bytes > ${codeLimit} byte max.`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Strip TypeScript
|
|
173
|
+
let jsCode: string;
|
|
174
|
+
try {
|
|
175
|
+
jsCode = await stripTypeScript(source);
|
|
176
|
+
} catch (err: unknown) {
|
|
177
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
178
|
+
return { error: `TypeScript parse error: ${msg}` };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Build per-call VFS bindings + merge with static bindings
|
|
182
|
+
const tenantId = context.tenantId ?? "__default__";
|
|
183
|
+
const adapter = bashManager.getAdapter(tenantId);
|
|
184
|
+
const vfsBindings = createVfsBindings(adapter);
|
|
185
|
+
const allBindings: Record<string, IsolateBinding> = {
|
|
186
|
+
...vfsBindings,
|
|
187
|
+
...staticBindings,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Execute
|
|
191
|
+
const result = await runtime.execute(
|
|
192
|
+
jsCode,
|
|
193
|
+
allBindings,
|
|
194
|
+
libraryPreamble,
|
|
195
|
+
context.abortSignal,
|
|
196
|
+
polyfillPreamble,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Format output
|
|
200
|
+
if (result.error) {
|
|
201
|
+
return {
|
|
202
|
+
error: result.error.message,
|
|
203
|
+
errorName: result.error.name,
|
|
204
|
+
line: result.error.line,
|
|
205
|
+
column: result.error.column,
|
|
206
|
+
stdout: result.stdout || undefined,
|
|
207
|
+
stderr: result.stderr || undefined,
|
|
208
|
+
executionTimeMs: result.executionTimeMs,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
result: result.result,
|
|
214
|
+
stdout: result.stdout || undefined,
|
|
215
|
+
stderr: result.stderr || undefined,
|
|
216
|
+
executionTimeMs: result.executionTimeMs,
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Isolate Runtime – thin wrapper around isolated-vm with async bridging,
|
|
3
|
+
// timeout, memory limits, and console capture.
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
import type { IsolateBinding } from "../config.js";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Types
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export interface ExecutionResult {
|
|
13
|
+
result?: unknown;
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
error?: { message: string; name?: string; line?: number; column?: number };
|
|
17
|
+
executionTimeMs: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface IsolateRuntime {
|
|
21
|
+
execute(
|
|
22
|
+
code: string,
|
|
23
|
+
bindings: Record<string, IsolateBinding>,
|
|
24
|
+
preamble: string | null,
|
|
25
|
+
signal?: AbortSignal,
|
|
26
|
+
polyfillPreamble?: string | null,
|
|
27
|
+
): Promise<ExecutionResult>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Dynamic import helper
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
let ivmModule: any | undefined;
|
|
36
|
+
|
|
37
|
+
async function loadIvm(): Promise<typeof import("isolated-vm")> {
|
|
38
|
+
if (ivmModule) return ivmModule;
|
|
39
|
+
try {
|
|
40
|
+
const mod = await import("isolated-vm");
|
|
41
|
+
// CJS native module: handle both ESM interop shapes
|
|
42
|
+
ivmModule = mod.default ?? mod;
|
|
43
|
+
return ivmModule;
|
|
44
|
+
} catch {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"Code execution requires isolated-vm. Run: pnpm add isolated-vm",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Runtime preamble – injected into every isolate context.
|
|
53
|
+
//
|
|
54
|
+
// Provides:
|
|
55
|
+
// - console.log / console.error / console.warn (captured to buffers)
|
|
56
|
+
// - Ergonomic wrappers for __binding_* callbacks (JSON marshal/unmarshal)
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
function buildRuntimePreamble(): string {
|
|
60
|
+
return `
|
|
61
|
+
// --- console capture ---
|
|
62
|
+
const __stdout = [];
|
|
63
|
+
const __stderr = [];
|
|
64
|
+
let __outputBytes = 0;
|
|
65
|
+
const __outputLimit = typeof __OUTPUT_LIMIT === "number" ? __OUTPUT_LIMIT : 65536;
|
|
66
|
+
|
|
67
|
+
function __serialize(v) {
|
|
68
|
+
if (typeof v === "string") return v;
|
|
69
|
+
try {
|
|
70
|
+
const seen = new WeakSet();
|
|
71
|
+
return JSON.stringify(v, function(_k, val) {
|
|
72
|
+
if (typeof val === "object" && val !== null) {
|
|
73
|
+
if (seen.has(val)) return "[Circular]";
|
|
74
|
+
seen.add(val);
|
|
75
|
+
}
|
|
76
|
+
return val;
|
|
77
|
+
}, 2);
|
|
78
|
+
} catch { return String(v); }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function __capture(arr, args) {
|
|
82
|
+
const line = Array.from(args).map(__serialize).join(" ");
|
|
83
|
+
const bytes = line.length;
|
|
84
|
+
if (__outputBytes + bytes > __outputLimit) {
|
|
85
|
+
arr.push("[output truncated at " + __outputLimit + " bytes]");
|
|
86
|
+
__outputBytes = __outputLimit;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
__outputBytes += bytes;
|
|
90
|
+
arr.push(line);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const console = {
|
|
94
|
+
log: function() { __capture(__stdout, arguments); },
|
|
95
|
+
info: function() { __capture(__stdout, arguments); },
|
|
96
|
+
warn: function() { __capture(__stderr, arguments); },
|
|
97
|
+
error: function() { __capture(__stderr, arguments); },
|
|
98
|
+
debug: function() { __capture(__stdout, arguments); },
|
|
99
|
+
};
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Factory
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
export function createIsolateRuntime(config: {
|
|
108
|
+
memoryLimit: number;
|
|
109
|
+
timeout: number;
|
|
110
|
+
outputLimit: number;
|
|
111
|
+
}): IsolateRuntime {
|
|
112
|
+
return {
|
|
113
|
+
async execute(code, bindings, preamble, signal, polyfillPreamble) {
|
|
114
|
+
const ivm = await loadIvm();
|
|
115
|
+
|
|
116
|
+
const isolate = new ivm.Isolate({
|
|
117
|
+
memoryLimit: config.memoryLimit,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Wire abort → dispose
|
|
121
|
+
let abortHandler: (() => void) | undefined;
|
|
122
|
+
let aborted = false;
|
|
123
|
+
if (signal) {
|
|
124
|
+
if (signal.aborted) {
|
|
125
|
+
isolate.dispose();
|
|
126
|
+
return {
|
|
127
|
+
stdout: "",
|
|
128
|
+
stderr: "",
|
|
129
|
+
error: { message: "Execution cancelled", name: "AbortError" },
|
|
130
|
+
executionTimeMs: 0,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
abortHandler = () => {
|
|
134
|
+
aborted = true;
|
|
135
|
+
isolate.dispose();
|
|
136
|
+
};
|
|
137
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const t0 = performance.now();
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
142
|
+
let context: any;
|
|
143
|
+
try {
|
|
144
|
+
context = await isolate.createContext();
|
|
145
|
+
const jail = context.global;
|
|
146
|
+
|
|
147
|
+
// Inject output limit constant
|
|
148
|
+
jail.setSync("__OUTPUT_LIMIT", config.outputLimit);
|
|
149
|
+
|
|
150
|
+
// Inject binding References and build wrapper declarations
|
|
151
|
+
const bindingNames = Object.keys(bindings);
|
|
152
|
+
const wrapperDecls: string[] = [];
|
|
153
|
+
for (const name of bindingNames) {
|
|
154
|
+
const binding = bindings[name]!;
|
|
155
|
+
const ref = new ivm.Reference(async (inputJson: string) => {
|
|
156
|
+
const input = JSON.parse(inputJson) as Record<string, unknown>;
|
|
157
|
+
const result = await binding.handler(input);
|
|
158
|
+
return JSON.stringify(result ?? null);
|
|
159
|
+
});
|
|
160
|
+
jail.setSync(`__binding_${name}`, ref);
|
|
161
|
+
wrapperDecls.push(
|
|
162
|
+
`async function ${name}(input) {\n` +
|
|
163
|
+
` const raw = await __binding_${name}.apply(undefined, [JSON.stringify(input)], { result: { promise: true, copy: true } });\n` +
|
|
164
|
+
` return JSON.parse(raw);\n` +
|
|
165
|
+
`}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Evaluate runtime preamble (console capture + binding wrappers)
|
|
170
|
+
const runtimePreamble = buildRuntimePreamble() + "\n" + wrapperDecls.join("\n");
|
|
171
|
+
await context.eval(runtimePreamble, { filename: "<runtime>" });
|
|
172
|
+
|
|
173
|
+
// Evaluate polyfill preamble (standard APIs wrapping internal bindings)
|
|
174
|
+
if (polyfillPreamble) {
|
|
175
|
+
await context.eval(polyfillPreamble, { filename: "<polyfills>" });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Evaluate library preamble (bundled libs + require shim)
|
|
179
|
+
if (preamble) {
|
|
180
|
+
await context.eval(preamble, { filename: "<libraries>" });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Wrap user code in async IIFE and execute via context.eval
|
|
184
|
+
// (context.eval + promise option handles Reference.apply resolution
|
|
185
|
+
// correctly, unlike compileScript().run())
|
|
186
|
+
const wrapped = `(async () => {\n${code}\n})()`;
|
|
187
|
+
const rawResult = await context.eval(wrapped, {
|
|
188
|
+
filename: "<user-code>",
|
|
189
|
+
promise: true,
|
|
190
|
+
copy: true,
|
|
191
|
+
timeout: config.timeout,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Read captured stdout/stderr from isolate
|
|
195
|
+
const stdout = (await context.eval("__stdout.join('\\n')", { copy: true })) as string;
|
|
196
|
+
const stderr = (await context.eval("__stderr.join('\\n')", { copy: true })) as string;
|
|
197
|
+
|
|
198
|
+
// Serialize result
|
|
199
|
+
let result: unknown;
|
|
200
|
+
try {
|
|
201
|
+
result =
|
|
202
|
+
rawResult === undefined || rawResult === null
|
|
203
|
+
? rawResult
|
|
204
|
+
: JSON.parse(JSON.stringify(rawResult));
|
|
205
|
+
} catch {
|
|
206
|
+
result = undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
result,
|
|
211
|
+
stdout,
|
|
212
|
+
stderr,
|
|
213
|
+
executionTimeMs: performance.now() - t0,
|
|
214
|
+
};
|
|
215
|
+
} catch (err: unknown) {
|
|
216
|
+
const elapsed = performance.now() - t0;
|
|
217
|
+
|
|
218
|
+
if (aborted) {
|
|
219
|
+
return {
|
|
220
|
+
stdout: "",
|
|
221
|
+
stderr: "",
|
|
222
|
+
error: { message: "Execution cancelled", name: "AbortError" },
|
|
223
|
+
executionTimeMs: elapsed,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Try to recover stdout/stderr captured before the error
|
|
228
|
+
let stdout = "";
|
|
229
|
+
let stderr = "";
|
|
230
|
+
if (context) {
|
|
231
|
+
try {
|
|
232
|
+
stdout = (await context.eval("__stdout.join('\\n')", { copy: true })) as string;
|
|
233
|
+
stderr = (await context.eval("__stderr.join('\\n')", { copy: true })) as string;
|
|
234
|
+
} catch {
|
|
235
|
+
// Context may be disposed or unavailable
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
240
|
+
const parsed = parseV8Error(error);
|
|
241
|
+
return {
|
|
242
|
+
stdout,
|
|
243
|
+
stderr,
|
|
244
|
+
error: parsed,
|
|
245
|
+
executionTimeMs: elapsed,
|
|
246
|
+
};
|
|
247
|
+
} finally {
|
|
248
|
+
if (abortHandler && signal) {
|
|
249
|
+
signal.removeEventListener("abort", abortHandler);
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
isolate.dispose();
|
|
253
|
+
} catch {
|
|
254
|
+
// Already disposed (e.g. via abort)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Error parsing – extract line/column from V8 stack traces
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
function parseV8Error(error: Error): {
|
|
266
|
+
message: string;
|
|
267
|
+
name?: string;
|
|
268
|
+
line?: number;
|
|
269
|
+
column?: number;
|
|
270
|
+
} {
|
|
271
|
+
const result: { message: string; name?: string; line?: number; column?: number } = {
|
|
272
|
+
message: error.message,
|
|
273
|
+
name: error.name,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// Match "<user-code>:N:N" in stack trace
|
|
277
|
+
const match = error.stack?.match(/<user-code>:(\d+):(\d+)/);
|
|
278
|
+
if (match) {
|
|
279
|
+
// Subtract 1 for the async IIFE wrapper line
|
|
280
|
+
const rawLine = parseInt(match[1]!, 10);
|
|
281
|
+
result.line = Math.max(1, rawLine - 1);
|
|
282
|
+
result.column = parseInt(match[2]!, 10);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Type stubs – generate TypeScript declarations for the isolate system prompt.
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import type { IsolateConfig, IsolateBinding } from "../config.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a TypeScript declaration block listing all APIs available inside the
|
|
9
|
+
* isolate. Included in the agent system prompt when `run_code` is registered.
|
|
10
|
+
*/
|
|
11
|
+
export function generateIsolateTypeStubs(config: IsolateConfig): string {
|
|
12
|
+
const lines: string[] = [];
|
|
13
|
+
|
|
14
|
+
// Standard APIs
|
|
15
|
+
lines.push(
|
|
16
|
+
"// Standard Web/Node.js APIs available in the sandbox",
|
|
17
|
+
"",
|
|
18
|
+
"// --- fetch (standard Web API) ---",
|
|
19
|
+
"declare function fetch(url: string, init?: { method?: string; headers?: Record<string, string>; body?: string }): Promise<Response>;",
|
|
20
|
+
"declare class Response {",
|
|
21
|
+
" readonly ok: boolean;",
|
|
22
|
+
" readonly status: number;",
|
|
23
|
+
" readonly statusText: string;",
|
|
24
|
+
" readonly headers: Headers;",
|
|
25
|
+
" text(): Promise<string>;",
|
|
26
|
+
" json(): Promise<any>;",
|
|
27
|
+
" arrayBuffer(): Promise<ArrayBuffer>;",
|
|
28
|
+
" blob(): Promise<Blob>;",
|
|
29
|
+
"}",
|
|
30
|
+
"",
|
|
31
|
+
"// --- fs (Node.js-compatible) ---",
|
|
32
|
+
"declare const fs: {",
|
|
33
|
+
" readFile(path: string, encoding?: string): Promise<string | Buffer>;",
|
|
34
|
+
" writeFile(path: string, data: string | Buffer | Uint8Array): Promise<void>;",
|
|
35
|
+
" readdir(path: string): Promise<string[]>;",
|
|
36
|
+
" mkdir(path: string): Promise<void>;",
|
|
37
|
+
" stat(path: string): Promise<{ isFile(): boolean; isDirectory(): boolean; size: number; mtime: Date }>;",
|
|
38
|
+
" exists(path: string): Promise<boolean>;",
|
|
39
|
+
" unlink(path: string): Promise<void>;",
|
|
40
|
+
" rm(path: string): Promise<void>;",
|
|
41
|
+
"};",
|
|
42
|
+
"",
|
|
43
|
+
"// --- path ---",
|
|
44
|
+
"declare const path: {",
|
|
45
|
+
" join(...parts: string[]): string;",
|
|
46
|
+
" resolve(...parts: string[]): string;",
|
|
47
|
+
" basename(p: string, ext?: string): string;",
|
|
48
|
+
" dirname(p: string): string;",
|
|
49
|
+
" extname(p: string): string;",
|
|
50
|
+
"};",
|
|
51
|
+
"",
|
|
52
|
+
"// --- Buffer, encoding, crypto ---",
|
|
53
|
+
"declare class Buffer extends Uint8Array {",
|
|
54
|
+
" static from(input: string | ArrayBuffer | Uint8Array | number[], encoding?: string): Buffer;",
|
|
55
|
+
" static alloc(size: number, fill?: number): Buffer;",
|
|
56
|
+
" static concat(list: Uint8Array[]): Buffer;",
|
|
57
|
+
" toString(encoding?: 'utf-8' | 'base64' | 'hex'): string;",
|
|
58
|
+
"}",
|
|
59
|
+
"declare function atob(data: string): string;",
|
|
60
|
+
"declare function btoa(data: string): string;",
|
|
61
|
+
"declare function setTimeout(fn: () => void, ms?: number): number;",
|
|
62
|
+
"declare function clearTimeout(id: number): void;",
|
|
63
|
+
"declare const crypto: { randomUUID(): string; getRandomValues(arr: Uint8Array): Uint8Array };",
|
|
64
|
+
"declare function structuredClone<T>(value: T): T;",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Console
|
|
68
|
+
lines.push(
|
|
69
|
+
"",
|
|
70
|
+
"// Console (output captured and returned in tool result)",
|
|
71
|
+
"declare const console: {",
|
|
72
|
+
" log(...args: unknown[]): void; error(...args: unknown[]): void;",
|
|
73
|
+
" warn(...args: unknown[]): void; info(...args: unknown[]): void;",
|
|
74
|
+
" table(data: unknown): void; time(label?: string): void; timeEnd(label?: string): void;",
|
|
75
|
+
"};",
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Builder custom bindings
|
|
79
|
+
if (config.bindings) {
|
|
80
|
+
const entries = Object.entries(config.bindings);
|
|
81
|
+
if (entries.length > 0) {
|
|
82
|
+
lines.push("", "// Custom bindings");
|
|
83
|
+
for (const [name, binding] of entries) {
|
|
84
|
+
lines.push(formatBindingStub(name, binding));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Libraries
|
|
90
|
+
if (config.libraries?.length) {
|
|
91
|
+
lines.push(
|
|
92
|
+
"",
|
|
93
|
+
`// Pre-bundled libraries (use require())`,
|
|
94
|
+
`declare function require(name: ${config.libraries.map((l) => `"${l}"`).join(" | ")}): any;`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build the dynamic tool description for `run_code` based on the isolate config.
|
|
103
|
+
*/
|
|
104
|
+
export function buildRunCodeDescription(
|
|
105
|
+
config: IsolateConfig,
|
|
106
|
+
hasNetwork?: boolean,
|
|
107
|
+
): string {
|
|
108
|
+
const parts: string[] = [
|
|
109
|
+
"Execute JavaScript/TypeScript code in a sandboxed V8 isolate with standard Node.js/Web APIs.",
|
|
110
|
+
"",
|
|
111
|
+
"Input: provide either `code` (inline string) or `file` (path to a .js/.ts file in the VFS).",
|
|
112
|
+
"",
|
|
113
|
+
"Available standard APIs:",
|
|
114
|
+
"- fs.readFile(path, encoding?) / fs.writeFile(path, data) / fs.readdir(path) / fs.mkdir(path)",
|
|
115
|
+
"- fs.stat(path) / fs.exists(path) / fs.unlink(path)",
|
|
116
|
+
"- path.join() / path.resolve() / path.basename() / path.dirname() / path.extname()",
|
|
117
|
+
"- Buffer.from() / Buffer.alloc() / Buffer.concat() / buf.toString(encoding)",
|
|
118
|
+
"- atob() / btoa() / setTimeout() / crypto.randomUUID() / structuredClone()",
|
|
119
|
+
"- console.log() / console.error() / console.table()",
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
if (hasNetwork || config.apis?.fetch) {
|
|
123
|
+
parts.push(
|
|
124
|
+
"- fetch(url, init?) — standard Web fetch API with Response.text(), .json(), .arrayBuffer()",
|
|
125
|
+
);
|
|
126
|
+
} else {
|
|
127
|
+
parts.push(
|
|
128
|
+
"- fetch() — not available (enable `network` in poncho.config.js)",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (config.bindings) {
|
|
133
|
+
for (const [name, binding] of Object.entries(config.bindings)) {
|
|
134
|
+
parts.push(`- ${name}({...}) — ${binding.description}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (config.libraries?.length) {
|
|
139
|
+
parts.push(
|
|
140
|
+
"",
|
|
141
|
+
`Pre-bundled libraries (use require()):`,
|
|
142
|
+
`- ${config.libraries.join(", ")}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const memoryLimit = config.memoryLimit ?? 128;
|
|
147
|
+
const timeLimit = config.timeLimit ?? 10_000;
|
|
148
|
+
const codeLimit = config.codeLimit ?? 102_400;
|
|
149
|
+
parts.push(
|
|
150
|
+
"",
|
|
151
|
+
"Notes:",
|
|
152
|
+
"- Code is wrapped in an async IIFE. Use `return` to return a value.",
|
|
153
|
+
"- Files written during execution persist even if the code throws an error.",
|
|
154
|
+
"- TypeScript is supported (type annotations are stripped before execution).",
|
|
155
|
+
`- Execution timeout: ${timeLimit / 1000}s. Memory limit: ${memoryLimit}MB. Max code size: ${Math.round(codeLimit / 1024)}KB.`,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return parts.join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Helpers
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
function formatBindingStub(name: string, binding: IsolateBinding): string {
|
|
166
|
+
const schema = binding.inputSchema;
|
|
167
|
+
const props = schema.properties ?? {};
|
|
168
|
+
const required = new Set(schema.required ?? []);
|
|
169
|
+
const params = Object.entries(props)
|
|
170
|
+
.map(([k, v]) => {
|
|
171
|
+
const opt = required.has(k) ? "" : "?";
|
|
172
|
+
const tsType = jsonSchemaToTsType(v);
|
|
173
|
+
return `${k}${opt}: ${tsType}`;
|
|
174
|
+
})
|
|
175
|
+
.join("; ");
|
|
176
|
+
|
|
177
|
+
return `declare function ${name}(input: { ${params} }): Promise<unknown>; // ${binding.description}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function jsonSchemaToTsType(schema: { type?: string; [key: string]: unknown }): string {
|
|
181
|
+
switch (schema.type) {
|
|
182
|
+
case "string":
|
|
183
|
+
return "string";
|
|
184
|
+
case "number":
|
|
185
|
+
case "integer":
|
|
186
|
+
return "number";
|
|
187
|
+
case "boolean":
|
|
188
|
+
return "boolean";
|
|
189
|
+
case "array":
|
|
190
|
+
return "unknown[]";
|
|
191
|
+
case "object":
|
|
192
|
+
return "Record<string, unknown>";
|
|
193
|
+
default:
|
|
194
|
+
return "unknown";
|
|
195
|
+
}
|
|
196
|
+
}
|