@poncho-ai/harness 0.35.0 → 0.36.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.
Files changed (57) hide show
  1. package/.turbo/turbo-build.log +6 -5
  2. package/.turbo/turbo-test.log +15169 -0
  3. package/CHANGELOG.md +18 -0
  4. package/dist/chunk-MCKGQKYU.js +15 -0
  5. package/dist/dist-3KMQR4IO.js +27092 -0
  6. package/dist/index.d.ts +485 -29
  7. package/dist/index.js +2839 -2114
  8. package/dist/isolate-5MISBSUK.js +733 -0
  9. package/dist/isolate-5R6762YA.js +605 -0
  10. package/dist/isolate-KUZ5NOPG.js +727 -0
  11. package/dist/isolate-LOL3T7RA.js +729 -0
  12. package/dist/isolate-N22X4TCE.js +740 -0
  13. package/dist/isolate-T7WXM7IL.js +1490 -0
  14. package/dist/isolate-TCWTUVG4.js +1532 -0
  15. package/dist/isolate-WFOLANOB.js +768 -0
  16. package/package.json +22 -3
  17. package/scripts/migrate-to-engine.mjs +556 -0
  18. package/src/config.ts +106 -1
  19. package/src/harness.ts +226 -91
  20. package/src/index.ts +5 -0
  21. package/src/isolate/bindings.ts +206 -0
  22. package/src/isolate/bundler.ts +179 -0
  23. package/src/isolate/index.ts +10 -0
  24. package/src/isolate/polyfills.ts +796 -0
  25. package/src/isolate/run-code-tool.ts +220 -0
  26. package/src/isolate/runtime.ts +286 -0
  27. package/src/isolate/type-stubs.ts +196 -0
  28. package/src/memory.ts +129 -198
  29. package/src/reminder-store.ts +3 -237
  30. package/src/secrets-store.ts +2 -91
  31. package/src/state.ts +11 -1302
  32. package/src/storage/engine.ts +106 -0
  33. package/src/storage/index.ts +59 -0
  34. package/src/storage/memory-engine.ts +588 -0
  35. package/src/storage/postgres-engine.ts +139 -0
  36. package/src/storage/schema.ts +145 -0
  37. package/src/storage/sql-dialect.ts +963 -0
  38. package/src/storage/sqlite-engine.ts +99 -0
  39. package/src/storage/store-adapters.ts +100 -0
  40. package/src/todo-tools.ts +1 -136
  41. package/src/upload-store.ts +1 -0
  42. package/src/vfs/bash-manager.ts +120 -0
  43. package/src/vfs/bash-tool.ts +59 -0
  44. package/src/vfs/create-bash-fs.ts +32 -0
  45. package/src/vfs/edit-file-tool.ts +72 -0
  46. package/src/vfs/index.ts +5 -0
  47. package/src/vfs/poncho-fs-adapter.ts +267 -0
  48. package/src/vfs/protected-fs.ts +177 -0
  49. package/src/vfs/read-file-tool.ts +103 -0
  50. package/src/vfs/write-file-tool.ts +49 -0
  51. package/test/harness.test.ts +30 -36
  52. package/test/isolate-vfs.test.ts +453 -0
  53. package/test/isolate.test.ts +252 -0
  54. package/test/state.test.ts +4 -27
  55. package/test/storage-engine.test.ts +250 -0
  56. package/test/vfs.test.ts +242 -0
  57. package/src/kv-store.ts +0 -216
@@ -37,10 +37,9 @@ model:
37
37
  await harness.initialize();
38
38
  const names = harness.listTools().map((tool) => tool.name);
39
39
 
40
- expect(names).toContain("list_directory");
41
- expect(names).toContain("read_file");
42
- expect(names).toContain("write_file");
43
- expect(names).toContain("edit_file");
40
+ expect(names).toContain("bash");
41
+ expect(names).toContain("web_search");
42
+ expect(names).toContain("todo_list");
44
43
  });
