@poncho-ai/harness 0.34.1 → 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 (64) hide show
  1. package/.turbo/turbo-build.log +12 -11
  2. package/.turbo/turbo-lint.log +6 -0
  3. package/.turbo/turbo-test.log +27100 -0
  4. package/CHANGELOG.md +37 -0
  5. package/dist/chunk-MCKGQKYU.js +15 -0
  6. package/dist/dist-3KMQR4IO.js +27092 -0
  7. package/dist/index.d.ts +553 -29
  8. package/dist/index.js +3132 -1902
  9. package/dist/isolate-5MISBSUK.js +733 -0
  10. package/dist/isolate-5R6762YA.js +605 -0
  11. package/dist/isolate-KUZ5NOPG.js +727 -0
  12. package/dist/isolate-LOL3T7RA.js +729 -0
  13. package/dist/isolate-N22X4TCE.js +740 -0
  14. package/dist/isolate-T7WXM7IL.js +1490 -0
  15. package/dist/isolate-TCWTUVG4.js +1532 -0
  16. package/dist/isolate-WFOLANOB.js +768 -0
  17. package/package.json +24 -4
  18. package/scripts/migrate-to-engine.mjs +556 -0
  19. package/src/config.ts +112 -1
  20. package/src/harness.ts +282 -91
  21. package/src/index.ts +7 -0
  22. package/src/isolate/bindings.ts +206 -0
  23. package/src/isolate/bundler.ts +179 -0
  24. package/src/isolate/index.ts +10 -0
  25. package/src/isolate/polyfills.ts +796 -0
  26. package/src/isolate/run-code-tool.ts +220 -0
  27. package/src/isolate/runtime.ts +286 -0
  28. package/src/isolate/type-stubs.ts +196 -0
  29. package/src/mcp.ts +140 -9
  30. package/src/memory.ts +142 -191
  31. package/src/reminder-store.ts +7 -235
  32. package/src/reminder-tools.ts +15 -2
  33. package/src/secrets-store.ts +163 -0
  34. package/src/state.ts +22 -1291
  35. package/src/storage/engine.ts +106 -0
  36. package/src/storage/index.ts +59 -0
  37. package/src/storage/memory-engine.ts +588 -0
  38. package/src/storage/postgres-engine.ts +139 -0
  39. package/src/storage/schema.ts +145 -0
  40. package/src/storage/sql-dialect.ts +963 -0
  41. package/src/storage/sqlite-engine.ts +99 -0
  42. package/src/storage/store-adapters.ts +100 -0
  43. package/src/subagent-manager.ts +1 -0
  44. package/src/subagent-tools.ts +1 -0
  45. package/src/telemetry.ts +5 -1
  46. package/src/tenant-token.ts +42 -0
  47. package/src/todo-tools.ts +1 -136
  48. package/src/upload-store.ts +1 -0
  49. package/src/vfs/bash-manager.ts +120 -0
  50. package/src/vfs/bash-tool.ts +59 -0
  51. package/src/vfs/create-bash-fs.ts +32 -0
  52. package/src/vfs/edit-file-tool.ts +72 -0
  53. package/src/vfs/index.ts +5 -0
  54. package/src/vfs/poncho-fs-adapter.ts +267 -0
  55. package/src/vfs/protected-fs.ts +177 -0
  56. package/src/vfs/read-file-tool.ts +103 -0
  57. package/src/vfs/write-file-tool.ts +49 -0
  58. package/test/harness.test.ts +30 -36
  59. package/test/isolate-vfs.test.ts +453 -0
  60. package/test/isolate.test.ts +252 -0
  61. package/test/state.test.ts +4 -27
  62. package/test/storage-engine.test.ts +250 -0
  63. package/test/vfs.test.ts +242 -0
  64. package/src/kv-store.ts +0 -216
package/src/harness.ts CHANGED
@@ -14,17 +14,33 @@ import type {
14
14
  } from "@poncho-ai/sdk";
15
15
  import { defineTool, getTextContent } from "@poncho-ai/sdk";
16
16
  import type { UploadStore } from "./upload-store.js";
