@poncho-ai/harness 0.43.1 → 0.44.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.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +75 -0
- package/dist/index.d.ts +96 -45
- package/dist/index.js +365 -97
- package/package.json +1 -1
- package/src/harness.ts +32 -2
- package/src/orchestrator/run-conversation-turn.ts +2 -2
- package/src/storage/schema.ts +19 -0
- package/src/storage/sql-dialect.ts +5 -2
- package/src/upload-store.ts +27 -0
- package/src/vfs/bash-manager.ts +6 -3
- package/src/vfs/poncho-fs-adapter.ts +333 -51
- package/test/upload-store-decode.test.ts +43 -0
- package/test/vfs.test.ts +111 -0
package/package.json
CHANGED
package/src/harness.ts
CHANGED
|
@@ -20,7 +20,7 @@ const costLog = createLogger("cost");
|
|
|
20
20
|
const mcpLog = createLogger("mcp");
|
|
21
21
|
const modelLog = createLogger("model");
|
|
22
22
|
import type { UploadStore } from "./upload-store.js";
|
|
23
|
-
import { PONCHO_UPLOAD_SCHEME, VFS_SCHEME, deriveUploadKey } from "./upload-store.js";
|
|
23
|
+
import { PONCHO_UPLOAD_SCHEME, VFS_SCHEME, decodeFileInputData, deriveUploadKey } from "./upload-store.js";
|
|
24
24
|
import type { StorageEngine } from "./storage/engine.js";
|
|
25
25
|
import { createStorageEngine, type StorageProvider } from "./storage/index.js";
|
|
26
26
|
import {
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
createReminderStoreFromEngine,
|
|
31
31
|
} from "./storage/store-adapters.js";
|
|
32
32
|
import { BashEnvironmentManager } from "./vfs/bash-manager.js";
|
|
33
|
+
import type { VirtualMount } from "./vfs/poncho-fs-adapter.js";
|
|
34
|
+
export type { VirtualMount } from "./vfs/poncho-fs-adapter.js";
|
|
33
35
|
import { createBashTool } from "./vfs/bash-tool.js";
|
|
34
36
|
import { createReadFileTool } from "./vfs/read-file-tool.js";
|
|
35
37
|
import { createEditFileTool } from "./vfs/edit-file-tool.js";
|
|
@@ -123,6 +125,15 @@ export interface HarnessOptions {
|
|
|
123
125
|
* `resolveStateConfig`, etc.) run as today regardless of source.
|
|
124
126
|
*/
|
|
125
127
|
config?: PonchoConfig;
|
|
128
|
+
/**
|
|
129
|
+
* Read-only virtual mounts overlaid on the VFS. Each mount maps a VFS
|
|
130
|
+
* prefix (e.g. "/system/") to a local filesystem directory; reads under
|
|
131
|
+
* the prefix are served from local disk, writes are rejected. Used by
|
|
132
|
+
* platforms like PonchOS to expose deployment-shipped defaults (system
|
|
133
|
+
* jobs, system skills) without storing them in each tenant's VFS.
|
|
134
|
+
* Empty by default — no system mounts in the CLI / dev workflow.
|
|
135
|
+
*/
|
|
136
|
+
virtualMounts?: VirtualMount[];
|
|
126
137
|
}
|
|
127
138
|
|
|
128
139
|
export interface HarnessRunOutput {
|
|
@@ -855,6 +866,8 @@ export class AgentHarness {
|
|
|
855
866
|
storageEngine?: StorageEngine;
|
|
856
867
|
/** Bash environment manager (creates per-tenant bash instances). */
|
|
857
868
|
private bashManager?: BashEnvironmentManager;
|
|
869
|
+
/** Read-only virtual mounts overlaid on the VFS. Empty by default. */
|
|
870
|
+
private virtualMounts: VirtualMount[] = [];
|
|
858
871
|
|
|
859
872
|
private resolveToolAccess(toolName: string): ToolAccess {
|
|
860
873
|
const tools = this.loadedConfig?.tools;
|
|
@@ -1048,6 +1061,7 @@ export class AgentHarness {
|
|
|
1048
1061
|
this.storageEngine = options.storageEngine;
|
|
1049
1062
|
this.injectedStorageEngine = true;
|
|
1050
1063
|
}
|
|
1064
|
+
this.virtualMounts = options.virtualMounts ?? [];
|
|
1051
1065
|
|
|
1052
1066
|
if (options.toolDefinitions?.length) {
|
|
1053
1067
|
this.dispatcher.registerMany(options.toolDefinitions);
|
|
@@ -1255,6 +1269,21 @@ export class AgentHarness {
|
|
|
1255
1269
|
// "__default__" so dev-mode (no auth) conversations see the same VFS
|
|
1256
1270
|
// namespace the Files sidebar writes to.
|
|
1257
1271
|
const effectiveTenant = tenantId || "__default__";
|
|
1272
|
+
// Refresh the engine's path cache before fingerprinting. The cache is
|
|
1273
|
+
// the only thing `computeVfsSkillFingerprint` reads, and historically
|
|
1274
|
+
// it was only populated by `bash-manager.refreshPathCache` — chat-only
|
|
1275
|
+
// flows (no bash) left it empty, the patched writeFile's incremental
|
|
1276
|
+
// update became a no-op (it skips when the cache isn't loaded), the
|
|
1277
|
+
// fingerprint stuck at "" across runs, and any skill the agent (or a
|
|
1278
|
+
// client like PonchOS's iOS Files browser) authored after the harness
|
|
1279
|
+
// was first instantiated was invisible from the next turn onward.
|
|
1280
|
+
// One SELECT per turn is the cost of correctness here.
|
|
1281
|
+
const engineWithRefresh = this.storageEngine as unknown as {
|
|
1282
|
+
refreshPathCache?: (tenantId: string) => Promise<void>;
|
|
1283
|
+
};
|
|
1284
|
+
if (typeof engineWithRefresh.refreshPathCache === "function") {
|
|
1285
|
+
await engineWithRefresh.refreshPathCache(effectiveTenant);
|
|
1286
|
+
}
|
|
1258
1287
|
const fingerprint = this.computeVfsSkillFingerprint(effectiveTenant);
|
|
1259
1288
|
const cached = this.skillCache.get(effectiveTenant);
|
|
1260
1289
|
if (cached && cached.fingerprint === fingerprint) {
|
|
@@ -1695,6 +1724,7 @@ export class AgentHarness {
|
|
|
1695
1724
|
bashWorkingDir,
|
|
1696
1725
|
config?.bash,
|
|
1697
1726
|
config?.network,
|
|
1727
|
+
this.virtualMounts,
|
|
1698
1728
|
);
|
|
1699
1729
|
// Register VFS tools
|
|
1700
1730
|
this.registerIfMissing(createBashTool(this.bashManager));
|
|
@@ -2256,7 +2286,7 @@ Code is wrapped in an async IIFE — use \`return\` to return a value to the too
|
|
|
2256
2286
|
];
|
|
2257
2287
|
for (const file of input.files) {
|
|
2258
2288
|
if (this.uploadStore) {
|
|
2259
|
-
const buf =
|
|
2289
|
+
const buf = await decodeFileInputData(file.data);
|
|
2260
2290
|
const key = deriveUploadKey(buf, file.mediaType);
|
|
2261
2291
|
const ref = await this.uploadStore.put(key, buf, file.mediaType);
|
|
2262
2292
|
parts.push({
|
|
@@ -24,7 +24,7 @@ import type { AgentEvent, FileInput, Message } from "@poncho-ai/sdk";
|
|
|
24
24
|
import { createLogger } from "@poncho-ai/sdk";
|
|
25
25
|
import type { AgentHarness } from "../harness.js";
|
|
26
26
|
import type { ConversationStore } from "../state.js";
|
|
27
|
-
import { deriveUploadKey } from "../upload-store.js";
|
|
27
|
+
import { decodeFileInputData, deriveUploadKey } from "../upload-store.js";
|
|
28
28
|
import { withToolResultArchiveParam } from "./continuation.js";
|
|
29
29
|
import { resolveRunRequest } from "./history.js";
|
|
30
30
|
import {
|
|
@@ -104,7 +104,7 @@ export const runConversationTurn = async (
|
|
|
104
104
|
if (opts.files && opts.files.length > 0 && opts.harness.uploadStore) {
|
|
105
105
|
const uploadedParts = await Promise.all(
|
|
106
106
|
opts.files.map(async (f) => {
|
|
107
|
-
const buf =
|
|
107
|
+
const buf = await decodeFileInputData(f.data);
|
|
108
108
|
const key = deriveUploadKey(buf, f.mediaType);
|
|
109
109
|
const ref = await opts.harness.uploadStore!.put(key, buf, f.mediaType);
|
|
110
110
|
return {
|
package/src/storage/schema.ts
CHANGED
|
@@ -195,4 +195,23 @@ export const migrations: Migration[] = [
|
|
|
195
195
|
WHERE parent_message_id IS NOT NULL`,
|
|
196
196
|
],
|
|
197
197
|
},
|
|
198
|
+
{
|
|
199
|
+
version: 7,
|
|
200
|
+
name: "fix_reminder_scheduled_at_precision",
|
|
201
|
+
// Postgres maps `REAL` to float4 (4 bytes, ~7 digit precision),
|
|
202
|
+
// which silently rounds millisecond epochs (13 digits) — every
|
|
203
|
+
// reminder write+read on Postgres returned a different value than
|
|
204
|
+
// it stored. SQLite's REAL is float8 (15+ digit precision) so it
|
|
205
|
+
// was always fine there.
|
|
206
|
+
//
|
|
207
|
+
// Convert the Postgres column to BIGINT so future writes are exact
|
|
208
|
+
// (already-stored rounded values aren't recoverable but they were
|
|
209
|
+
// never correct in the first place).
|
|
210
|
+
up: (d) => {
|
|
211
|
+
if (d === "sqlite") return [];
|
|
212
|
+
return [
|
|
213
|
+
`ALTER TABLE reminders ALTER COLUMN scheduled_at TYPE BIGINT USING scheduled_at::bigint`,
|
|
214
|
+
];
|
|
215
|
+
},
|
|
216
|
+
},
|
|
198
217
|
];
|
|
@@ -1195,7 +1195,10 @@ export abstract class SqlStorageEngine implements StorageEngine {
|
|
|
1195
1195
|
return {
|
|
1196
1196
|
id: row.id as string,
|
|
1197
1197
|
task: row.task as string,
|
|
1198
|
-
|
|
1198
|
+
// Postgres-js returns BIGINT columns as strings to avoid silent
|
|
1199
|
+
// precision loss in JS. Coerce to Number — ms epochs max out at
|
|
1200
|
+
// ~10^16 in year 2286, well under Number.MAX_SAFE_INTEGER (2^53).
|
|
1201
|
+
scheduledAt: Number(row.scheduled_at),
|
|
1199
1202
|
timezone: (row.timezone as string) ?? undefined,
|
|
1200
1203
|
status: row.status as Reminder["status"],
|
|
1201
1204
|
createdAt: new Date(row.created_at as string).getTime(),
|
|
@@ -1203,7 +1206,7 @@ export abstract class SqlStorageEngine implements StorageEngine {
|
|
|
1203
1206
|
ownerId: (row.owner_id as string) ?? undefined,
|
|
1204
1207
|
tenantId: tid === DEFAULT_TENANT ? null : tid,
|
|
1205
1208
|
recurrence,
|
|
1206
|
-
occurrenceCount: (row.occurrence_count
|
|
1209
|
+
occurrenceCount: Number(row.occurrence_count ?? 0),
|
|
1207
1210
|
};
|
|
1208
1211
|
}
|
|
1209
1212
|
|
package/src/upload-store.ts
CHANGED
|
@@ -108,6 +108,33 @@ export const deriveUploadKey = (
|
|
|
108
108
|
return `${hash}${ext}`;
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
+
const DATA_URI_PREFIX = /^data:[^,]*?;base64,/;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Decode the `FileInput.data` field per its documented contract: it may be a
|
|
115
|
+
* raw base64 string, a `data:<mime>;base64,<…>` URI, or an `https?://` URL.
|
|
116
|
+
* Returns the decoded bytes. Older versions of the harness called
|
|
117
|
+
* `Buffer.from(data, "base64")` unconditionally; that silently produced
|
|
118
|
+
* garbage bytes for data URIs (the `:` / `;` / `,` in the prefix are not
|
|
119
|
+
* valid base64 chars but Node's decoder ignores them rather than throwing).
|
|
120
|
+
*/
|
|
121
|
+
export const decodeFileInputData = async (data: string): Promise<Buffer> => {
|
|
122
|
+
if (data.startsWith("http://") || data.startsWith("https://")) {
|
|
123
|
+
const resp = await fetch(data);
|
|
124
|
+
if (!resp.ok) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`uploads: failed to fetch file at ${data}: ${resp.status} ${resp.statusText}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return Buffer.from(await resp.arrayBuffer());
|
|
130
|
+
}
|
|
131
|
+
const match = data.match(DATA_URI_PREFIX);
|
|
132
|
+
if (match) {
|
|
133
|
+
return Buffer.from(data.slice(match[0].length), "base64");
|
|
134
|
+
}
|
|
135
|
+
return Buffer.from(data, "base64");
|
|
136
|
+
};
|
|
137
|
+
|
|
111
138
|
const MIME_EXT_MAP: Record<string, string> = {
|
|
112
139
|
"image/jpeg": ".jpg",
|
|
113
140
|
"image/png": ".png",
|
package/src/vfs/bash-manager.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
} from "just-bash";
|
|
12
12
|
import type { StorageEngine } from "../storage/engine.js";
|
|
13
13
|
import type { BashConfig, NetworkConfig } from "../config.js";
|
|
14
|
-
import { PonchoFsAdapter } from "./poncho-fs-adapter.js";
|
|
14
|
+
import { PonchoFsAdapter, type VirtualMount } from "./poncho-fs-adapter.js";
|
|
15
15
|
import { createBashFs } from "./create-bash-fs.js";
|
|
16
16
|
import type { PostgresEngine } from "../storage/postgres-engine.js";
|
|
17
17
|
|
|
@@ -69,6 +69,7 @@ export class BashEnvironmentManager {
|
|
|
69
69
|
private filesystems = new Map<string, IFileSystem>();
|
|
70
70
|
private readonly workingDir: string | null;
|
|
71
71
|
private readonly bashOptions: Partial<BashOptions>;
|
|
72
|
+
private readonly virtualMounts: VirtualMount[];
|
|
72
73
|
|
|
73
74
|
constructor(
|
|
74
75
|
private engine: StorageEngine,
|
|
@@ -76,16 +77,18 @@ export class BashEnvironmentManager {
|
|
|
76
77
|
workingDir: string | null,
|
|
77
78
|
bashConfig?: BashConfig,
|
|
78
79
|
network?: NetworkConfig,
|
|
80
|
+
virtualMounts: VirtualMount[] = [],
|
|
79
81
|
) {
|
|
80
82
|
this.workingDir = workingDir;
|
|
81
83
|
this.bashOptions = toBashOptions(bashConfig, network);
|
|
84
|
+
this.virtualMounts = virtualMounts;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
/** Return the combined IFileSystem (VFS + optional /project mount) for a tenant. */
|
|
85
88
|
getFs(tenantId: string): IFileSystem {
|
|
86
89
|
let fs = this.filesystems.get(tenantId);
|
|
87
90
|
if (!fs) {
|
|
88
|
-
const adapter = new PonchoFsAdapter(this.engine, tenantId, this.limits);
|
|
91
|
+
const adapter = new PonchoFsAdapter(this.engine, tenantId, this.limits, this.virtualMounts);
|
|
89
92
|
fs = createBashFs(adapter, this.workingDir);
|
|
90
93
|
this.filesystems.set(tenantId, fs);
|
|
91
94
|
}
|
|
@@ -107,7 +110,7 @@ export class BashEnvironmentManager {
|
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
getAdapter(tenantId: string): PonchoFsAdapter {
|
|
110
|
-
return new PonchoFsAdapter(this.engine, tenantId, this.limits);
|
|
113
|
+
return new PonchoFsAdapter(this.engine, tenantId, this.limits, this.virtualMounts);
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
/** Refresh the PostgreSQL path cache before a bash.exec() call. */
|