@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,206 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Isolate Bindings – factory functions that create IsolateBinding objects
3
+ // for VFS operations, scoped fetch, and builder custom bindings.
4
+ //
5
+ // All bindings use __poncho_ prefix to avoid colliding with the standard
6
+ // API polyfills that wrap them (see polyfills.ts).
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import type { IsolateBinding, IsolateConfig, NetworkConfig } from "../config.js";
10
+ import type { PonchoFsAdapter } from "../vfs/poncho-fs-adapter.js";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // VFS bindings (created per-invocation with a tenant-scoped adapter)
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export function createVfsBindings(
17
+ adapter: PonchoFsAdapter,
18
+ ): Record<string, IsolateBinding> {
19
+ return {
20
+ __poncho_fs_read: {
21
+ description: "Read a text file from the VFS",
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: { path: { type: "string" } },
25
+ required: ["path"],
26
+ },
27
+ handler: async (input) => {
28
+ return await adapter.readFile(input.path as string);
29
+ },
30
+ },
31
+
32
+ __poncho_fs_write: {
33
+ description: "Write a text file to the VFS",
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ path: { type: "string" },
38
+ content: { type: "string" },
39
+ },
40
+ required: ["path", "content"],
41
+ },
42
+ handler: async (input) => {
43
+ await adapter.writeFile(input.path as string, input.content as string);
44
+ },
45
+ },
46
+
47
+ __poncho_fs_read_binary: {
48
+ description: "Read a binary file (returns base64)",
49
+ inputSchema: {
50
+ type: "object",
51
+ properties: { path: { type: "string" } },
52
+ required: ["path"],
53
+ },
54
+ handler: async (input) => {
55
+ const buf = await adapter.readFileBuffer(input.path as string);
56
+ return Buffer.from(buf).toString("base64");
57
+ },
58
+ },
59
+
60
+ __poncho_fs_write_binary: {
61
+ description: "Write a binary file (content is base64)",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ path: { type: "string" },
66
+ content: { type: "string" },
67
+ },
68
+ required: ["path", "content"],
69
+ },
70
+ handler: async (input) => {
71
+ const buf = Buffer.from(input.content as string, "base64");
72
+ await adapter.writeFile(input.path as string, buf);
73
+ },
74
+ },
75
+
76
+ __poncho_fs_list: {
77
+ description: "List directory entries",
78
+ inputSchema: {
79
+ type: "object",
80
+ properties: { path: { type: "string" } },
81
+ required: ["path"],
82
+ },
83
+ handler: async (input) => {
84
+ return await adapter.readdir(input.path as string);
85
+ },
86
+ },
87
+
88
+ __poncho_fs_exists: {
89
+ description: "Check if path exists",
90
+ inputSchema: {
91
+ type: "object",
92
+ properties: { path: { type: "string" } },
93
+ required: ["path"],
94
+ },
95
+ handler: async (input) => {
96
+ return await adapter.exists(input.path as string);
97
+ },
98
+ },
99
+
100
+ __poncho_fs_delete: {
101
+ description: "Delete a file or directory",
102
+ inputSchema: {
103
+ type: "object",
104
+ properties: { path: { type: "string" } },
105
+ required: ["path"],
106
+ },
107
+ handler: async (input) => {
108
+ await adapter.rm(input.path as string, { force: true });
109
+ },
110
+ },
111
+
112
+ __poncho_fs_mkdir: {
113
+ description: "Create a directory (recursive)",
114
+ inputSchema: {
115
+ type: "object",
116
+ properties: { path: { type: "string" } },
117
+ required: ["path"],
118
+ },
119
+ handler: async (input) => {
120
+ await adapter.mkdir(input.path as string, { recursive: true });
121
+ },
122
+ },
123
+
124
+ __poncho_fs_stat: {
125
+ description: "Get file metadata",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: { path: { type: "string" } },
129
+ required: ["path"],
130
+ },
131
+ handler: async (input) => {
132
+ const stat = await adapter.stat(input.path as string);
133
+ return {
134
+ isFile: stat.isFile,
135
+ isDirectory: stat.isDirectory,
136
+ size: stat.size,
137
+ mtime: stat.mtime.toISOString(),
138
+ };
139
+ },
140
+ },
141
+ };
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Scoped fetch binding (created once at registration time)
146
+ // ---------------------------------------------------------------------------
147
+
148
+ export function createFetchBinding(
149
+ allowedDomains: string[],
150
+ network?: NetworkConfig,
151
+ ): IsolateBinding {
152
+ const allowAll = network?.dangerouslyAllowAll === true;
153
+ const domainSet = new Set(allowedDomains.map((d) => d.toLowerCase()));
154
+
155
+ return {
156
+ description: "Internal fetch binding",
157
+ inputSchema: {
158
+ type: "object",
159
+ properties: {
160
+ url: { type: "string" },
161
+ method: { type: "string" },
162
+ headers: { type: "object", additionalProperties: { type: "string" } },
163
+ body: { type: "string" },
164
+ binary: { type: "boolean" },
165
+ },
166
+ required: ["url"],
167
+ },
168
+ handler: async (input) => {
169
+ const url = new URL(input.url as string);
170
+ if (!allowAll && !domainSet.has(url.hostname.toLowerCase())) {
171
+ throw new Error(
172
+ `Fetch blocked: domain "${url.hostname}" is not in the allowed list [${allowedDomains.join(", ")}]`,
173
+ );
174
+ }
175
+
176
+ const resp = await fetch(input.url as string, {
177
+ method: (input.method as string) ?? "GET",
178
+ headers: (input.headers as Record<string, string>) ?? undefined,
179
+ body: (input.body as string) ?? undefined,
180
+ redirect: "follow",
181
+ });
182
+
183
+ const headers: Record<string, string> = {};
184
+ resp.headers.forEach((v, k) => { headers[k] = v; });
185
+
186
+ if (input.binary) {
187
+ const buf = await resp.arrayBuffer();
188
+ const base64 = Buffer.from(buf).toString("base64");
189
+ return { status: resp.status, statusText: resp.statusText, headers, body: base64, encoding: "base64" };
190
+ }
191
+
192
+ const body = await resp.text();
193
+ return { status: resp.status, statusText: resp.statusText, headers, body };
194
+ },
195
+ };
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Builder custom bindings (adapt from config format)
200
+ // ---------------------------------------------------------------------------
201
+
202
+ export function mergeBuilderBindings(
203
+ configBindings: NonNullable<IsolateConfig["bindings"]>,
204
+ ): Record<string, IsolateBinding> {
205
+ return { ...configBindings };
206
+ }
@@ -0,0 +1,179 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Library Bundler – esbuild-based npm library bundling with require() shim.
3
+ //
4
+ // Bundles each declared library into a self-contained IIFE, then generates
5
+ // a module map + require() shim that the isolate can use.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ import { resolve } from "node:path";
9
+ import { existsSync, readFileSync } from "node:fs";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Dynamic import
13
+ // ---------------------------------------------------------------------------
14
+
15
+ let esbuildBuild: typeof import("esbuild").build | undefined;
16
+
17
+ async function loadEsbuild(): Promise<typeof import("esbuild").build> {
18
+ if (esbuildBuild) return esbuildBuild;
19
+ try {
20
+ const mod = await import("esbuild");
21
+ esbuildBuild = mod.build;
22
+ return esbuildBuild;
23
+ } catch {
24
+ throw new Error(
25
+ "Library bundling requires esbuild. Run: pnpm add esbuild",
26
+ );
27
+ }
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Node built-in stubs
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /** Node built-ins that we refuse to polyfill — they need real OS access. */
35
+ const BLOCKED_BUILTINS = new Set([
36
+ "fs",
37
+ "fs/promises",
38
+ "net",
39
+ "tls",
40
+ "child_process",
41
+ "cluster",
42
+ "dgram",
43
+ "dns",
44
+ "http",
45
+ "http2",
46
+ "https",
47
+ "inspector",
48
+ "module",
49
+ "os",
50
+ "perf_hooks",
51
+ "readline",
52
+ "repl",
53
+ "stream",
54
+ "tty",
55
+ "v8",
56
+ "vm",
57
+ "worker_threads",
58
+ "wasi",
59
+ ]);
60
+
61
+ /**
62
+ * Build an esbuild plugin that stubs out blocked Node built-ins with a
63
+ * throw at import time, so libraries that depend on them fail clearly.
64
+ */
65
+ function blockedBuiltinsPlugin(): import("esbuild").Plugin {
66
+ return {
67
+ name: "blocked-builtins",
68
+ setup(build) {
69
+ // Match bare specifiers and node: prefixed
70
+ const filter = new RegExp(
71
+ `^(node:)?(${[...BLOCKED_BUILTINS].join("|")})$`,
72
+ );
73
+ build.onResolve({ filter }, (args) => ({
74
+ path: args.path,
75
+ namespace: "blocked-builtin",
76
+ }));
77
+ build.onLoad(
78
+ { filter: /.*/, namespace: "blocked-builtin" },
79
+ (args) => {
80
+ const name = args.path.replace(/^node:/, "");
81
+ return {
82
+ contents: `throw new Error("Module '${name}' is not available in the isolate. Use the injected fs_* functions instead.");`,
83
+ loader: "js",
84
+ };
85
+ },
86
+ );
87
+ },
88
+ };
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Public API
93
+ // ---------------------------------------------------------------------------
94
+
95
+ /**
96
+ * Bundle the declared libraries into a single JS preamble string.
97
+ *
98
+ * Each library is bundled as an IIFE with `globalName: '__lib_<safeName>'`.
99
+ * The preamble ends with a module map and `require()` shim.
100
+ *
101
+ * @param libraries - npm package names declared in config
102
+ * @param projectDir - project root for resolving node_modules
103
+ * @returns Concatenated JS preamble, or null if no libraries
104
+ */
105
+ export async function bundleLibraries(
106
+ libraries: string[],
107
+ projectDir: string,
108
+ ): Promise<string | null> {
109
+ if (libraries.length === 0) return null;
110
+
111
+ const build = await loadEsbuild();
112
+ const chunks: string[] = [];
113
+ const moduleEntries: string[] = [];
114
+
115
+ for (const lib of libraries) {
116
+ // Verify the library is installed
117
+ const pkgPath = resolve(projectDir, "node_modules", lib, "package.json");
118
+ if (!existsSync(pkgPath)) {
119
+ throw new Error(
120
+ `Library '${lib}' is declared in isolate.libraries but not installed. Run: pnpm add ${lib}`,
121
+ );
122
+ }
123
+
124
+ // Read version for cache key (informational)
125
+ let version = "unknown";
126
+ try {
127
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version?: string };
128
+ version = pkg.version ?? "unknown";
129
+ } catch {
130
+ // Non-critical
131
+ }
132
+
133
+ const safeName = lib.replace(/[^a-zA-Z0-9_]/g, "_");
134
+ const globalName = `__lib_${safeName}`;
135
+
136
+ const result = await build({
137
+ entryPoints: [lib],
138
+ bundle: true,
139
+ write: false,
140
+ format: "iife",
141
+ globalName,
142
+ platform: "neutral",
143
+ target: "es2022",
144
+ // Resolve from the project's node_modules
145
+ nodePaths: [resolve(projectDir, "node_modules")],
146
+ plugins: [blockedBuiltinsPlugin()],
147
+ logLevel: "silent",
148
+ minify: false,
149
+ });
150
+
151
+ if (result.outputFiles?.[0]) {
152
+ chunks.push(
153
+ `// --- ${lib}@${version} ---\n${result.outputFiles[0].text}`,
154
+ );
155
+ moduleEntries.push(` ${JSON.stringify(lib)}: ${globalName}`);
156
+ } else {
157
+ throw new Error(
158
+ `Failed to bundle library '${lib}': esbuild produced no output.`,
159
+ );
160
+ }
161
+ }
162
+
163
+ // Build the require() shim
164
+ const requireShim = `
165
+ // --- require() shim ---
166
+ var __modules = {
167
+ ${moduleEntries.join(",\n")}
168
+ };
169
+ function require(name) {
170
+ var mod = __modules[name];
171
+ if (!mod) throw new Error('Module "' + name + '" is not available. Available: ${libraries.join(", ")}');
172
+ // Handle both default and namespace exports
173
+ if (mod && mod.default !== undefined && Object.keys(mod).length === 1) return mod.default;
174
+ return mod;
175
+ }
176
+ `;
177
+
178
+ return chunks.join("\n\n") + "\n\n" + requireShim;
179
+ }
@@ -0,0 +1,10 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Isolate module – public exports for harness integration.
3
+ // ---------------------------------------------------------------------------
4
+
5
+ export { createRunCodeTool, type CreateRunCodeToolOptions } from "./run-code-tool.js";
6
+ export { createIsolateRuntime, type IsolateRuntime, type ExecutionResult } from "./runtime.js";
7
+ export { generateIsolateTypeStubs, buildRunCodeDescription } from "./type-stubs.js";
8
+ export { createVfsBindings, createFetchBinding, mergeBuilderBindings } from "./bindings.js";
9
+ export { buildPolyfillPreamble } from "./polyfills.js";
10
+ export { bundleLibraries } from "./bundler.js";