17
- import { PONCHO_UPLOAD_SCHEME, deriveUploadKey } from "./upload-store.js";
17
+ import { PONCHO_UPLOAD_SCHEME, VFS_SCHEME, deriveUploadKey } from "./upload-store.js";
18
+ import type { StorageEngine } from "./storage/engine.js";
19
+ import { createStorageEngine, type StorageProvider } from "./storage/index.js";
20
+ import {
21
+ createConversationStoreFromEngine,
22
+ createMemoryStoreFromEngine,
23
+ createTodoStoreFromEngine,
24
+ createReminderStoreFromEngine,
25
+ } from "./storage/store-adapters.js";
26
+ import { BashEnvironmentManager } from "./vfs/bash-manager.js";
27
+ import { createBashTool } from "./vfs/bash-tool.js";
28
+ import { createReadFileTool } from "./vfs/read-file-tool.js";
29
+ import { createEditFileTool } from "./vfs/edit-file-tool.js";
30
+ import { createWriteFileTool } from "./vfs/write-file-tool.js";
31
+ import { PonchoFsAdapter } from "./vfs/poncho-fs-adapter.js";
18
32
  import { parseAgentFile, parseAgentMarkdown, renderAgentPrompt, type ParsedAgent, type AgentFrontmatter } from "./agent-parser.js";
19
33
  import { loadPonchoConfig, resolveMemoryConfig, resolveStateConfig, type PonchoConfig, type ToolAccess, type BuiltInToolToggles } from "./config.js";
20
- import { createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createWriteTool, ponchoDocsTool } from "./default-tools.js";
34
+ import { ponchoDocsTool } from "./default-tools.js";
21
35
  import {
22
36
  createMemoryStore,
23
37
  createMemoryTools,
38
+ type MemoryConfig,
24
39
  type MemoryStore,
25
40
  } from "./memory.js";
26
41
  import { createTodoStore, createTodoTools, type TodoItem, type TodoStore } from "./todo-tools.js";
27
42
  import { createReminderStore, type ReminderStore } from "./reminder-store.js";
43
+ import { createSecretsStore, resolveEnv, type SecretsStore } from "./secrets-store.js";
28
44
  import { createReminderTools } from "./reminder-tools.js";
29
45
  import { LocalMcpBridge } from "./mcp.js";
30
46
  import { createModelProvider, getModelContextWindow, type ModelProviderFactory, type ProviderConfig } from "./model-factory.js";
@@ -610,14 +626,14 @@ function extractMediaFromToolOutput(output: unknown): {
610
626
  obj.type === "file" &&
611
627
  typeof obj.data === "string" &&
612
628
  typeof obj.mediaType === "string" &&
613
- (obj.mediaType as string).startsWith("image/")
629
+ ((obj.mediaType as string).startsWith("image/") || obj.mediaType === "application/pdf")
614
630
  ) {
615
631
  mediaItems.push({
616
632
  type: "media",
617
633
  data: obj.data as string,
618
634
  mediaType: obj.mediaType as string,
619
635
  });
620
- return { type: "file", mediaType: obj.mediaType, filename: obj.filename ?? "image", _stripped: true };
636
+ return { type: "file", mediaType: obj.mediaType, filename: obj.filename ?? "file", _stripped: true };
621
637
  }
622
638
  const out: Record<string, unknown> = {};
623
639
  for (const [k, v] of Object.entries(obj)) out[k] = walk(v);
