@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.
Files changed (49) hide show
  1. package/.turbo/turbo-build.log +12 -11
  2. package/CHANGELOG.md +25 -0
  3. package/dist/index.d.ts +485 -29
  4. package/dist/index.js +2839 -2114
  5. package/dist/isolate-TCWTUVG4.js +1532 -0
  6. package/package.json +23 -4
  7. package/scripts/migrate-to-engine.mjs +556 -0
  8. package/src/config.ts +106 -1
  9. package/src/harness.ts +226 -91
  10. package/src/index.ts +5 -0
  11. package/src/isolate/bindings.ts +206 -0
  12. package/src/isolate/bundler.ts +179 -0
  13. package/src/isolate/index.ts +10 -0
  14. package/src/isolate/polyfills.ts +796 -0
  15. package/src/isolate/run-code-tool.ts +220 -0
  16. package/src/isolate/runtime.ts +286 -0
  17. package/src/isolate/type-stubs.ts +196 -0
  18. package/src/memory.ts +129 -198
  19. package/src/reminder-store.ts +3 -237
  20. package/src/secrets-store.ts +2 -91
  21. package/src/state.ts +11 -1302
  22. package/src/storage/engine.ts +106 -0
  23. package/src/storage/index.ts +59 -0
  24. package/src/storage/memory-engine.ts +588 -0
  25. package/src/storage/postgres-engine.ts +139 -0
  26. package/src/storage/schema.ts +145 -0
  27. package/src/storage/sql-dialect.ts +963 -0
  28. package/src/storage/sqlite-engine.ts +99 -0
  29. package/src/storage/store-adapters.ts +100 -0
  30. package/src/todo-tools.ts +1 -136
  31. package/src/upload-store.ts +1 -0
  32. package/src/vfs/bash-manager.ts +120 -0
  33. package/src/vfs/bash-tool.ts +59 -0
  34. package/src/vfs/create-bash-fs.ts +32 -0
  35. package/src/vfs/edit-file-tool.ts +72 -0
  36. package/src/vfs/index.ts +5 -0
  37. package/src/vfs/poncho-fs-adapter.ts +267 -0
  38. package/src/vfs/protected-fs.ts +177 -0
  39. package/src/vfs/read-file-tool.ts +103 -0
  40. package/src/vfs/write-file-tool.ts +49 -0
  41. package/test/harness.test.ts +30 -36
  42. package/test/isolate-vfs.test.ts +453 -0
  43. package/test/isolate.test.ts +252 -0
  44. package/test/state.test.ts +4 -27
  45. package/test/storage-engine.test.ts +250 -0
  46. package/test/vfs.test.ts +242 -0
  47. package/.turbo/turbo-lint.log +0 -6
  48. package/.turbo/turbo-test.log +0 -11931
  49. 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
+ }