45
44
 
46
45
  it("disables write_file by default in production environment", async () => {
@@ -63,10 +62,9 @@ model:
63
62
  await harness.initialize();
64
63
  const names = harness.listTools().map((tool) => tool.name);
65
64
 
66
- expect(names).toContain("list_directory");
67
- expect(names).toContain("read_file");
68
- expect(names).not.toContain("write_file");
69
- expect(names).not.toContain("edit_file");
65
+ // In production, bash is registered but poncho_docs is not
66
+ expect(names).toContain("bash");
67
+ expect(names).not.toContain("poncho_docs");
70
68
  });
71
69
 
72
70
  it("allows disabling built-in tools via poncho.config.js", async () => {
@@ -88,9 +86,7 @@ model:
88
86
  join(dir, "poncho.config.js"),
89
87
  `export default {
90
88
  tools: {
91
- defaults: {
92
- read_file: false
93
- }
89
+ web_search: false
94
90
  }
95
91
  };
96
92
  `,
@@ -100,8 +96,8 @@ model:
100
96
  const harness = new AgentHarness({ workingDir: dir, environment: "production" });
101
97
  await harness.initialize();
102
98
  const names = harness.listTools().map((tool) => tool.name);
103
- expect(names).toContain("list_directory");
104
- expect(names).not.toContain("read_file");
99
+ expect(names).toContain("bash");
100
+ expect(names).not.toContain("web_search");
105
101
  });
106
102
 
107
103
  it("supports per-environment tool overrides", async () => {
@@ -123,15 +119,13 @@ model:
123
119
  join(dir, "poncho.config.js"),
124
120
  `export default {
125
121
  tools: {
126
- defaults: {
127
- read_file: false
128
- },
122
+ web_search: false,
129
123
  byEnvironment: {
130
124
  development: {
131
- read_file: true
125
+ web_search: true
132
126
  },
133
127
  production: {
134
- write_file: false
128
+ web_fetch: false
135
129
  }
136
130
  }
137
131
  }
@@ -143,14 +137,14 @@ model:
143
137
  const developmentHarness = new AgentHarness({ workingDir: dir, environment: "development" });
144
138
  await developmentHarness.initialize();
145
139
  const developmentTools = developmentHarness.listTools().map((tool) => tool.name);
146
- expect(developmentTools).toContain("read_file");
147
- expect(developmentTools).toContain("write_file");
140
+ expect(developmentTools).toContain("web_search");
141
+ expect(developmentTools).toContain("bash");
148
142
 
149
143
  const productionHarness = new AgentHarness({ workingDir: dir, environment: "production" });
150
144
  await productionHarness.initialize();
151
145
  const productionTools = productionHarness.listTools().map((tool) => tool.name);
152
- expect(productionTools).not.toContain("read_file");
153
- expect(productionTools).not.toContain("write_file");
146
+ expect(productionTools).not.toContain("web_search");
147
+ expect(productionTools).not.toContain("web_fetch");
154
148
  });
155
149
 
156
150
  it("does not auto-register exported tool objects from skill scripts", async () => {
@@ -1410,8 +1404,8 @@ model:
1410
1404
  join(dir, "poncho.config.js"),
1411
1405
  `export default {
1412
1406
  tools: {
1413
- read_file: false,
1414
- list_directory: true,
1407
+ web_search: false,
1408
+ web_fetch: true,
1415
1409
  }
1416
1410
  };
1417
1411
  `,
@@ -1421,8 +1415,8 @@ model:
1421
1415
  const harness = new AgentHarness({ workingDir: dir });
1422
1416
  await harness.initialize();
1423
1417
  const names = harness.listTools().map((tool) => tool.name);
1424
- expect(names).toContain("list_directory");
1425
- expect(names).not.toContain("read_file");
1418
+ expect(names).toContain("web_fetch");
1419
+ expect(names).not.toContain("web_search");
1426
1420
  });
1427
1421
 
1428
1422
  it("flat tool access takes priority over legacy defaults", async () => {
@@ -1444,9 +1438,9 @@ model:
1444
1438
  join(dir, "poncho.config.js"),
1445
1439
  `export default {
1446
1440
  tools: {
1447
- read_file: true,
1441
+ web_search: true,
1448
1442
  defaults: {
1449
- read_file: false,
1443
+ web_search: false,
1450
1444
  },
1451
1445
  }
1452
1446
  };
@@ -1457,7 +1451,7 @@ model:
1457
1451
  const harness = new AgentHarness({ workingDir: dir });
1458
1452
  await harness.initialize();
1459
1453
  const names = harness.listTools().map((tool) => tool.name);
1460
- expect(names).toContain("read_file");
1454
+ expect(names).toContain("web_search");
1461
1455
  });
