@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.
- package/.turbo/turbo-build.log +12 -11
- package/.turbo/turbo-lint.log +6 -0
- package/.turbo/turbo-test.log +27100 -0
- package/CHANGELOG.md +37 -0
- package/dist/chunk-MCKGQKYU.js +15 -0
- package/dist/dist-3KMQR4IO.js +27092 -0
- package/dist/index.d.ts +553 -29
- package/dist/index.js +3132 -1902
- package/dist/isolate-5MISBSUK.js +733 -0
- package/dist/isolate-5R6762YA.js +605 -0
- package/dist/isolate-KUZ5NOPG.js +727 -0
- package/dist/isolate-LOL3T7RA.js +729 -0
- package/dist/isolate-N22X4TCE.js +740 -0
- package/dist/isolate-T7WXM7IL.js +1490 -0
- package/dist/isolate-TCWTUVG4.js +1532 -0
- package/dist/isolate-WFOLANOB.js +768 -0
- package/package.json +24 -4
- package/scripts/migrate-to-engine.mjs +556 -0
- package/src/config.ts +112 -1
- package/src/harness.ts +282 -91
- package/src/index.ts +7 -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/mcp.ts +140 -9
- package/src/memory.ts +142 -191
- package/src/reminder-store.ts +7 -235
- package/src/reminder-tools.ts +15 -2
- package/src/secrets-store.ts +163 -0
- package/src/state.ts +22 -1291
- 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/subagent-manager.ts +1 -0
- package/src/subagent-tools.ts +1 -0
- package/src/telemetry.ts +5 -1
- package/src/tenant-token.ts +42 -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/src/kv-store.ts +0 -216
package/src/reminder-store.ts
CHANGED
|
@@ -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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
};
|
package/src/reminder-tools.ts
CHANGED
|
@@ -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
|
+
}
|