@poncho-ai/harness 0.35.0 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +12 -11
- package/CHANGELOG.md +25 -0
- package/dist/index.d.ts +485 -29
- package/dist/index.js +2839 -2114
- package/dist/isolate-TCWTUVG4.js +1532 -0
- package/package.json +23 -4
- package/scripts/migrate-to-engine.mjs +556 -0
- package/src/config.ts +106 -1
- package/src/harness.ts +226 -91
- package/src/index.ts +5 -0
- package/src/isolate/bindings.ts +206 -0
- package/src/isolate/bundler.ts +179 -0
- package/src/isolate/index.ts +10 -0
- package/src/isolate/polyfills.ts +796 -0
- package/src/isolate/run-code-tool.ts +220 -0
- package/src/isolate/runtime.ts +286 -0
- package/src/isolate/type-stubs.ts +196 -0
- package/src/memory.ts +129 -198
- package/src/reminder-store.ts +3 -237
- package/src/secrets-store.ts +2 -91
- package/src/state.ts +11 -1302
- package/src/storage/engine.ts +106 -0
- package/src/storage/index.ts +59 -0
- package/src/storage/memory-engine.ts +588 -0
- package/src/storage/postgres-engine.ts +139 -0
- package/src/storage/schema.ts +145 -0
- package/src/storage/sql-dialect.ts +963 -0
- package/src/storage/sqlite-engine.ts +99 -0
- package/src/storage/store-adapters.ts +100 -0
- package/src/todo-tools.ts +1 -136
- package/src/upload-store.ts +1 -0
- package/src/vfs/bash-manager.ts +120 -0
- package/src/vfs/bash-tool.ts +59 -0
- package/src/vfs/create-bash-fs.ts +32 -0
- package/src/vfs/edit-file-tool.ts +72 -0
- package/src/vfs/index.ts +5 -0
- package/src/vfs/poncho-fs-adapter.ts +267 -0
- package/src/vfs/protected-fs.ts +177 -0
- package/src/vfs/read-file-tool.ts +103 -0
- package/src/vfs/write-file-tool.ts +49 -0
- package/test/harness.test.ts +30 -36
- package/test/isolate-vfs.test.ts +453 -0
- package/test/isolate.test.ts +252 -0
- package/test/state.test.ts +4 -27
- package/test/storage-engine.test.ts +250 -0
- package/test/vfs.test.ts +242 -0
- package/.turbo/turbo-lint.log +0 -6
- package/.turbo/turbo-test.log +0 -11931
- package/src/kv-store.ts +0 -216
package/src/config.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { createJiti } from "jiti";
|
|
4
|
+
import type { JsonSchema } from "@poncho-ai/sdk";
|
|
4
5
|
import type { MemoryConfig } from "./memory.js";
|
|
5
6
|
import type { McpConfig } from "./mcp.js";
|
|
6
7
|
import type { StateConfig } from "./state.js";
|
|
7
8
|
|
|
8
9
|
export interface StorageConfig {
|
|
9
|
-
provider?: "local" | "memory" | "redis" | "upstash" | "dynamodb";
|
|
10
|
+
provider?: "local" | "memory" | "sqlite" | "postgresql" | "redis" | "upstash" | "dynamodb";
|
|
10
11
|
urlEnv?: string;
|
|
11
12
|
tokenEnv?: string;
|
|
12
13
|
table?: string;
|
|
@@ -21,6 +22,10 @@ export interface StorageConfig {
|
|
|
21
22
|
enabled?: boolean;
|
|
22
23
|
maxRecallConversations?: number;
|
|
23
24
|
};
|
|
25
|
+
limits?: {
|
|
26
|
+
maxFileSize?: number;
|
|
27
|
+
maxTotalStorage?: number;
|
|
28
|
+
};
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export interface UploadsConfig {
|
|
@@ -68,6 +73,97 @@ export interface MessagingChannelConfig {
|
|
|
68
73
|
allowedUserIds?: number[];
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
export interface IsolateBinding {
|
|
77
|
+
description: string;
|
|
78
|
+
inputSchema: JsonSchema;
|
|
79
|
+
handler: (input: Record<string, unknown>) => Promise<unknown> | unknown;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Network access configuration for the bash sandbox (curl, wget).
|
|
84
|
+
* Network access is disabled by default — you must explicitly allow URLs.
|
|
85
|
+
*/
|
|
86
|
+
export interface NetworkConfig {
|
|
87
|
+
/**
|
|
88
|
+
* List of allowed URL prefixes. Each entry must be a full origin (scheme + host),
|
|
89
|
+
* optionally followed by a path prefix.
|
|
90
|
+
*
|
|
91
|
+
* Examples:
|
|
92
|
+
* - `"https://api.example.com"` — allows all paths on this origin
|
|
93
|
+
* - `"https://api.example.com/v1/"` — allows only paths starting with /v1/
|
|
94
|
+
*
|
|
95
|
+
* Entries can be plain strings or objects with header transforms for credentials brokering:
|
|
96
|
+
* ```
|
|
97
|
+
* { url: "https://api.example.com", transform: [{ headers: { "Authorization": "Bearer ..." } }] }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
allowedUrls?: (string | { url: string; transform?: { headers: Record<string, string> }[] })[];
|
|
101
|
+
/** Allowed HTTP methods. Defaults to `["GET", "HEAD"]`. */
|
|
102
|
+
allowedMethods?: ("GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS")[];
|
|
103
|
+
/** Bypass the allow-list and permit all URLs and methods. Only use in trusted environments. */
|
|
104
|
+
dangerouslyAllowAll?: boolean;
|
|
105
|
+
/** Maximum number of redirects to follow. Default: 20. */
|
|
106
|
+
maxRedirects?: number;
|
|
107
|
+
/** Request timeout in milliseconds. Default: 30000. */
|
|
108
|
+
timeoutMs?: number;
|
|
109
|
+
/** Maximum response body size in bytes. Default: 10MB. */
|
|
110
|
+
maxResponseSize?: number;
|
|
111
|
+
/** Reject URLs resolving to private/loopback IPs (SSRF protection). Default: false. */
|
|
112
|
+
denyPrivateRanges?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface BashExecutionLimits {
|
|
116
|
+
/** Maximum function call/recursion depth. Default: 100. */
|
|
117
|
+
maxCallDepth?: number;
|
|
118
|
+
/** Maximum number of commands to execute. Default: 10000. */
|
|
119
|
+
maxCommandCount?: number;
|
|
120
|
+
/** Maximum loop iterations for while/for/until. Default: 10000. */
|
|
121
|
+
maxLoopIterations?: number;
|
|
122
|
+
/** Maximum total output size (stdout + stderr) in bytes. Default: 10MB. */
|
|
123
|
+
maxOutputSize?: number;
|
|
124
|
+
/** Maximum string length in bytes. Default: 10MB. */
|
|
125
|
+
maxStringLength?: number;
|
|
126
|
+
/** Maximum array elements. Default: 100000. */
|
|
127
|
+
maxArrayElements?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface BashConfig {
|
|
131
|
+
/**
|
|
132
|
+
* Whitelist of allowed commands. When set, only these commands are available.
|
|
133
|
+
* Omit to allow all built-in commands.
|
|
134
|
+
*
|
|
135
|
+
* @example ["cat", "grep", "jq", "echo", "ls", "head", "tail", "wc", "sort"]
|
|
136
|
+
*/
|
|
137
|
+
commands?: string[];
|
|
138
|
+
/** Execution limits to prevent runaway scripts. */
|
|
139
|
+
executionLimits?: BashExecutionLimits;
|
|
140
|
+
/** Enable python3/python commands in the sandbox. Default: false. */
|
|
141
|
+
python?: boolean;
|
|
142
|
+
/** Enable js-exec/node commands via QuickJS in the sandbox. Default: false. */
|
|
143
|
+
javascript?: boolean;
|
|
144
|
+
/** Environment variables injected into every bash session. */
|
|
145
|
+
env?: Record<string, string>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface IsolateConfig {
|
|
149
|
+
/** V8 isolate memory limit in MB. Default: 128 */
|
|
150
|
+
memoryLimit?: number;
|
|
151
|
+
/** Execution timeout in ms. Default: 10000 */
|
|
152
|
+
timeLimit?: number;
|
|
153
|
+
/** Max combined stdout+stderr in bytes. Default: 65536 */
|
|
154
|
+
outputLimit?: number;
|
|
155
|
+
/** Max code input size in bytes. Default: 102400 (100KB) */
|
|
156
|
+
codeLimit?: number;
|
|
157
|
+
/** npm packages to bundle and make available via require() */
|
|
158
|
+
libraries?: string[];
|
|
159
|
+
/** External API access */
|
|
160
|
+
apis?: {
|
|
161
|
+
fetch?: { allowedDomains: string[] };
|
|
162
|
+
};
|
|
163
|
+
/** Builder-defined custom bindings injected into the isolate */
|
|
164
|
+
bindings?: Record<string, IsolateBinding>;
|
|
165
|
+
}
|
|
166
|
+
|
|
71
167
|
export interface PonchoConfig extends McpConfig {
|
|
72
168
|
harness?: string;
|
|
73
169
|
messaging?: MessagingChannelConfig[];
|
|
@@ -142,6 +238,15 @@ export interface PonchoConfig extends McpConfig {
|
|
|
142
238
|
tenantSecrets?: Record<string, string>;
|
|
143
239
|
/** Set to `false` to disable the built-in web UI (headless / API-only mode). */
|
|
144
240
|
webUi?: false;
|
|
241
|
+
/** Enable sandboxed V8 isolate code execution. */
|
|
242
|
+
isolate?: IsolateConfig;
|
|
243
|
+
/**
|
|
244
|
+
* Network access for sandboxed tools (bash curl/wget, isolate fetch).
|
|
245
|
+
* Disabled by default — you must explicitly allow URLs.
|
|
246
|
+
*/
|
|
247
|
+
network?: NetworkConfig;
|
|
248
|
+
/** Bash sandbox configuration. */
|
|
249
|
+
bash?: BashConfig;
|
|
145
250
|
/** Enable browser automation tools. Set `true` for defaults, or provide config. */
|
|
146
251
|
browser?:
|
|
147
252
|
| boolean
|
package/src/harness.ts
CHANGED
|
@@ -14,10 +14,24 @@ 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 {
|
|
34
|
+
import { ponchoDocsTool } from "./default-tools.js";
|
|
21
35
|
import {
|
|
22
36
|
createMemoryStore,
|
|
23
37
|
createMemoryTools,
|
|
@@ -612,14 +626,14 @@ function extractMediaFromToolOutput(output: unknown): {
|
|
|
612
626
|
obj.type === "file" &&
|
|
613
627
|
typeof obj.data === "string" &&
|
|
614
628
|
typeof obj.mediaType === "string" &&
|
|
615
|
-
(obj.mediaType as string).startsWith("image/")
|
|
629
|
+
((obj.mediaType as string).startsWith("image/") || obj.mediaType === "application/pdf")
|
|
616
630
|
) {
|
|
617
631
|
mediaItems.push({
|
|
618
632
|
type: "media",
|
|
619
633
|
data: obj.data as string,
|
|
620
634
|
mediaType: obj.mediaType as string,
|
|
621
635
|
});
|
|
622
|
-
return { type: "file", mediaType: obj.mediaType, filename: obj.filename ?? "
|
|
636
|
+
return { type: "file", mediaType: obj.mediaType, filename: obj.filename ?? "file", _stripped: true };
|
|
623
637
|
}
|
|
624
638
|
const out: Record<string, unknown> = {};
|
|
625
639
|
for (const [k, v] of Object.entries(obj)) out[k] = walk(v);
|
|
@@ -667,6 +681,11 @@ export class AgentHarness {
|
|
|
667
681
|
private subagentManager?: SubagentManager;
|
|
668
682
|
private readonly archivedToolResultsByConversation = new Map<string, Record<string, ArchivedToolResult>>();
|
|
669
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
|
+
|
|
670
689
|
private resolveToolAccess(toolName: string): ToolAccess {
|
|
671
690
|
const tools = this.loadedConfig?.tools;
|
|
672
691
|
if (!tools) return true;
|
|
@@ -723,23 +742,8 @@ export class AgentHarness {
|
|
|
723
742
|
}
|
|
724
743
|
|
|
725
744
|
private registerConfiguredBuiltInTools(config: PonchoConfig | undefined): void {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
this.registerIfMissing(tool);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
if (this.isToolEnabled("write_file")) {
|
|
732
|
-
this.registerIfMissing(createWriteTool(this.workingDir));
|
|
733
|
-
}
|
|
734
|
-
if (this.isToolEnabled("edit_file")) {
|
|
735
|
-
this.registerIfMissing(createEditTool(this.workingDir));
|
|
736
|
-
}
|
|
737
|
-
if (this.isToolEnabled("delete_file")) {
|
|
738
|
-
this.registerIfMissing(createDeleteTool(this.workingDir));
|
|
739
|
-
}
|
|
740
|
-
if (this.isToolEnabled("delete_directory")) {
|
|
741
|
-
this.registerIfMissing(createDeleteDirectoryTool(this.workingDir));
|
|
742
|
-
}
|
|
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.
|
|
743
747
|
for (const tool of createSearchTools()) {
|
|
744
748
|
if (this.isToolEnabled(tool.name)) {
|
|
745
749
|
this.registerIfMissing(tool);
|
|
@@ -804,6 +808,33 @@ export class AgentHarness {
|
|
|
804
808
|
});
|
|
805
809
|
}
|
|
806
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
|
+
|
|
807
838
|
private shouldEnableWriteTool(): boolean {
|
|
808
839
|
const override = process.env.PONCHO_FS_WRITE?.toLowerCase();
|
|
809
840
|
if (override === "1" || override === "true" || override === "yes") {
|
|
@@ -990,11 +1021,15 @@ export class AgentHarness {
|
|
|
990
1021
|
|
|
991
1022
|
let store = this.tenantMemoryStores.get(tenantId);
|
|
992
1023
|
if (!store) {
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
+
}
|
|
998
1033
|
this.tenantMemoryStores.set(tenantId, store);
|
|
999
1034
|
// Evict oldest entries if cache grows too large
|
|
1000
1035
|
if (this.tenantMemoryStores.size > 100) {
|
|
@@ -1385,13 +1420,55 @@ export class AgentHarness {
|
|
|
1385
1420
|
this.registerSkillTools(skillMetadata);
|
|
1386
1421
|
const agentId = this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name;
|
|
1387
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) ---
|
|
1388
1469
|
this.memoryConfig = memoryConfig ?? undefined;
|
|
1389
1470
|
if (memoryConfig?.enabled) {
|
|
1390
|
-
this.memoryStore =
|
|
1391
|
-
agentId,
|
|
1392
|
-
memoryConfig,
|
|
1393
|
-
{ workingDir: this.workingDir },
|
|
1394
|
-
);
|
|
1471
|
+
this.memoryStore = createMemoryStoreFromEngine(engine);
|
|
1395
1472
|
this.dispatcher.registerMany(
|
|
1396
1473
|
createMemoryTools(
|
|
1397
1474
|
(ctx) => this.getMemoryStore(ctx.tenantId) ?? this.memoryStore!,
|
|
@@ -1400,16 +1477,17 @@ export class AgentHarness {
|
|
|
1400
1477
|
);
|
|
1401
1478
|
}
|
|
1402
1479
|
|
|
1403
|
-
|
|
1404
|
-
this.todoStore =
|
|
1480
|
+
// --- Todos (engine-backed) ---
|
|
1481
|
+
this.todoStore = createTodoStoreFromEngine(engine);
|
|
1405
1482
|
for (const tool of createTodoTools(this.todoStore)) {
|
|
1406
1483
|
if (this.isToolEnabled(tool.name)) {
|
|
1407
1484
|
this.registerIfMissing(tool);
|
|
1408
1485
|
}
|
|
1409
1486
|
}
|
|
1410
1487
|
|
|
1488
|
+
// --- Reminders (engine-backed) ---
|
|
1411
1489
|
if (config?.reminders?.enabled) {
|
|
1412
|
-
this.reminderStore =
|
|
1490
|
+
this.reminderStore = createReminderStoreFromEngine(engine);
|
|
1413
1491
|
for (const tool of createReminderTools(this.reminderStore)) {
|
|
1414
1492
|
if (this.isToolEnabled(tool.name)) {
|
|
1415
1493
|
this.registerIfMissing(tool);
|
|
@@ -1427,6 +1505,7 @@ export class AgentHarness {
|
|
|
1427
1505
|
}
|
|
1428
1506
|
|
|
1429
1507
|
// Secrets store for per-tenant env var overrides
|
|
1508
|
+
const stateConfig = resolveStateConfig(config);
|
|
1430
1509
|
const authTokenEnv = config?.auth?.tokenEnv ?? "PONCHO_AUTH_TOKEN";
|
|
1431
1510
|
const authToken = process.env[authTokenEnv];
|
|
1432
1511
|
if (authToken) {
|
|
@@ -1487,60 +1566,22 @@ export class AgentHarness {
|
|
|
1487
1566
|
};
|
|
1488
1567
|
}
|
|
1489
1568
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
const
|
|
1494
|
-
const
|
|
1495
|
-
|
|
1496
|
-
const
|
|
1497
|
-
return {
|
|
1498
|
-
async save(json: string) {
|
|
1499
|
-
await fetch(`${baseUrl}/set/${encodeURIComponent(stateKey)}/${encodeURIComponent(json)}`, { method: "POST", headers });
|
|
1500
|
-
},
|
|
1501
|
-
async load() {
|
|
1502
|
-
const res = await fetch(`${baseUrl}/get/${encodeURIComponent(stateKey)}`, { headers });
|
|
1503
|
-
if (!res.ok) return undefined;
|
|
1504
|
-
const body = await res.json() as { result?: string | null };
|
|
1505
|
-
return body.result ?? undefined;
|
|
1506
|
-
},
|
|
1507
|
-
};
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
if (provider === "redis") {
|
|
1511
|
-
const urlEnv = config.storage?.urlEnv ?? "REDIS_URL";
|
|
1512
|
-
const url = process.env[urlEnv] ?? "";
|
|
1513
|
-
if (!url) return undefined;
|
|
1514
|
-
let clientPromise: Promise<{ get(k: string): Promise<string | null>; set(k: string, v: string): Promise<unknown> } | undefined> | undefined;
|
|
1515
|
-
const getClient = () => {
|
|
1516
|
-
if (!clientPromise) {
|
|
1517
|
-
clientPromise = (async () => {
|
|
1518
|
-
try {
|
|
1519
|
-
const mod = (await import("redis")) as unknown as {
|
|
1520
|
-
createClient: (opts: { url: string }) => {
|
|
1521
|
-
connect(): Promise<unknown>;
|
|
1522
|
-
get(k: string): Promise<string | null>;
|
|
1523
|
-
set(k: string, v: string): Promise<unknown>;
|
|
1524
|
-
};
|
|
1525
|
-
};
|
|
1526
|
-
const c = mod.createClient({ url });
|
|
1527
|
-
await c.connect();
|
|
1528
|
-
return c;
|
|
1529
|
-
} catch { return undefined; }
|
|
1530
|
-
})();
|
|
1531
|
-
}
|
|
1532
|
-
return clientPromise;
|
|
1533
|
-
};
|
|
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`);
|
|
1534
1576
|
return {
|
|
1535
1577
|
async save(json: string) {
|
|
1536
|
-
const
|
|
1537
|
-
|
|
1578
|
+
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
1579
|
+
await mkdir(stateDir, { recursive: true });
|
|
1580
|
+
await writeFile(filePath, json, "utf8");
|
|
1538
1581
|
},
|
|
1539
1582
|
async load() {
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1542
|
-
const val = await c.get(stateKey);
|
|
1543
|
-
return val ?? undefined;
|
|
1583
|
+
const { readFile } = await import("node:fs/promises");
|
|
1584
|
+
try { return await readFile(filePath, "utf8"); } catch { return undefined; }
|
|
1544
1585
|
},
|
|
1545
1586
|
};
|
|
1546
1587
|
}
|
|
@@ -1632,12 +1673,20 @@ export class AgentHarness {
|
|
|
1632
1673
|
this.otlpTracerProvider = undefined;
|
|
1633
1674
|
}
|
|
1634
1675
|
this.hasOtlpExporter = false;
|
|
1676
|
+
|
|
1677
|
+
// Cleanup bash environments and storage engine
|
|
1678
|
+
this.bashManager?.destroyAll();
|
|
1679
|
+
await this.storageEngine?.close();
|
|
1635
1680
|
}
|
|
1636
1681
|
|
|
1637
1682
|
listTools(): ToolDefinition[] {
|
|
1638
1683
|
return this.dispatcher.list();
|
|
1639
1684
|
}
|
|
1640
1685
|
|
|
1686
|
+
listSkills(): Array<{ name: string; description: string }> {
|
|
1687
|
+
return this.loadedSkills.map((s) => ({ name: s.name, description: s.description }));
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1641
1690
|
/**
|
|
1642
1691
|
* Wraps the run() generator with an OTel root span (invoke_agent) so all
|
|
1643
1692
|
* child spans (LLM calls via AI SDK, tool execution) group under one trace.
|
|
@@ -1818,11 +1867,56 @@ ${boundedMainMemory.trim()}`
|
|
|
1818
1867
|
? `\n\n## Open Tasks\n\n${openTodos.map((t) => `- [${t.status === "in_progress" ? "IN PROGRESS" : "PENDING"}] ${t.content} (id: ${t.id})`).join("\n")}`
|
|
1819
1868
|
: "";
|
|
1820
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
|
+
|
|
1821
1915
|
const buildSystemPrompt = (): string => {
|
|
1822
1916
|
const agentPrompt = renderCurrentAgentPrompt();
|
|
1823
1917
|
const promptWithSkills = this.skillContextWindow
|
|
1824
|
-
? `${agentPrompt}${developmentContext}\n\n${this.skillContextWindow}${browserContext}`
|
|
1825
|
-
: `${agentPrompt}${developmentContext}${browserContext}`;
|
|
1918
|
+
? `${agentPrompt}${developmentContext}\n\n${this.skillContextWindow}${browserContext}${fsContext}${isolateContext}`
|
|
1919
|
+
: `${agentPrompt}${developmentContext}${browserContext}${fsContext}${isolateContext}`;
|
|
1826
1920
|
const timeContext = this.reminderStore
|
|
1827
1921
|
? `\n\nCurrent UTC time: ${new Date().toISOString()}`
|
|
1828
1922
|
: "";
|
|
@@ -2025,9 +2119,37 @@ ${boundedMainMemory.trim()}`
|
|
|
2025
2119
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2026
2120
|
const rich = (meta as any)?._richToolResults as unknown[] | undefined;
|
|
2027
2121
|
if (rich && rich.length > 0) {
|
|
2028
|
-
//
|
|
2029
|
-
//
|
|
2030
|
-
//
|
|
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
|
+
}
|
|
2031
2153
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2032
2154
|
return [{ role: "tool" as const, content: rich as any }];
|
|
2033
2155
|
}
|
|
@@ -2181,7 +2303,11 @@ ${boundedMainMemory.trim()}`
|
|
|
2181
2303
|
if (!isSupportedImage && !isSupportedFile && isTextBasedMime(part.mediaType)) {
|
|
2182
2304
|
let textContent: string;
|
|
2183
2305
|
try {
|
|
2184
|
-
if (part.data.startsWith(
|
|
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) {
|
|
2185
2311
|
const buf = await this.uploadStore.get(part.data);
|
|
2186
2312
|
textContent = buf.toString("utf8");
|
|
2187
2313
|
} else if (part.data.startsWith("https://") || part.data.startsWith("http://")) {
|
|
@@ -2210,7 +2336,11 @@ ${boundedMainMemory.trim()}`
|
|
|
2210
2336
|
// fetch URLs itself (which fails for private blob stores).
|
|
2211
2337
|
let resolvedData: string;
|
|
2212
2338
|
try {
|
|
2213
|
-
if (part.data.startsWith(
|
|
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) {
|
|
2214
2344
|
const buf = await this.uploadStore.get(part.data);
|
|
2215
2345
|
resolvedData = buf.toString("base64");
|
|
2216
2346
|
} else if (part.data.startsWith("https://") || part.data.startsWith("http://")) {
|
|
@@ -2649,6 +2779,9 @@ ${boundedMainMemory.trim()}`
|
|
|
2649
2779
|
abortSignal: input.abortSignal,
|
|
2650
2780
|
conversationId: input.conversationId,
|
|
2651
2781
|
tenantId: input.tenantId,
|
|
2782
|
+
vfs: this.bashManager
|
|
2783
|
+
? this.createVfsAccess(input.tenantId ?? "__default__")
|
|
2784
|
+
: undefined,
|
|
2652
2785
|
};
|
|
2653
2786
|
|
|
2654
2787
|
const toolResultsForModel: Array<{
|
|
@@ -2841,6 +2974,7 @@ ${boundedMainMemory.trim()}`
|
|
|
2841
2974
|
return;
|
|
2842
2975
|
}
|
|
2843
2976
|
|
|
2977
|
+
const callInputMap = new Map(approvedCalls.map((c) => [c.id, c.input]));
|
|
2844
2978
|
for (const result of batchResults) {
|
|
2845
2979
|
const span = toolSpans.get(result.callId);
|
|
2846
2980
|
if (result.error) {
|
|
@@ -2894,6 +3028,7 @@ ${boundedMainMemory.trim()}`
|
|
|
2894
3028
|
yield pushEvent({
|
|
2895
3029
|
type: "tool:completed",
|
|
2896
3030
|
tool: result.tool,
|
|
3031
|
+
input: callInputMap.get(result.callId),
|
|
2897
3032
|
output: result.output,
|
|
2898
3033
|
duration: now() - batchStart,
|
|
2899
3034
|
outputTokenEstimate,
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,11 @@ export * from "./state.js";
|
|
|
18
18
|
export * from "./upload-store.js";
|
|
19
19
|
export * from "./telemetry.js";
|
|
20
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";
|
|
21
26
|
export * from "./tenant-token.js";
|
|
22
27
|
export * from "./tool-dispatcher.js";
|
|
23
28
|
export * from "./subagent-manager.js";
|