1462
1456
 
1463
1457
  it("byEnvironment overrides flat tool access", async () => {
@@ -1479,10 +1473,10 @@ model:
1479
1473
  join(dir, "poncho.config.js"),
1480
1474
  `export default {
1481
1475
  tools: {
1482
- read_file: false,
1476
+ web_search: false,
1483
1477
  byEnvironment: {
1484
1478
  development: {
1485
- read_file: true,
1479
+ web_search: true,
1486
1480
  },
1487
1481
  },
1488
1482
  }
@@ -1494,7 +1488,7 @@ model:
1494
1488
  const harness = new AgentHarness({ workingDir: dir, environment: "development" });
1495
1489
  await harness.initialize();
1496
1490
  const names = harness.listTools().map((tool) => tool.name);
1497
- expect(names).toContain("read_file");
1491
+ expect(names).toContain("web_search");
1498
1492
  });
1499
1493
 
1500
1494
  it("registerTools skips tools disabled via config", async () => {
@@ -1563,7 +1557,7 @@ model:
1563
1557
  join(dir, "poncho.config.js"),
1564
1558
  `export default {
1565
1559
  tools: {
1566
- write_file: 'approval',
1560
+ web_search: 'approval',
1567
1561
  }
1568
1562
  };
1569
1563
  `,
@@ -1573,9 +1567,9 @@ model:
1573
1567
  const harness = new AgentHarness({ workingDir: dir });
1574
1568
  await harness.initialize();
1575
1569
  const names = harness.listTools().map((tool) => tool.name);
1576
- expect(names).toContain("write_file");
1570
+ expect(names).toContain("web_search");
1577
1571
 
1578
- const requiresApproval = (harness as any).requiresApprovalForToolCall("write_file", {});
1572
+ const requiresApproval = (harness as any).requiresApprovalForToolCall("web_search", {});
1579
1573
  expect(requiresApproval).toBe(true);
1580
1574
  });
1581
1575
 
@@ -1597,7 +1591,7 @@ model:
1597
1591
 
1598
1592
  const harness = new AgentHarness({ workingDir: dir });
1599
1593
  await harness.initialize();
1600
- const requiresApproval = (harness as any).requiresApprovalForToolCall("write_file", {});
1594
+ const requiresApproval = (harness as any).requiresApprovalForToolCall("web_search", {});
1601
1595
  expect(requiresApproval).toBe(false);
1602
1596
  });
1603
1597
 
@@ -0,0 +1,453 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createIsolateRuntime } from "../src/isolate/runtime.js";
3
+ import { createVfsBindings, createFetchBinding } from "../src/isolate/bindings.js";
4
+ import { buildPolyfillPreamble } from "../src/isolate/polyfills.js";
5
+ import { InMemoryEngine } from "../src/storage/memory-engine.js";
6
+ import { PonchoFsAdapter } from "../src/vfs/poncho-fs-adapter.js";
7
+ import type { IsolateBinding } from "../src/config.js";
8
+
9
+ const MB = 1024 * 1024;
10
+ const LIMITS = { maxFileSize: 10 * MB, maxTotalStorage: 100 * MB };
11
+ const DEFAULT_CONFIG = { memoryLimit: 64, timeout: 5000, outputLimit: 65536 };
12
+
13
+ async function createTestAdapter(tenantId = "t1"): Promise<{
14
+ adapter: PonchoFsAdapter;
15
+ engine: InMemoryEngine;
16
+ }> {
17
+ const engine = new InMemoryEngine("test");
18
+ await engine.initialize();
19
+ const adapter = new PonchoFsAdapter(engine, tenantId, LIMITS);
20
+ return { adapter, engine };
21
+ }
22
+
23
+ const polyfills = buildPolyfillPreamble(false);
24
+ const polyfillsWithFetch = buildPolyfillPreamble(true);
25
+
26
+ describe("Standard fs API in isolate", () => {
27
+ it("reads and writes text files via fs.readFile/writeFile", async () => {
28
+ const { adapter, engine } = await createTestAdapter();
29
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
30
+ const bindings = createVfsBindings(adapter);
31
+
32
+ const res = await runtime.execute(
33
+ `
34
+ await fs.writeFile("/test.txt", "hello world");
35
+ const content = await fs.readFile("/test.txt", "utf-8");
36
+ return content;
37
+ `,
38
+ bindings, null, undefined, polyfills,
39
+ );
40
+
41
+ expect(res.error).toBeUndefined();
42
+ expect(res.result).toBe("hello world");
43
+
44
+ const persisted = await adapter.readFile("/test.txt");
45
+ expect(persisted).toBe("hello world");
46
+
47
+ await engine.close();
48
+ });
49
+
50
+ it("reads binary files as Buffer", async () => {
51
+ const { adapter, engine } = await createTestAdapter();
52
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
53
+ const bindings = createVfsBindings(adapter);
54
+
55
+ // Write binary via adapter
56
+ await adapter.writeFile("/data.bin", Buffer.from([0x41, 0x42, 0x43]));
57
+
58
+ const res = await runtime.execute(
59
+ `
60
+ const buf = await fs.readFile("/data.bin");
61
+ return buf.toString("utf-8");
62
+ `,
63
+ bindings, null, undefined, polyfills,
64
+ );
65
+
66
+ expect(res.error).toBeUndefined();
67
+ expect(res.result).toBe("ABC");
68
+
69
+ await engine.close();
70
+ });
71
+
72
+ it("writes Buffer/Uint8Array binary data", async () => {
73
+ const { adapter, engine } = await createTestAdapter();
74
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
75
+ const bindings = createVfsBindings(adapter);
76
+
77
+ const res = await runtime.execute(
78
+ `
79
+ const buf = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
80
+ await fs.writeFile("/hello.bin", buf);
81
+ return true;
82
+ `,
83
+ bindings, null, undefined, polyfills,
84
+ );
85
+
86
+ expect(res.error).toBeUndefined();
87
+ const content = await adapter.readFileBuffer("/hello.bin");
88
+ expect(Buffer.from(content).toString("utf-8")).toBe("Hello");
89
+
90
+ await engine.close();
91
+ });
92
+
93
+ it("lists directories via fs.readdir", async () => {
94
+ const { adapter, engine } = await createTestAdapter();
95
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
96
+ const bindings = createVfsBindings(adapter);
97
+
98
+ await adapter.mkdir("/mydir", { recursive: true });
99
+ await adapter.writeFile("/mydir/a.txt", "a");
100
+ await adapter.writeFile("/mydir/b.txt", "b");
101
+
102
+ const res = await runtime.execute(
103
+ `return await fs.readdir("/mydir");`,
104
+ bindings, null, undefined, polyfills,
105
+ );
106
+
107
+ expect(res.error).toBeUndefined();
108
+ expect((res.result as string[]).sort()).toEqual(["a.txt", "b.txt"]);
109
+
110
+ await engine.close();
111
+ });
112
+
113
+ it("checks file existence via fs.exists", async () => {
114
+ const { adapter, engine } = await createTestAdapter();
115
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
116
+ const bindings = createVfsBindings(adapter);
117
+
118
+ await adapter.writeFile("/exists.txt", "yes");
119
+
120
+ const res = await runtime.execute(
121
+ `
122
+ const a = await fs.exists("/exists.txt");
123
+ const b = await fs.exists("/nope.txt");
124
+ return { a, b };
125
+ `,
126
+ bindings, null, undefined, polyfills,
127
+ );
128
+
129
+ expect(res.error).toBeUndefined();
130
+ expect(res.result).toEqual({ a: true, b: false });
131
+
132
+ await engine.close();
133
+ });
134
+
135
+ it("gets file stats via fs.stat", async () => {
136
+ const { adapter, engine } = await createTestAdapter();
137
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
138
+ const bindings = createVfsBindings(adapter);
139
+
140
+ await adapter.writeFile("/info.txt", "12345");
141
+
142
+ const res = await runtime.execute(
143
+ `
144
+ const s = await fs.stat("/info.txt");
145
+ return { isFile: s.isFile(), isDir: s.isDirectory(), size: s.size };
146
+ `,
147
+ bindings, null, undefined, polyfills,
148
+ );
149
+
150
+ expect(res.error).toBeUndefined();
151
+ expect(res.result).toEqual({ isFile: true, isDir: false, size: 5 });
152
+
153
+ await engine.close();
154
+ });
155
+
156
+ it("deletes files via fs.unlink", async () => {
157
+ const { adapter, engine } = await createTestAdapter();
158
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
159
+ const bindings = createVfsBindings(adapter);
160
+
161
+ await adapter.writeFile("/doomed.txt", "bye");
162
+
163
+ const res = await runtime.execute(
164
+ `
165
+ await fs.unlink("/doomed.txt");
166
+ return await fs.exists("/doomed.txt");
167
+ `,
168
+ bindings, null, undefined, polyfills,
169
+ );
170
+
171
+ expect(res.error).toBeUndefined();
172
+ expect(res.result).toBe(false);
173
+
174
+ await engine.close();
175
+ });
176
+
177
+ it("creates directories via fs.mkdir", async () => {
178
+ const { adapter, engine } = await createTestAdapter();
179
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
180
+ const bindings = createVfsBindings(adapter);
181
+
182
+ const res = await runtime.execute(
183
+ `
184
+ await fs.mkdir("/deep/nested/dir");
185
+ await fs.writeFile("/deep/nested/dir/file.txt", "hi");
186
+ return await fs.readFile("/deep/nested/dir/file.txt", "utf-8");
187
+ `,
188
+ bindings, null, undefined, polyfills,
189
+ );
190
+
191
+ expect(res.error).toBeUndefined();
192
+ expect(res.result).toBe("hi");
193
+
194
+ await engine.close();
195
+ });
196
+ });
197
+
198
+ describe("Buffer polyfill in isolate", () => {
199
+ it("supports from/toString with encodings", async () => {
200
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
201
+
202
+ const res = await runtime.execute(
203
+ `
204
+ const b = Buffer.from("hello");
205
+ const hex = b.toString("hex");
206
+ const b64 = b.toString("base64");
207
+ const back = Buffer.from(b64, "base64").toString("utf-8");
208
+ return { hex, b64, back };
209
+ `,
210
+ {}, null, undefined, polyfills,
211
+ );
212
+
213
+ expect(res.error).toBeUndefined();
214
+ expect(res.result).toEqual({
215
+ hex: "68656c6c6f",
216
+ b64: "aGVsbG8=",
217
+ back: "hello",
218
+ });
219
+ });
220
+
221
+ it("supports concat and alloc", async () => {
222
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
223
+
224
+ const res = await runtime.execute(
225
+ `
226
+ const a = Buffer.from("hel");
227
+ const b = Buffer.from("lo");
228
+ const c = Buffer.concat([a, b]);
229
+ return c.toString("utf-8");
230
+ `,
231
+ {}, null, undefined, polyfills,
232
+ );
233
+
234
+ expect(res.error).toBeUndefined();
235
+ expect(res.result).toBe("hello");
236
+ });
237
+ });
238
+
239
+ describe("path polyfill in isolate", () => {
240
+ it("join, basename, dirname, extname work", async () => {
241
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
242
+
243
+ const res = await runtime.execute(
244
+ `
245
+ return {
246
+ joined: path.join("/foo", "bar", "baz.txt"),
247
+ base: path.basename("/foo/bar/baz.txt"),
248
+ baseNoExt: path.basename("/foo/bar/baz.txt", ".txt"),
249
+ dir: path.dirname("/foo/bar/baz.txt"),
250
+ ext: path.extname("/foo/bar/baz.txt"),
251
+ };
252
+ `,
253
+ {}, null, undefined, polyfills,
254
+ );
255
+
256
+ expect(res.error).toBeUndefined();
257
+ expect(res.result).toEqual({
258
+ joined: "/foo/bar/baz.txt",
259
+ base: "baz.txt",
260
+ baseNoExt: "baz",
261
+ dir: "/foo/bar",
262
+ ext: ".txt",
263
+ });
264
+ });
265
+ });
266
+
267
+ describe("Polyfill basics in isolate", () => {
268
+ it("atob/btoa work", async () => {
269
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
270
+
271
+ const res = await runtime.execute(
272
+ `return { encoded: btoa("hello"), decoded: atob("aGVsbG8=") };`,
273
+ {}, null, undefined, polyfills,
274
+ );
275
+
276
+ expect(res.error).toBeUndefined();
277
+ expect(res.result).toEqual({ encoded: "aGVsbG8=", decoded: "hello" });
278
+ });
279
+
280
+ it("setTimeout with 0 delay works", async () => {
281
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
282
+
283
+ const res = await runtime.execute(
284
+ `
285
+ let called = false;
286
+ setTimeout(() => { called = true; }, 0);
287
+ await new Promise(r => setTimeout(r, 0));
288
+ // Give microtasks a chance to flush
289
+ await new Promise(r => setTimeout(r, 0));
290
+ return called;
291
+ `,
292
+ {}, null, undefined, polyfills,
293
+ );
294
+
295
+ expect(res.error).toBeUndefined();
296
+ expect(res.result).toBe(true);
297
+ });
298
+
299
+ it("crypto.randomUUID returns valid format", async () => {
300
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
301
+
302
+ const res = await runtime.execute(
303
+ `return crypto.randomUUID();`,
304
+ {}, null, undefined, polyfills,
305
+ );
306
+
307
+ expect(res.error).toBeUndefined();
308
+ expect(res.result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
309
+ });
310
+
311
+ it("Blob works", async () => {
312
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
313
+
314
+ const res = await runtime.execute(
315
+ `
316
+ const blob = new Blob(["hello ", "world"], { type: "text/plain" });
317
+ const text = await blob.text();
318
+ return { size: blob.size, type: blob.type, text };
319
+ `,
320
+ {}, null, undefined, polyfills,
321
+ );
322
+
323
+ expect(res.error).toBeUndefined();
324
+ expect(res.result).toEqual({ size: 11, type: "text/plain", text: "hello world" });
325
+ });
326
+
327
+ it("structuredClone works", async () => {
328
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
329
+
330
+ const res = await runtime.execute(
331
+ `
332
+ const obj = { a: 1, b: [2, 3] };
333
+ const clone = structuredClone(obj);
334
+ clone.b.push(4);
335
+ return { original: obj.b.length, cloned: clone.b.length };
336
+ `,
337
+ {}, null, undefined, polyfills,
338
+ );
339
+
340
+ expect(res.error).toBeUndefined();
341
+ expect(res.result).toEqual({ original: 2, cloned: 3 });
342
+ });
343
+
344
+ it("console.table works", async () => {
345
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
346
+
347
+ const res = await runtime.execute(
348
+ `console.table([{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }]);`,
349
+ {}, null, undefined, polyfills,
350
+ );
351
+
352
+ expect(res.error).toBeUndefined();
353
+ expect(res.stdout).toContain("Alice");
354
+ expect(res.stdout).toContain("Bob");
355
+ });
356
+
357
+ it("fetch() gives helpful error when network not configured", async () => {
358
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
359
+
360
+ const res = await runtime.execute(
361
+ `
362
+ try {
363
+ await fetch("https://example.com");
364
+ return "should not reach here";
365
+ } catch (e) {
366
+ return e.message;
367
+ }
368
+ `,
369
+ {}, null, undefined, polyfills,
370
+ );
371
+
372
+ expect(res.error).toBeUndefined();
373
+ expect(res.result).toMatch(/not available|network/i);
374
+ });
375
+ });
376
+
377
+ describe("Standard fetch API in isolate", () => {
378
+ it("fetches text content with standard API", async () => {
379
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
380
+ const fetchBinding = createFetchBinding(["example.com"], { dangerouslyAllowAll: true });
381
+ const bindings: Record<string, IsolateBinding> = {
382
+ __poncho_fetch: fetchBinding,
383
+ };
384
+
385
+ const res = await runtime.execute(
386
+ `
387
+ const resp = await fetch("https://example.com");
388
+ const text = await resp.text();
389
+ return { ok: resp.ok, status: resp.status, hasContent: text.length > 0 };
390
+ `,
391
+ bindings, null, undefined, polyfillsWithFetch,
392
+ );
393
+
394
+ expect(res.error).toBeUndefined();
395
+ const result = res.result as { ok: boolean; status: number; hasContent: boolean };
396
+ expect(result.ok).toBe(true);
397
+ expect(result.status).toBe(200);
398
+ expect(result.hasContent).toBe(true);
399
+ });
400
+
401
+ it("rejects requests to non-allowed domains", async () => {
402
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
403
+ const fetchBinding = createFetchBinding(["api.example.com"]);
404
+ const bindings: Record<string, IsolateBinding> = {
405
+ __poncho_fetch: fetchBinding,
406
+ };
407
+
408
+ const res = await runtime.execute(
409
+ `
410
+ try {
411
+ await fetch("https://evil.com/data");
412
+ return "should not reach here";
413
+ } catch (e) {
414
+ return e.message;
415
+ }
416
+ `,
417
+ bindings, null, undefined, polyfillsWithFetch,
418
+ );
419
+
420
+ expect(res.error).toBeUndefined();
421
+ expect(res.result).toMatch(/blocked.*evil\.com/i);
422
+ });
423
+ });
424
+
425
+ describe("VFS tenant isolation", () => {
426
+ it("isolates file systems between tenants", async () => {
427
+ const engine = new InMemoryEngine("test");
428
+ await engine.initialize();
429
+ const runtime = createIsolateRuntime(DEFAULT_CONFIG);
430
+
431
+ const adapter1 = new PonchoFsAdapter(engine, "tenant-a", LIMITS);
432
+ const adapter2 = new PonchoFsAdapter(engine, "tenant-b", LIMITS);
433
+
434
+ const bindings1 = createVfsBindings(adapter1);
435
+ const bindings2 = createVfsBindings(adapter2);
436
+
437
+ // Write in tenant A
438
+ await runtime.execute(
439
+ `await fs.writeFile("/secret.txt", "tenant-a-data");`,
440
+ bindings1, null, undefined, polyfills,
441
+ );
442
+
443
+ // Tenant B should not see it
444
+ const res = await runtime.execute(
445
+ `return await fs.exists("/secret.txt");`,
446
+ bindings2, null, undefined, polyfills,
447
+ );
448
+
449
+ expect(res.result).toBe(false);
450
+
451
+ await engine.close();
452
+ });
453
+ });