@@ -639,8 +655,11 @@ export class AgentHarness {
639
655
  readonly uploadStore?: UploadStore;
640
656
  private skillContextWindow = "";
641
657
  private memoryStore?: MemoryStore;
658
+ private readonly tenantMemoryStores = new Map<string, MemoryStore>();
659
+ private memoryConfig?: MemoryConfig;
642
660
  private todoStore?: TodoStore;
643
661
  reminderStore?: ReminderStore;
662
+ secretsStore?: SecretsStore;
644
663
  private loadedConfig?: PonchoConfig;
645
664
  private loadedSkills: SkillMetadata[] = [];
646
665
  private skillFingerprint = "";
@@ -662,6 +681,11 @@ export class AgentHarness {
662
681
  private subagentManager?: SubagentManager;
663
682
  private readonly archivedToolResultsByConversation = new Map<string, Record<string, ArchivedToolResult>>();
664
683
 
684
+ /** Unified storage engine (replaces individual KV-backed stores). */
685
+ storageEngine?: StorageEngine;
686
+ /** Bash environment manager (creates per-tenant bash instances). */
687
+ private bashManager?: BashEnvironmentManager;
688
+
665
689
  private resolveToolAccess(toolName: string): ToolAccess {
666
690
  const tools = this.loadedConfig?.tools;
667
691
  if (!tools) return true;
@@ -718,23 +742,8 @@ export class AgentHarness {
718
742
  }
719
743
 
720
744
  private registerConfiguredBuiltInTools(config: PonchoConfig | undefined): void {
721
- for (const tool of createDefaultTools(this.workingDir)) {
722
- if (this.isToolEnabled(tool.name)) {
723
- this.registerIfMissing(tool);
724
- }
725
- }
726
- if (this.isToolEnabled("write_file")) {
727
- this.registerIfMissing(createWriteTool(this.workingDir));
728
- }
729
- if (this.isToolEnabled("edit_file")) {
730
- this.registerIfMissing(createEditTool(this.workingDir));
731
- }
732
- if (this.isToolEnabled("delete_file")) {
733
- this.registerIfMissing(createDeleteTool(this.workingDir));
734
- }
735
- if (this.isToolEnabled("delete_directory")) {
736
- this.registerIfMissing(createDeleteDirectoryTool(this.workingDir));
737
- }
745
+ // Old file tools (read_file, write_file, etc.) are replaced by the bash tool.
746
+ // Only register search tools, poncho_docs, and get_tool_result_by_id.
738
747
  for (const tool of createSearchTools()) {
739
748
  if (this.isToolEnabled(tool.name)) {
740
749
  this.registerIfMissing(tool);
@@ -799,6 +808,33 @@ export class AgentHarness {
799
808
  });
800
809
  }
801
810
 
811
+ private createVfsAccess(tenantId: string): NonNullable<ToolContext["vfs"]> {
812
+ const adapter = this.bashManager!.getAdapter(tenantId);
813
+ return {
814
+ readFile: (path: string) => adapter.readFileBuffer(path),
815
+ readText: (path: string) => adapter.readFile(path),
816
+ writeFile: (path: string, content: Uint8Array, mimeType?: string) =>
817
+ adapter.writeFile(path, content),
818
+ writeText: (path: string, content: string) =>
819
+ adapter.writeFile(path, content),
820
+ exists: (path: string) => adapter.exists(path),
821
+ stat: async (path: string) => {
822
+ const s = await adapter.stat(path);
823
+ return {
824
+ size: s.size,
825
+ isDirectory: s.isDirectory,
826
+ mimeType: undefined,
827
+ updatedAt: s.mtime.getTime(),
828
+ };
829
+ },
830
+ readdir: (path: string) => adapter.readdir(path),
831
+ mkdir: (path: string, options?: { recursive?: boolean }) =>
832
+ adapter.mkdir(path, options),
833
+ rm: (path: string, options?: { recursive?: boolean }) =>
834
+ adapter.rm(path, options),
835
+ };
836
+ }
837
+
802
838
  private shouldEnableWriteTool(): boolean {
803
839
  const override = process.env.PONCHO_FS_WRITE?.toLowerCase();
804
840
  if (override === "1" || override === "true" || override === "yes") {
@@ -975,6 +1011,35 @@ export class AgentHarness {
975
1011
  return this.todoStore.get(conversationId);
976
1012
  }
977
1013
 
1014
+ /**
1015
+ * Get a memory store, optionally scoped to a tenant.
1016
+ * Returns the default (agent-wide) store when tenantId is null/undefined.
1017
+ */
1018
+ private getMemoryStore(tenantId?: string): MemoryStore | undefined {
1019
+ if (!this.memoryConfig?.enabled) return undefined;
1020
+ if (!tenantId) return this.memoryStore;
1021
+
1022
+ let store = this.tenantMemoryStores.get(tenantId);
1023
+ if (!store) {
1024
+ if (this.storageEngine) {
1025
+ store = createMemoryStoreFromEngine(this.storageEngine, tenantId);
1026
+ } else {
1027
+ const agentId = this.parsedAgent?.frontmatter.id ?? this.parsedAgent?.frontmatter.name ?? "unknown";
1028
+ store = createMemoryStore(agentId, this.memoryConfig, {
1029
+ workingDir: this.workingDir,
1030
+ tenantId,
1031
+ });
1032
+ }
1033
+ this.tenantMemoryStores.set(tenantId, store);
1034
+ // Evict oldest entries if cache grows too large
1035
+ if (this.tenantMemoryStores.size > 100) {
1036
+ const oldest = this.tenantMemoryStores.keys().next().value;
1037
+ if (oldest) this.tenantMemoryStores.delete(oldest);
1038
+ }
1039
+ }
1040
+ return store;
1041
+ }
1042
+
978
1043
  private listActiveSkills(): string[] {
979
1044
  return [...this.activeSkillNames].sort();
980
1045
  }
@@ -1164,6 +1229,7 @@ export class AgentHarness {
1164
1229
  if (!this.mcpBridge) {
1165
1230
  return;
1166
1231
  }
1232
+
1167
1233
  const requestedPatterns = this.getRequestedMcpPatterns();
1168
1234
  this.dispatcher.unregisterMany(this.registeredMcpToolNames);
1169
1235
  this.registeredMcpToolNames.clear();
@@ -1354,29 +1420,74 @@ export class AgentHarness {
1354
1420
  this.registerSkillTools(skillMetadata);
1355
1421
  const agentId = this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name;
1356
1422
 
1423
+ // --- Unified Storage Engine ---
1424
+ const storageProvider = (config?.storage?.provider ?? "sqlite") as StorageProvider;
1425
+ const engine = createStorageEngine({
1426
+ provider: storageProvider,
1427
+ workingDir: this.workingDir,
1428
+ agentId,
1429
+ urlEnv: config?.storage?.urlEnv,
1430
+ });
1431
+ await engine.initialize();
1432
+ this.storageEngine = engine;
1433
+
1434
+ // --- Bash Environment Manager ---
1435
+ const maxFileSize = config?.storage?.limits?.maxFileSize ?? 100 * 1024 * 1024; // 100MB
1436
+ const maxTotalStorage = config?.storage?.limits?.maxTotalStorage ?? 1024 * 1024 * 1024; // 1GB
1437
+ const bashWorkingDir = this.environment === "production" ? null : this.workingDir;
1438
+ this.bashManager = new BashEnvironmentManager(
1439
+ engine,
1440
+ { maxFileSize, maxTotalStorage },
1441
+ bashWorkingDir,
1442
+ config?.bash,
1443
+ config?.network,
1444
+ );
1445
+ // Register VFS tools
1446
+ this.registerIfMissing(createBashTool(this.bashManager));
1447
+ this.registerIfMissing(createReadFileTool(engine));
1448
+ this.registerIfMissing(createEditFileTool(engine));
1449
+ this.registerIfMissing(createWriteFileTool(engine));
1450
+
1451
+ // --- Isolate (V8 sandboxed code execution) ---
1452
+ if (config?.isolate) {
1453
+ const { createRunCodeTool, buildRunCodeDescription, bundleLibraries } = await import("./isolate/index.js");
1454
+ let libraryPreamble: string | null = null;
1455
+ if (config.isolate.libraries?.length) {
1456
+ libraryPreamble = await bundleLibraries(config.isolate.libraries, this.workingDir);
1457
+ }
1458
+ const runCodeTool = createRunCodeTool({
1459
+ config: config.isolate,
1460
+ bashManager: this.bashManager,
1461
+ libraryPreamble,
1462
+ description: buildRunCodeDescription(config.isolate, !!config.network),
1463
+ network: config.network,
1464
+ });
1465
+ this.registerIfMissing(runCodeTool);
1466
+ }
1467
+
1468
+ // --- Memory (engine-backed or legacy fallback) ---
1469
+ this.memoryConfig = memoryConfig ?? undefined;
1357
1470
  if (memoryConfig?.enabled) {
1358
- this.memoryStore = createMemoryStore(
1359
- agentId,
1360
- memoryConfig,
1361
- { workingDir: this.workingDir },
1362
- );
1471
+ this.memoryStore = createMemoryStoreFromEngine(engine);
1363
1472
  this.dispatcher.registerMany(
1364
- createMemoryTools(this.memoryStore, {
1365
- maxRecallConversations: memoryConfig.maxRecallConversations,
1366
- }),
1473
+ createMemoryTools(
1474
+ (ctx) => this.getMemoryStore(ctx.tenantId) ?? this.memoryStore!,
1475
+ { maxRecallConversations: memoryConfig.maxRecallConversations },
1476
+ ),
1367
1477
  );
1368
1478
  }
1369
1479
 
1370
- const stateConfig = resolveStateConfig(config);
1371
- this.todoStore = createTodoStore(agentId, stateConfig, { workingDir: this.workingDir });
1480
+ // --- Todos (engine-backed) ---
1481
+ this.todoStore = createTodoStoreFromEngine(engine);
1372
1482
  for (const tool of createTodoTools(this.todoStore)) {
1373
1483
  if (this.isToolEnabled(tool.name)) {
1374
1484
  this.registerIfMissing(tool);
1375
1485
  }
1376
1486
  }
1377
1487
 
1488
+ // --- Reminders (engine-backed) ---
1378
1489
  if (config?.reminders?.enabled) {
1379
- this.reminderStore = createReminderStore(agentId, stateConfig, { workingDir: this.workingDir });
1490
+ this.reminderStore = createReminderStoreFromEngine(engine);
1380
1491
  for (const tool of createReminderTools(this.reminderStore)) {
1381
1492
  if (this.isToolEnabled(tool.name)) {
1382
1493
  this.registerIfMissing(tool);
@@ -1393,6 +1504,17 @@ export class AgentHarness {
1393
1504
  });
1394
1505
  }
1395
1506
 
1507
+ // Secrets store for per-tenant env var overrides
1508
+ const stateConfig = resolveStateConfig(config);
1509
+ const authTokenEnv = config?.auth?.tokenEnv ?? "PONCHO_AUTH_TOKEN";
1510
+ const authToken = process.env[authTokenEnv];
1511
+ if (authToken) {
1512
+ this.secretsStore = createSecretsStore(agentId, authToken, stateConfig, { workingDir: this.workingDir });
1513
+ bridge.setEnvResolver(async (tenantId, envName) => {
1514
+ return resolveEnv(this.secretsStore, tenantId, envName);
1515
+ });
1516
+ }
1517
+
1396
1518
  await bridge.startLocalServers();
1397
1519
  await bridge.discoverTools();
1398
1520
  await this.refreshMcpTools("initialize");
@@ -1444,60 +1566,22 @@ export class AgentHarness {
1444
1566
  };
1445
1567
  }
1446
1568
 
1447
- if (provider === "upstash") {
1448
- const urlEnv = config.storage?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
1449
- const tokenEnv = config.storage?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
1450
- const baseUrl = (process.env[urlEnv] ?? "").replace(/\/+$/, "");
1451
- const token = process.env[tokenEnv] ?? "";
1452
- if (!baseUrl || !token) return undefined;
1453
- const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
1454
- return {
1455
- async save(json: string) {
1456
- await fetch(`${baseUrl}/set/${encodeURIComponent(stateKey)}/${encodeURIComponent(json)}`, { method: "POST", headers });
1457
- },
1458
- async load() {
1459
- const res = await fetch(`${baseUrl}/get/${encodeURIComponent(stateKey)}`, { headers });
1460
- if (!res.ok) return undefined;
1461
- const body = await res.json() as { result?: string | null };
1462
- return body.result ?? undefined;
1463
- },
1464
- };
1465
- }
1466
-
1467
- if (provider === "redis") {
1468
- const urlEnv = config.storage?.urlEnv ?? "REDIS_URL";
1469
- const url = process.env[urlEnv] ?? "";
1470
- if (!url) return undefined;
1471
- let clientPromise: Promise<{ get(k: string): Promise<string | null>; set(k: string, v: string): Promise<unknown> } | undefined> | undefined;
1472
- const getClient = () => {
1473
- if (!clientPromise) {
1474
- clientPromise = (async () => {
1475
- try {
1476
- const mod = (await import("redis")) as unknown as {
1477
- createClient: (opts: { url: string }) => {
1478
- connect(): Promise<unknown>;
1479
- get(k: string): Promise<string | null>;
1480
- set(k: string, v: string): Promise<unknown>;
1481
- };
1482
- };
1483
- const c = mod.createClient({ url });
1484
- await c.connect();
1485
- return c;
1486
- } catch { return undefined; }
1487
- })();
1488
- }
1489
- return clientPromise;
1490
- };
1569
+ // For sqlite, postgresql, and all other providers: use local file persistence
1570
+ // (same as "local" above). The old upstash/redis branches have been removed.
1571
+ if (provider === "sqlite" || provider === "postgresql") {
1572
+ const { resolve: pathResolve } = await import("node:path");
1573
+ const { homedir: home } = await import("node:os");
1574
+ const stateDir = pathResolve(home(), ".poncho", "browser-state");
1575
+ const filePath = pathResolve(stateDir, `${sessionId}.json`);
1491
1576
  return {
1492
1577
  async save(json: string) {
1493
- const c = await getClient();
1494
- if (c) await c.set(stateKey, json);
1578
+ const { mkdir, writeFile } = await import("node:fs/promises");
1579
+ await mkdir(stateDir, { recursive: true });
1580
+ await writeFile(filePath, json, "utf8");
1495
1581
  },
1496
1582
  async load() {
1497
- const c = await getClient();
1498
- if (!c) return undefined;
1499
- const val = await c.get(stateKey);
1500
- return val ?? undefined;
1583
+ const { readFile } = await import("node:fs/promises");
1584
+ try { return await readFile(filePath, "utf8"); } catch { return undefined; }
1501
1585
  },
1502
1586
  };
1503
1587
  }
@@ -1589,12 +1673,20 @@ export class AgentHarness {
1589
1673
  this.otlpTracerProvider = undefined;
1590
1674
  }
1591
1675
  this.hasOtlpExporter = false;
1676
+
1677
+ // Cleanup bash environments and storage engine
1678
+ this.bashManager?.destroyAll();
1679
+ await this.storageEngine?.close();
1592
1680
  }
1593
1681
 
1594
1682
  listTools(): ToolDefinition[] {
1595
1683
  return this.dispatcher.list();
1596
1684
  }
1597
1685
 
1686
+ listSkills(): Array<{ name: string; description: string }> {
1687
+ return this.loadedSkills.map((s) => ({ name: s.name, description: s.description }));
1688
+ }
1689
+
1598
1690
  /**
1599
1691
  * Wraps the run() generator with an OTel root span (invoke_agent) so all
1600
1692
  * child spans (LLM calls via AI SDK, tool execution) group under one trace.
@@ -1609,6 +1701,7 @@ export class AgentHarness {
1609
1701
  attributes: {
1610
1702
  "gen_ai.operation.name": "invoke_agent",
1611
1703
  ...(input.conversationId ? { "gen_ai.conversation.id": input.conversationId } : {}),
1704
+ ...(input.tenantId ? { "tenant.id": input.tenantId } : {}),
1612
1705
  },
1613
1706
  });
1614
1707
 
@@ -1662,8 +1755,9 @@ export class AgentHarness {
1662
1755
  await this.initialize();
1663
1756
  }
1664
1757
  // Start memory + todo fetches early so they overlap with refresh I/O
1665
- const memoryPromise = this.memoryStore
1666
- ? this.memoryStore.getMainMemory()
1758
+ const activeMemoryStore = this.getMemoryStore(input.tenantId);
1759
+ const memoryPromise = activeMemoryStore
1760
+ ? activeMemoryStore.getMainMemory()
1667
1761
  : undefined;
1668
1762
  const todosPromise = this.todoStore
1669
1763
  ? this.todoStore.get(input.conversationId ?? "__default__")
@@ -1671,6 +1765,16 @@ export class AgentHarness {
1671
1765
  await this.refreshAgentIfChanged();
1672
1766
  await this.refreshSkillsIfChanged();
1673
1767
 
1768
+ // Deferred MCP discovery: servers that couldn't discover at startup because the
1769
+ // env var was missing (tenant secrets provide the token instead).
1770
+ if (input.tenantId && this.mcpBridge?.hasDeferredServers()) {
1771
+ const newTools = await this.mcpBridge.discoverAndLoadDeferred(input.tenantId);
1772
+ for (const tool of newTools) {
1773
+ this.dispatcher.register(tool);
1774
+ this.registeredMcpToolNames.add(tool.name);
1775
+ }
1776
+ }
1777
+
1674
1778
  let agent = this.parsedAgent as ParsedAgent;
1675
1779
  const runId = `run_${randomUUID()}`;
1676
1780
  const start = now();
@@ -1763,11 +1867,56 @@ ${boundedMainMemory.trim()}`
1763
1867
  ? `\n\n## Open Tasks\n\n${openTodos.map((t) => `- [${t.status === "in_progress" ? "IN PROGRESS" : "PENDING"}] ${t.content} (id: ${t.id})`).join("\n")}`
1764
1868
  : "";
1765
1869
 
1870
+ const fsContext = this.bashManager
1871
+ ? `\n\n## Filesystem
1872
+
1873
+ You have a persistent virtual filesystem at \`/\`. Files you create are durable across conversations.
1874
+ Use the \`bash\` tool for all file operations (cat, echo, grep, awk, jq, sed, find, etc.).
1875
+
1876
+ Filesystem layout:
1877
+ - \`/\` — your working directory (persistent, database-backed)${
1878
+ this.environment !== "production"
1879
+ ? `\n- \`/project/\` — the project source code (read-write in dev; protected paths like .env, .git/ are blocked)`
1880
+ : ""
1881
+ }
1882
+
1883
+ Examples:${
1884
+ this.environment !== "production"
1885
+ ? `\n- Read a project file: \`cat /project/src/index.ts\``
1886
+ : ""
1887
+ }
1888
+ - Write a working file: \`echo "data" > /notes.txt\`
1889
+ - Process data: \`cat /data.csv | awk -F, '{print $2}' | sort | uniq -c\`
1890
+
1891
+ Files in the VFS are accessible to the user via \`/api/vfs/{path}\`. For example, a file at \`/downloads/report.pdf\` can be linked as \`/api/vfs/downloads/report.pdf\`. Use this to share downloadable files with the user.`
1892
+ : "";
1893
+
1894
+ // Isolate context (code execution guidance + type stubs)
1895
+ let isolateContext = "";
1896
+ if (this.loadedConfig?.isolate && this.dispatcher.get("run_code")) {
1897
+ const { generateIsolateTypeStubs } = await import("./isolate/index.js");
1898
+ const typeStubs = generateIsolateTypeStubs(this.loadedConfig.isolate);
1899
+ isolateContext = `\n\n## Code Execution
1900
+
1901
+ You have a \`run_code\` tool for executing JavaScript/TypeScript in a sandboxed V8 isolate.
1902
+
1903
+ **When to use \`run_code\` vs \`bash\`:**
1904
+ - \`bash\`: file manipulation, text processing with unix tools, shell pipelines
1905
+ - \`run_code\`: complex data processing, structured data, npm libraries, multi-step logic, binary file generation
1906
+
1907
+ **API reference (available inside the isolate):**
1908
+ \`\`\`typescript
1909
+ ${typeStubs}
1910
+ \`\`\`
1911
+
1912
+ Code is wrapped in an async IIFE — use \`return\` to return a value to the tool result.`;
1913
+ }
1914
+
1766
1915
  const buildSystemPrompt = (): string => {
1767
1916
  const agentPrompt = renderCurrentAgentPrompt();
1768
1917
  const promptWithSkills = this.skillContextWindow
1769
- ? `${agentPrompt}${developmentContext}\n\n${this.skillContextWindow}${browserContext}`
1770
- : `${agentPrompt}${developmentContext}${browserContext}`;
1918
+ ? `${agentPrompt}${developmentContext}\n\n${this.skillContextWindow}${browserContext}${fsContext}${isolateContext}`
1919
+ : `${agentPrompt}${developmentContext}${browserContext}${fsContext}${isolateContext}`;
1771
1920
  const timeContext = this.reminderStore
1772
1921
  ? `\n\nCurrent UTC time: ${new Date().toISOString()}`
1773
1922
  : "";
@@ -1970,9 +2119,37 @@ ${boundedMainMemory.trim()}`
1970
2119
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1971
2120
  const rich = (meta as any)?._richToolResults as unknown[] | undefined;
1972
2121
  if (rich && rich.length > 0) {
1973
- // The rich array already conforms to the AI SDK ToolContent shape
1974
- // (tool-result parts with multi-part content outputs). Cast
1975
- // through `any` because the exact generic types are internal.
2122
+ // Resolve any vfs:// references in media items before sending
2123
+ // to the model. This keeps conversation history lightweight
2124
+ // (only stores the reference) while materializing the actual
2125
+ // bytes on demand at model-request time.
2126
+ if (this.storageEngine) {
2127
+ const tid = input.tenantId ?? "__default__";
2128
+ for (const part of rich) {
2129
+ const p = part as Record<string, unknown>;
2130
+ if (p.output && typeof p.output === "object") {
2131
+ const out = p.output as Record<string, unknown>;
2132
+ if (Array.isArray(out.value)) {
2133
+ for (let i = 0; i < out.value.length; i++) {
2134
+ const item = out.value[i] as Record<string, unknown>;
2135
+ if (
2136
+ item.type === "media" &&
2137
+ typeof item.data === "string" &&
2138
+ (item.data as string).startsWith(VFS_SCHEME)
2139
+ ) {
2140
+ try {
2141
+ const vfsPath = (item.data as string).slice(VFS_SCHEME.length);
2142
+ const buf = await this.storageEngine.vfs.readFile(tid, vfsPath);
2143
+ item.data = Buffer.from(buf).toString("base64");
2144
+ } catch {
2145
+ // File no longer available; leave as-is
2146
+ }
2147
+ }
2148
+ }
2149
+ }
2150
+ }
2151
+ }
2152
+ }
1976
2153
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1977
2154
  return [{ role: "tool" as const, content: rich as any }];
1978
2155
  }
@@ -2126,7 +2303,11 @@ ${boundedMainMemory.trim()}`
2126
2303
  if (!isSupportedImage && !isSupportedFile && isTextBasedMime(part.mediaType)) {
2127
2304
  let textContent: string;
2128
2305
  try {
2129
- if (part.data.startsWith(PONCHO_UPLOAD_SCHEME) && this.uploadStore) {
2306
+ if (part.data.startsWith(VFS_SCHEME) && this.storageEngine) {
2307
+ const vfsPath = part.data.slice(VFS_SCHEME.length);
2308
+ const buf = await this.storageEngine.vfs.readFile(input.tenantId ?? "__default__", vfsPath);
2309
+ textContent = Buffer.from(buf).toString("utf8");
2310
+ } else if (part.data.startsWith(PONCHO_UPLOAD_SCHEME) && this.uploadStore) {
2130
2311
  const buf = await this.uploadStore.get(part.data);
2131
2312
  textContent = buf.toString("utf8");
2132
2313
  } else if (part.data.startsWith("https://") || part.data.startsWith("http://")) {
@@ -2155,7 +2336,11 @@ ${boundedMainMemory.trim()}`
2155
2336
  // fetch URLs itself (which fails for private blob stores).
2156
2337
  let resolvedData: string;
2157
2338
  try {
2158
- if (part.data.startsWith(PONCHO_UPLOAD_SCHEME) && this.uploadStore) {
2339
+ if (part.data.startsWith(VFS_SCHEME) && this.storageEngine) {
2340
+ const vfsPath = part.data.slice(VFS_SCHEME.length);
2341
+ const buf = await this.storageEngine.vfs.readFile(input.tenantId ?? "__default__", vfsPath);
2342
+ resolvedData = Buffer.from(buf).toString("base64");
2343
+ } else if (part.data.startsWith(PONCHO_UPLOAD_SCHEME) && this.uploadStore) {
2159
2344
  const buf = await this.uploadStore.get(part.data);
2160
2345
  resolvedData = buf.toString("base64");
2161
2346
  } else if (part.data.startsWith("https://") || part.data.startsWith("http://")) {
@@ -2593,6 +2778,10 @@ ${boundedMainMemory.trim()}`
2593
2778
  parameters: input.parameters ?? {},
2594
2779
  abortSignal: input.abortSignal,
2595
2780
  conversationId: input.conversationId,
2781
+ tenantId: input.tenantId,
2782
+ vfs: this.bashManager
2783
+ ? this.createVfsAccess(input.tenantId ?? "__default__")
2784
+ : undefined,
2596
2785
  };
2597
2786
 
2598
2787
  const toolResultsForModel: Array<{
@@ -2785,6 +2974,7 @@ ${boundedMainMemory.trim()}`
2785
2974
  return;
2786
2975
  }
2787
2976
 
2977
+ const callInputMap = new Map(approvedCalls.map((c) => [c.id, c.input]));
2788
2978
  for (const result of batchResults) {
2789
2979
  const span = toolSpans.get(result.callId);
2790
2980
  if (result.error) {
@@ -2838,6 +3028,7 @@ ${boundedMainMemory.trim()}`
2838
3028
  yield pushEvent({
2839
3029
  type: "tool:completed",
2840
3030
  tool: result.tool,
3031
+ input: callInputMap.get(result.callId),
2841
3032
  output: result.output,
2842
3033
  duration: now() - batchStart,
2843
3034
  outputTokenEstimate,
package/src/index.ts CHANGED
@@ -17,6 +17,13 @@ export * from "./reminder-tools.js";
17
17
  export * from "./state.js";
18
18
  export * from "./upload-store.js";
19
19
  export * from "./telemetry.js";
20
+ export * from "./secrets-store.js";
21
+ export * from "./storage/index.js";
22
+ export * from "./storage/store-adapters.js";
23
+ export { PonchoFsAdapter } from "./vfs/poncho-fs-adapter.js";
24
+ export { BashEnvironmentManager } from "./vfs/bash-manager.js";
25
+ export { createBashTool } from "./vfs/bash-tool.js";
26
+ export * from "./tenant-token.js";
20
27
  export * from "./tool-dispatcher.js";
21
28
  export * from "./subagent-manager.js";
22
29
  export * from "./subagent-tools.js";