@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
@@ -1,13 +1,4 @@
1
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
- import { dirname, resolve } from "node:path";
3
1
  import type { StateConfig } from "./state.js";
4
- import {
5
- ensureAgentIdentity,
6
- getAgentStoreDirectory,
7
- slugifyStorageComponent,
8
- STORAGE_SCHEMA_VERSION,
9
- } from "./agent-identity.js";
10
- import { createRawKVStore, type RawKVStore } from "./kv-store.js";
11
2
 
12
3
  // ---------------------------------------------------------------------------
13
4
  // Data model
@@ -24,6 +15,7 @@ export interface Reminder {
24
15
  createdAt: number;
25
16
  conversationId: string;
26
17
  ownerId?: string;
18
+ tenantId?: string | null;
27
19
  }
28
20
 
29
21
  export interface ReminderStore {
@@ -34,6 +26,7 @@ export interface ReminderStore {
34
26
  timezone?: string;
35
27
  conversationId: string;
36
28
  ownerId?: string;
29
+ tenantId?: string | null;
37
30
  }): Promise<Reminder>;
38
31
  cancel(id: string): Promise<Reminder>;
39
32
  delete(id: string): Promise<void>;
@@ -43,29 +36,8 @@ export interface ReminderStore {
43
36
  // Helpers
44
37
  // ---------------------------------------------------------------------------
45
38
 
46
- const REMINDERS_FILE = "reminders.json";
47
39
  const STALE_CANCELLED_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
48
40
 
49
- const writeJsonAtomic = async (filePath: string, payload: unknown): Promise<void> => {
50
- await mkdir(dirname(filePath), { recursive: true });
51
- const tmpPath = `${filePath}.tmp`;
52
- await writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf8");
53
- await rename(tmpPath, filePath);
54
- };
55
-
56
- const isValidReminder = (item: unknown): item is Reminder =>
57
- typeof item === "object" &&
58
- item !== null &&
59
- typeof (item as Record<string, unknown>).id === "string" &&
60
- typeof (item as Record<string, unknown>).task === "string" &&
61
- typeof (item as Record<string, unknown>).scheduledAt === "number" &&
62
- typeof (item as Record<string, unknown>).status === "string";
63
-
64
- const parseReminderList = (raw: unknown): Reminder[] => {
65
- if (!Array.isArray(raw)) return [];
66
- return raw.filter(isValidReminder);
67
- };
68
-
69
41
  /** Remove all fired reminders and cancelled reminders older than 7 days. */
70
42
  const pruneStale = (reminders: Reminder[]): Reminder[] => {
71
43
  const cutoff = Date.now() - STALE_CANCELLED_MS;
@@ -97,6 +69,7 @@ class InMemoryReminderStore implements ReminderStore {
97
69
  timezone?: string;
98
70
  conversationId: string;
99
71
  ownerId?: string;
72
+ tenantId?: string | null;
100
73
  }): Promise<Reminder> {
101
74
  const reminder: Reminder = {
102
75
  id: generateId(),
@@ -107,6 +80,7 @@ class InMemoryReminderStore implements ReminderStore {
107
80
  createdAt: Date.now(),
108
81
  conversationId: input.conversationId,
109
82
  ownerId: input.ownerId,
83
+ tenantId: input.tenantId,
110
84
  };
111
85
  this.reminders = pruneStale(this.reminders);
112
86
  this.reminders.push(reminder);
@@ -128,216 +102,14 @@ class InMemoryReminderStore implements ReminderStore {
128
102
  }
129
103
  }
130
104
 
131
- // ---------------------------------------------------------------------------
132
- // FileReminderStore — single JSON file for all reminders
133
- // ---------------------------------------------------------------------------
134
-
135
- class FileReminderStore implements ReminderStore {
136
- private readonly workingDir: string;
137
- private filePath = "";
138
-
139
- constructor(workingDir: string) {
140
- this.workingDir = workingDir;
141
- }
142
-
143
- private async ensureFilePath(): Promise<string> {
144
- if (this.filePath) return this.filePath;
145
- const identity = await ensureAgentIdentity(this.workingDir);
146
- this.filePath = resolve(getAgentStoreDirectory(identity), REMINDERS_FILE);
147
- return this.filePath;
148
- }
149
-
150
- private async readAll(): Promise<Reminder[]> {
151
- try {
152
- const fp = await this.ensureFilePath();
153
- const raw = await readFile(fp, "utf8");
154
- return parseReminderList(JSON.parse(raw));
155
- } catch {
156
- return [];
157
- }
158
- }
159
-
160
- private async writeAll(reminders: Reminder[]): Promise<void> {
161
- const fp = await this.ensureFilePath();
162
- await writeJsonAtomic(fp, reminders);
163
- }
164
-
165
- async list(): Promise<Reminder[]> {
166
- const all = await this.readAll();
167
- const pruned = pruneStale(all);
168
- if (pruned.length !== all.length) await this.writeAll(pruned);
169
- return pruned;
170
- }
171
-
172
- async create(input: {
173
- task: string;
174
- scheduledAt: number;
175
- timezone?: string;
176
- conversationId: string;
177
- ownerId?: string;
178
- }): Promise<Reminder> {
179
- const reminder: Reminder = {
180
- id: generateId(),
181
- task: input.task,
182
- scheduledAt: input.scheduledAt,
183
- timezone: input.timezone,
184
- status: "pending",
185
- createdAt: Date.now(),
186
- conversationId: input.conversationId,
187
- ownerId: input.ownerId,
188
- };
189
- let reminders = await this.readAll();
190
- reminders = pruneStale(reminders);
191
- reminders.push(reminder);
192
- await this.writeAll(reminders);
193
- return reminder;
194
- }
195
-
196
- async cancel(id: string): Promise<Reminder> {
197
- const reminders = await this.readAll();
198
- const reminder = reminders.find((r) => r.id === id);
199
- if (!reminder) throw new Error(`Reminder "${id}" not found`);
200
- if (reminder.status !== "pending") {
201
- throw new Error(`Reminder "${id}" is already ${reminder.status}`);
202
- }
203
- reminder.status = "cancelled";
204
- await this.writeAll(reminders);
205
- return reminder;
206
- }
207
-
208
- async delete(id: string): Promise<void> {
209
- const reminders = await this.readAll();
210
- await this.writeAll(reminders.filter((r) => r.id !== id));
211
- }
212
- }
213
-
214
- // ---------------------------------------------------------------------------
215
- // KVBackedReminderStore — wraps any RawKVStore (Upstash, Redis, DynamoDB)
216
- // ---------------------------------------------------------------------------
217
-
218
- class KVBackedReminderStore implements ReminderStore {
219
- private readonly kv: RawKVStore;
220
- private readonly key: string;
221
- private readonly ttl?: number;
222
- private readonly memoryFallback = new InMemoryReminderStore();
223
-
224
- constructor(kv: RawKVStore, key: string, ttl?: number) {
225
- this.kv = kv;
226
- this.key = key;
227
- this.ttl = ttl;
228
- }
229
-
230
- private async readAll(): Promise<Reminder[]> {
231
- try {
232
- const raw = await this.kv.get(this.key);
233
- if (!raw) return [];
234
- return parseReminderList(JSON.parse(raw));
235
- } catch {
236
- return this.memoryFallback.list();
237
- }
238
- }
239
-
240
- private async writeAll(reminders: Reminder[]): Promise<void> {
241
- try {
242
- const serialized = JSON.stringify(reminders);
243
- if (typeof this.ttl === "number") {
244
- await this.kv.setWithTtl(this.key, serialized, Math.max(1, this.ttl));
245
- } else {
246
- await this.kv.set(this.key, serialized);
247
- }
248
- } catch {
249
- // KV write failed; operations already applied in-memory via caller
250
- }
251
- }
252
-
253
- async list(): Promise<Reminder[]> {
254
- const all = await this.readAll();
255
- const pruned = pruneStale(all);
256
- if (pruned.length !== all.length) await this.writeAll(pruned);
257
- return pruned;
258
- }
259
-
260
- async create(input: {
261
- task: string;
262
- scheduledAt: number;
263
- timezone?: string;
264
- conversationId: string;
265
- ownerId?: string;
266
- }): Promise<Reminder> {
267
- let reminders: Reminder[];
268
- try {
269
- reminders = await this.readAll();
270
- } catch {
271
- return this.memoryFallback.create(input);
272
- }
273
- const reminder: Reminder = {
274
- id: generateId(),
275
- task: input.task,
276
- scheduledAt: input.scheduledAt,
277
- timezone: input.timezone,
278
- status: "pending",
279
- createdAt: Date.now(),
280
- conversationId: input.conversationId,
281
- ownerId: input.ownerId,
282
- };
283
- reminders = pruneStale(reminders);
284
- reminders.push(reminder);
285
- await this.writeAll(reminders);
286
- return reminder;
287
- }
288
-
289
- async cancel(id: string): Promise<Reminder> {
290
- let reminders: Reminder[];
291
- try {
292
- reminders = await this.readAll();
293
- } catch {
294
- return this.memoryFallback.cancel(id);
295
- }
296
- const reminder = reminders.find((r) => r.id === id);
297
- if (!reminder) throw new Error(`Reminder "${id}" not found`);
298
- if (reminder.status !== "pending") {
299
- throw new Error(`Reminder "${id}" is already ${reminder.status}`);
300
- }
301
- reminder.status = "cancelled";
302
- await this.writeAll(reminders);
303
- return reminder;
304
- }
305
-
306
- async delete(id: string): Promise<void> {
307
- let reminders: Reminder[];
308
- try {
309
- reminders = await this.readAll();
310
- } catch {
311
- return this.memoryFallback.delete(id);
312
- }
313
- await this.writeAll(reminders.filter((r) => r.id !== id));
314
- }
315
- }
316
-
317
105
  // ---------------------------------------------------------------------------
318
106
  // Factory
319
107
  // ---------------------------------------------------------------------------
320
108
 
321
109
  export const createReminderStore = (
322
- agentId: string,
323
- config?: StateConfig,
324
- options?: { workingDir?: string },
110
+ _agentId: string,
111
+ _config?: StateConfig,
112
+ _options?: { workingDir?: string },
325
113
  ): ReminderStore => {
326
- const provider = config?.provider ?? "local";
327
- const ttl = config?.ttl;
328
- const workingDir = options?.workingDir ?? process.cwd();
329
-
330
- if (provider === "local") {
331
- return new FileReminderStore(workingDir);
332
- }
333
- if (provider === "memory") {
334
- return new InMemoryReminderStore();
335
- }
336
-
337
- const kv = createRawKVStore(config);
338
- if (kv) {
339
- const key = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}:reminders`;
340
- return new KVBackedReminderStore(kv, key, ttl);
341
- }
342
114
  return new InMemoryReminderStore();
343
115
  };
@@ -84,6 +84,7 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
84
84
  scheduledAt,
85
85
  timezone,
86
86
  conversationId,
87
+ tenantId: context.tenantId,
87
88
  });
88
89
 
89
90
  return {
@@ -116,8 +117,12 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
116
117
  },
117
118
  additionalProperties: false,
118
119
  },
119
- handler: async (input) => {
120
+ handler: async (input, context) => {
120
121
  let reminders = await store.list();
122
+ // Tenant-scoped: only show reminders belonging to the current tenant
123
+ if (context.tenantId) {
124
+ reminders = reminders.filter((r) => r.tenantId === context.tenantId);
125
+ }
121
126
  const status = typeof input.status === "string" ? input.status : undefined;
122
127
  if (status && VALID_STATUSES.includes(status as ReminderStatus)) {
123
128
  reminders = reminders.filter((r) => r.status === status);
@@ -150,9 +155,17 @@ export const createReminderTools = (store: ReminderStore): ToolDefinition[] => [
150
155
  required: ["id"],
151
156
  additionalProperties: false,
152
157
  },
153
- handler: async (input) => {
158
+ handler: async (input, context) => {
154
159
  const id = typeof input.id === "string" ? input.id.trim() : "";
155
160
  if (!id) throw new Error("id is required");
161
+ // Validate tenant ownership before cancelling
162
+ if (context.tenantId) {
163
+ const all = await store.list();
164
+ const target = all.find((r) => r.id === id);
165
+ if (target && target.tenantId !== context.tenantId) {
166
+ throw new Error("Reminder not found");
167
+ }
168
+ }
156
169
  const cancelled = await store.cancel(id);
157
170
  return {
158
171
  ok: true,
@@ -0,0 +1,163 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, createHash } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ import {
5
+ ensureAgentIdentity,
6
+ getAgentStoreDirectory,
7
+ slugifyStorageComponent,
8
+ } from "./agent-identity.js";
9
+ import type { StateConfig } from "./state.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Interface
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface SecretsStore {
16
+ get(tenantId: string): Promise<Record<string, string>>;
17
+ set(tenantId: string, key: string, value: string): Promise<void>;
18
+ delete(tenantId: string, key: string): Promise<void>;
19
+ list(tenantId: string): Promise<string[]>;
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Encryption helpers (AES-256-GCM)
24
+ // ---------------------------------------------------------------------------
25
+
26
+ type EncryptedBlob = { iv: string; ct: string; tag: string };
27
+
28
+ function deriveKey(signingKey: string): Buffer {
29
+ // HKDF-like derivation: SHA-256 of fixed salt + signing key
30
+ return createHash("sha256")
31
+ .update("poncho-secrets-v1:" + signingKey)
32
+ .digest();
33
+ }
34
+
35
+ function encrypt(plaintext: string, key: Buffer): EncryptedBlob {
36
+ const iv = randomBytes(12);
37
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
38
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
39
+ const tag = cipher.getAuthTag();
40
+ return {
41
+ iv: iv.toString("base64"),
42
+ ct: ct.toString("base64"),
43
+ tag: tag.toString("base64"),
44
+ };
45
+ }
46
+
47
+ function decrypt(blob: EncryptedBlob, key: Buffer): string {
48
+ const decipher = createDecipheriv(
49
+ "aes-256-gcm",
50
+ key,
51
+ Buffer.from(blob.iv, "base64"),
52
+ );
53
+ decipher.setAuthTag(Buffer.from(blob.tag, "base64"));
54
+ return Buffer.concat([
55
+ decipher.update(Buffer.from(blob.ct, "base64")),
56
+ decipher.final(),
57
+ ]).toString("utf8");
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // File-based implementation
62
+ // ---------------------------------------------------------------------------
63
+
64
+ class FileSecretsStore implements SecretsStore {
65
+ private readonly workingDir: string;
66
+ private readonly encKey: Buffer;
67
+
68
+ constructor(workingDir: string, signingKey: string) {
69
+ this.workingDir = workingDir;
70
+ this.encKey = deriveKey(signingKey);
71
+ }
72
+
73
+ private async filePath(tenantId: string): Promise<string> {
74
+ const identity = await ensureAgentIdentity(this.workingDir);
75
+ const dir = resolve(
76
+ getAgentStoreDirectory(identity),
77
+ "tenants",
78
+ slugifyStorageComponent(tenantId),
79
+ );
80
+ return resolve(dir, "secrets.json");
81
+ }
82
+
83
+ private async readAll(tenantId: string): Promise<Record<string, EncryptedBlob>> {
84
+ try {
85
+ const raw = await readFile(await this.filePath(tenantId), "utf8");
86
+ return JSON.parse(raw) as Record<string, EncryptedBlob>;
87
+ } catch {
88
+ return {};
89
+ }
90
+ }
91
+
92
+ private async writeAll(
93
+ tenantId: string,
94
+ data: Record<string, EncryptedBlob>,
95
+ ): Promise<void> {
96
+ const fp = await this.filePath(tenantId);
97
+ await mkdir(dirname(fp), { recursive: true });
98
+ await writeFile(fp, JSON.stringify(data, null, 2), "utf8");
99
+ }
100
+
101
+ async get(tenantId: string): Promise<Record<string, string>> {
102
+ const data = await this.readAll(tenantId);
103
+ const result: Record<string, string> = {};
104
+ for (const [k, blob] of Object.entries(data)) {
105
+ try {
106
+ result[k] = decrypt(blob, this.encKey);
107
+ } catch {
108
+ // Skip entries that can't be decrypted (key rotation)
109
+ }
110
+ }
111
+ return result;
112
+ }
113
+
114
+ async set(tenantId: string, key: string, value: string): Promise<void> {
115
+ const data = await this.readAll(tenantId);
116
+ data[key] = encrypt(value, this.encKey);
117
+ await this.writeAll(tenantId, data);
118
+ }
119
+
120
+ async delete(tenantId: string, key: string): Promise<void> {
121
+ const data = await this.readAll(tenantId);
122
+ delete data[key];
123
+ await this.writeAll(tenantId, data);
124
+ }
125
+
126
+ async list(tenantId: string): Promise<string[]> {
127
+ const data = await this.readAll(tenantId);
128
+ return Object.keys(data);
129
+ }
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Factory
134
+ // ---------------------------------------------------------------------------
135
+
136
+ export const createSecretsStore = (
137
+ _agentId: string,
138
+ signingKey: string,
139
+ _config?: StateConfig,
140
+ options?: { workingDir?: string },
141
+ ): SecretsStore => {
142
+ const workingDir = options?.workingDir ?? process.cwd();
143
+ return new FileSecretsStore(workingDir, signingKey);
144
+ };
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // Env resolution helper
148
+ // ---------------------------------------------------------------------------
149
+
150
+ /**
151
+ * Resolve an env var name: check tenant secrets first, then process.env.
152
+ */
153
+ export async function resolveEnv(
154
+ secretsStore: SecretsStore | undefined,
155
+ tenantId: string | null | undefined,
156
+ envName: string,
157
+ ): Promise<string | undefined> {
158
+ if (tenantId && secretsStore) {
159
+ const secrets = await secretsStore.get(tenantId);
160
+ if (secrets[envName]) return secrets[envName];
161
+ }
162
+ return process.env[envName];
163
+ }