@poncho-ai/harness 0.34.1 → 0.35.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 +11 -11
- package/.turbo/turbo-lint.log +6 -0
- package/.turbo/turbo-test.log +11931 -0
- package/CHANGELOG.md +19 -0
- package/dist/index.d.ts +77 -9
- package/dist/index.js +610 -105
- package/package.json +3 -2
- package/src/config.ts +6 -0
- package/src/harness.ts +61 -5
- package/src/index.ts +2 -0
- package/src/mcp.ts +140 -9
- package/src/memory.ts +33 -13
- package/src/reminder-store.ts +6 -0
- package/src/reminder-tools.ts +15 -2
- package/src/secrets-store.ts +252 -0
- package/src/state.ts +41 -19
- 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
|
@@ -0,0 +1,252 @@
|
|
|
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
|
+
STORAGE_SCHEMA_VERSION,
|
|
9
|
+
} from "./agent-identity.js";
|
|
10
|
+
import { createRawKVStore, type RawKVStore } from "./kv-store.js";
|
|
11
|
+
import type { StateConfig } from "./state.js";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Interface
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export interface SecretsStore {
|
|
18
|
+
get(tenantId: string): Promise<Record<string, string>>;
|
|
19
|
+
set(tenantId: string, key: string, value: string): Promise<void>;
|
|
20
|
+
delete(tenantId: string, key: string): Promise<void>;
|
|
21
|
+
list(tenantId: string): Promise<string[]>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Encryption helpers (AES-256-GCM)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
type EncryptedBlob = { iv: string; ct: string; tag: string };
|
|
29
|
+
|
|
30
|
+
function deriveKey(signingKey: string): Buffer {
|
|
31
|
+
// HKDF-like derivation: SHA-256 of fixed salt + signing key
|
|
32
|
+
return createHash("sha256")
|
|
33
|
+
.update("poncho-secrets-v1:" + signingKey)
|
|
34
|
+
.digest();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function encrypt(plaintext: string, key: Buffer): EncryptedBlob {
|
|
38
|
+
const iv = randomBytes(12);
|
|
39
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
40
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
41
|
+
const tag = cipher.getAuthTag();
|
|
42
|
+
return {
|
|
43
|
+
iv: iv.toString("base64"),
|
|
44
|
+
ct: ct.toString("base64"),
|
|
45
|
+
tag: tag.toString("base64"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function decrypt(blob: EncryptedBlob, key: Buffer): string {
|
|
50
|
+
const decipher = createDecipheriv(
|
|
51
|
+
"aes-256-gcm",
|
|
52
|
+
key,
|
|
53
|
+
Buffer.from(blob.iv, "base64"),
|
|
54
|
+
);
|
|
55
|
+
decipher.setAuthTag(Buffer.from(blob.tag, "base64"));
|
|
56
|
+
return Buffer.concat([
|
|
57
|
+
decipher.update(Buffer.from(blob.ct, "base64")),
|
|
58
|
+
decipher.final(),
|
|
59
|
+
]).toString("utf8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// File-based implementation
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
class FileSecretsStore implements SecretsStore {
|
|
67
|
+
private readonly workingDir: string;
|
|
68
|
+
private readonly encKey: Buffer;
|
|
69
|
+
|
|
70
|
+
constructor(workingDir: string, signingKey: string) {
|
|
71
|
+
this.workingDir = workingDir;
|
|
72
|
+
this.encKey = deriveKey(signingKey);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async filePath(tenantId: string): Promise<string> {
|
|
76
|
+
const identity = await ensureAgentIdentity(this.workingDir);
|
|
77
|
+
const dir = resolve(
|
|
78
|
+
getAgentStoreDirectory(identity),
|
|
79
|
+
"tenants",
|
|
80
|
+
slugifyStorageComponent(tenantId),
|
|
81
|
+
);
|
|
82
|
+
return resolve(dir, "secrets.json");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async readAll(tenantId: string): Promise<Record<string, EncryptedBlob>> {
|
|
86
|
+
try {
|
|
87
|
+
const raw = await readFile(await this.filePath(tenantId), "utf8");
|
|
88
|
+
return JSON.parse(raw) as Record<string, EncryptedBlob>;
|
|
89
|
+
} catch {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async writeAll(
|
|
95
|
+
tenantId: string,
|
|
96
|
+
data: Record<string, EncryptedBlob>,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const fp = await this.filePath(tenantId);
|
|
99
|
+
await mkdir(dirname(fp), { recursive: true });
|
|
100
|
+
await writeFile(fp, JSON.stringify(data, null, 2), "utf8");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async get(tenantId: string): Promise<Record<string, string>> {
|
|
104
|
+
const data = await this.readAll(tenantId);
|
|
105
|
+
const result: Record<string, string> = {};
|
|
106
|
+
for (const [k, blob] of Object.entries(data)) {
|
|
107
|
+
try {
|
|
108
|
+
result[k] = decrypt(blob, this.encKey);
|
|
109
|
+
} catch {
|
|
110
|
+
// Skip entries that can't be decrypted (key rotation)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async set(tenantId: string, key: string, value: string): Promise<void> {
|
|
117
|
+
const data = await this.readAll(tenantId);
|
|
118
|
+
data[key] = encrypt(value, this.encKey);
|
|
119
|
+
await this.writeAll(tenantId, data);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async delete(tenantId: string, key: string): Promise<void> {
|
|
123
|
+
const data = await this.readAll(tenantId);
|
|
124
|
+
delete data[key];
|
|
125
|
+
await this.writeAll(tenantId, data);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async list(tenantId: string): Promise<string[]> {
|
|
129
|
+
const data = await this.readAll(tenantId);
|
|
130
|
+
return Object.keys(data);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// KV-backed implementation
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
class KVSecretsStore implements SecretsStore {
|
|
139
|
+
private readonly kv: RawKVStore;
|
|
140
|
+
private readonly baseKey: string;
|
|
141
|
+
private readonly encKey: Buffer;
|
|
142
|
+
private readonly ttl?: number;
|
|
143
|
+
|
|
144
|
+
constructor(kv: RawKVStore, baseKey: string, signingKey: string, ttl?: number) {
|
|
145
|
+
this.kv = kv;
|
|
146
|
+
this.baseKey = baseKey;
|
|
147
|
+
this.encKey = deriveKey(signingKey);
|
|
148
|
+
this.ttl = ttl;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private kvKey(tenantId: string): string {
|
|
152
|
+
return `${this.baseKey}:t:${slugifyStorageComponent(tenantId)}:secrets`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async readAll(tenantId: string): Promise<Record<string, EncryptedBlob>> {
|
|
156
|
+
try {
|
|
157
|
+
const raw = await this.kv.get(this.kvKey(tenantId));
|
|
158
|
+
if (!raw) return {};
|
|
159
|
+
return JSON.parse(raw) as Record<string, EncryptedBlob>;
|
|
160
|
+
} catch {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private async writeAll(
|
|
166
|
+
tenantId: string,
|
|
167
|
+
data: Record<string, EncryptedBlob>,
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
const key = this.kvKey(tenantId);
|
|
170
|
+
const value = JSON.stringify(data);
|
|
171
|
+
if (this.ttl) {
|
|
172
|
+
await this.kv.setWithTtl(key, value, this.ttl);
|
|
173
|
+
} else {
|
|
174
|
+
await this.kv.set(key, value);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async get(tenantId: string): Promise<Record<string, string>> {
|
|
179
|
+
const data = await this.readAll(tenantId);
|
|
180
|
+
const result: Record<string, string> = {};
|
|
181
|
+
for (const [k, blob] of Object.entries(data)) {
|
|
182
|
+
try {
|
|
183
|
+
result[k] = decrypt(blob, this.encKey);
|
|
184
|
+
} catch {
|
|
185
|
+
// Skip entries that can't be decrypted
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async set(tenantId: string, key: string, value: string): Promise<void> {
|
|
192
|
+
const data = await this.readAll(tenantId);
|
|
193
|
+
data[key] = encrypt(value, this.encKey);
|
|
194
|
+
await this.writeAll(tenantId, data);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async delete(tenantId: string, key: string): Promise<void> {
|
|
198
|
+
const data = await this.readAll(tenantId);
|
|
199
|
+
delete data[key];
|
|
200
|
+
await this.writeAll(tenantId, data);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async list(tenantId: string): Promise<string[]> {
|
|
204
|
+
const data = await this.readAll(tenantId);
|
|
205
|
+
return Object.keys(data);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Factory
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
export const createSecretsStore = (
|
|
214
|
+
agentId: string,
|
|
215
|
+
signingKey: string,
|
|
216
|
+
config?: StateConfig,
|
|
217
|
+
options?: { workingDir?: string },
|
|
218
|
+
): SecretsStore => {
|
|
219
|
+
const provider = config?.provider ?? "local";
|
|
220
|
+
const ttl = typeof config?.ttl === "number" ? config.ttl : undefined;
|
|
221
|
+
const workingDir = options?.workingDir ?? process.cwd();
|
|
222
|
+
|
|
223
|
+
if (provider === "local" || provider === "memory") {
|
|
224
|
+
return new FileSecretsStore(workingDir, signingKey);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const kv = createRawKVStore(config);
|
|
228
|
+
if (kv) {
|
|
229
|
+
const baseKey = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}`;
|
|
230
|
+
return new KVSecretsStore(kv, baseKey, signingKey, ttl);
|
|
231
|
+
}
|
|
232
|
+
return new FileSecretsStore(workingDir, signingKey);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Env resolution helper
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Resolve an env var name: check tenant secrets first, then process.env.
|
|
241
|
+
*/
|
|
242
|
+
export async function resolveEnv(
|
|
243
|
+
secretsStore: SecretsStore | undefined,
|
|
244
|
+
tenantId: string | null | undefined,
|
|
245
|
+
envName: string,
|
|
246
|
+
): Promise<string | undefined> {
|
|
247
|
+
if (tenantId && secretsStore) {
|
|
248
|
+
const secrets = await secretsStore.get(tenantId);
|
|
249
|
+
if (secrets[envName]) return secrets[envName];
|
|
250
|
+
}
|
|
251
|
+
return process.env[envName];
|
|
252
|
+
}
|
package/src/state.ts
CHANGED
|
@@ -97,10 +97,16 @@ export interface Conversation {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
export interface ConversationStore {
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
/**
|
|
101
|
+
* List conversations. tenantId semantics:
|
|
102
|
+
* undefined = no filter (builder/admin sees everything)
|
|
103
|
+
* null = legacy single-user only
|
|
104
|
+
* string = tenant-scoped
|
|
105
|
+
*/
|
|
106
|
+
list(ownerId?: string, tenantId?: string | null): Promise<Conversation[]>;
|
|
107
|
+
listSummaries(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]>;
|
|
102
108
|
get(conversationId: string): Promise<Conversation | undefined>;
|
|
103
|
-
create(ownerId?: string, title?: string): Promise<Conversation>;
|
|
109
|
+
create(ownerId?: string, title?: string, tenantId?: string | null): Promise<Conversation>;
|
|
104
110
|
update(conversation: Conversation): Promise<void>;
|
|
105
111
|
rename(conversationId: string, title: string): Promise<Conversation | undefined>;
|
|
106
112
|
delete(conversationId: string): Promise<boolean>;
|
|
@@ -275,17 +281,19 @@ export class InMemoryConversationStore implements ConversationStore {
|
|
|
275
281
|
}
|
|
276
282
|
}
|
|
277
283
|
|
|
278
|
-
async list(ownerId?: string): Promise<Conversation[]> {
|
|
284
|
+
async list(ownerId?: string, tenantId?: string | null): Promise<Conversation[]> {
|
|
279
285
|
this.purgeExpired();
|
|
280
286
|
return Array.from(this.conversations.values())
|
|
281
287
|
.filter((conversation) => !ownerId || conversation.ownerId === ownerId)
|
|
288
|
+
.filter((c) => tenantId === undefined || c.tenantId === tenantId)
|
|
282
289
|
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
283
290
|
}
|
|
284
291
|
|
|
285
|
-
async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
|
|
292
|
+
async listSummaries(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]> {
|
|
286
293
|
this.purgeExpired();
|
|
287
294
|
return Array.from(this.conversations.values())
|
|
288
295
|
.filter((c) => !ownerId || c.ownerId === ownerId)
|
|
296
|
+
.filter((c) => tenantId === undefined || c.tenantId === tenantId)
|
|
289
297
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
290
298
|
.map((c) => ({
|
|
291
299
|
conversationId: c.conversationId,
|
|
@@ -293,6 +301,7 @@ export class InMemoryConversationStore implements ConversationStore {
|
|
|
293
301
|
updatedAt: c.updatedAt,
|
|
294
302
|
createdAt: c.createdAt,
|
|
295
303
|
ownerId: c.ownerId,
|
|
304
|
+
tenantId: c.tenantId,
|
|
296
305
|
parentConversationId: c.parentConversationId,
|
|
297
306
|
messageCount: c.messages.length,
|
|
298
307
|
hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0,
|
|
@@ -305,14 +314,14 @@ export class InMemoryConversationStore implements ConversationStore {
|
|
|
305
314
|
return this.conversations.get(conversationId);
|
|
306
315
|
}
|
|
307
316
|
|
|
308
|
-
async create(ownerId = DEFAULT_OWNER, title?: string): Promise<Conversation> {
|
|
317
|
+
async create(ownerId = DEFAULT_OWNER, title?: string, tenantId: string | null = null): Promise<Conversation> {
|
|
309
318
|
const now = Date.now();
|
|
310
319
|
const conversation: Conversation = {
|
|
311
320
|
conversationId: globalThis.crypto?.randomUUID?.() ?? `${now}-${Math.random()}`,
|
|
312
321
|
title: normalizeTitle(title),
|
|
313
322
|
messages: [],
|
|
314
323
|
ownerId,
|
|
315
|
-
tenantId
|
|
324
|
+
tenantId,
|
|
316
325
|
createdAt: now,
|
|
317
326
|
updatedAt: now,
|
|
318
327
|
};
|
|
@@ -368,6 +377,7 @@ export type ConversationSummary = {
|
|
|
368
377
|
updatedAt: number;
|
|
369
378
|
createdAt?: number;
|
|
370
379
|
ownerId: string;
|
|
380
|
+
tenantId?: string | null;
|
|
371
381
|
parentConversationId?: string;
|
|
372
382
|
messageCount?: number;
|
|
373
383
|
hasPendingApprovals?: boolean;
|
|
@@ -386,6 +396,7 @@ type ConversationStoreFile = {
|
|
|
386
396
|
updatedAt: number;
|
|
387
397
|
createdAt?: number;
|
|
388
398
|
ownerId: string;
|
|
399
|
+
tenantId?: string | null;
|
|
389
400
|
fileName: string;
|
|
390
401
|
parentConversationId?: string;
|
|
391
402
|
messageCount?: number;
|
|
@@ -465,6 +476,7 @@ class FileConversationStore implements ConversationStore {
|
|
|
465
476
|
updatedAt: conversation.updatedAt,
|
|
466
477
|
createdAt: conversation.createdAt,
|
|
467
478
|
ownerId: conversation.ownerId,
|
|
479
|
+
tenantId: conversation.tenantId,
|
|
468
480
|
fileName: entry.name,
|
|
469
481
|
parentConversationId: conversation.parentConversationId,
|
|
470
482
|
messageCount: conversation.messages.length,
|
|
@@ -523,6 +535,7 @@ class FileConversationStore implements ConversationStore {
|
|
|
523
535
|
updatedAt: conversation.updatedAt,
|
|
524
536
|
createdAt: conversation.createdAt,
|
|
525
537
|
ownerId: conversation.ownerId,
|
|
538
|
+
tenantId: conversation.tenantId,
|
|
526
539
|
fileName,
|
|
527
540
|
parentConversationId: conversation.parentConversationId,
|
|
528
541
|
messageCount: conversation.messages.length,
|
|
@@ -534,10 +547,11 @@ class FileConversationStore implements ConversationStore {
|
|
|
534
547
|
await this.writing;
|
|
535
548
|
}
|
|
536
549
|
|
|
537
|
-
async list(ownerId?: string): Promise<Conversation[]> {
|
|
550
|
+
async list(ownerId?: string, tenantId?: string | null): Promise<Conversation[]> {
|
|
538
551
|
await this.ensureLoaded();
|
|
539
552
|
const summaries = Array.from(this.conversations.values())
|
|
540
553
|
.filter((conversation) => !ownerId || conversation.ownerId === ownerId)
|
|
554
|
+
.filter((c) => tenantId === undefined || (c.tenantId ?? null) === tenantId)
|
|
541
555
|
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
542
556
|
const conversations: Conversation[] = [];
|
|
543
557
|
for (const summary of summaries) {
|
|
@@ -549,10 +563,11 @@ class FileConversationStore implements ConversationStore {
|
|
|
549
563
|
return conversations;
|
|
550
564
|
}
|
|
551
565
|
|
|
552
|
-
async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
|
|
566
|
+
async listSummaries(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]> {
|
|
553
567
|
await this.ensureLoaded();
|
|
554
568
|
return Array.from(this.conversations.values())
|
|
555
569
|
.filter((c) => !ownerId || c.ownerId === ownerId)
|
|
570
|
+
.filter((c) => tenantId === undefined || (c.tenantId ?? null) === tenantId)
|
|
556
571
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
557
572
|
.map((c) => ({
|
|
558
573
|
conversationId: c.conversationId,
|
|
@@ -560,6 +575,7 @@ class FileConversationStore implements ConversationStore {
|
|
|
560
575
|
updatedAt: c.updatedAt,
|
|
561
576
|
createdAt: c.createdAt,
|
|
562
577
|
ownerId: c.ownerId,
|
|
578
|
+
tenantId: c.tenantId,
|
|
563
579
|
parentConversationId: c.parentConversationId,
|
|
564
580
|
messageCount: c.messageCount,
|
|
565
581
|
hasPendingApprovals: c.hasPendingApprovals,
|
|
@@ -576,7 +592,7 @@ class FileConversationStore implements ConversationStore {
|
|
|
576
592
|
return await this.readConversationFile(summary.fileName);
|
|
577
593
|
}
|
|
578
594
|
|
|
579
|
-
async create(ownerId = DEFAULT_OWNER, title?: string): Promise<Conversation> {
|
|
595
|
+
async create(ownerId = DEFAULT_OWNER, title?: string, tenantId: string | null = null): Promise<Conversation> {
|
|
580
596
|
await this.ensureLoaded();
|
|
581
597
|
const now = Date.now();
|
|
582
598
|
const conversation: Conversation = {
|
|
@@ -584,7 +600,7 @@ class FileConversationStore implements ConversationStore {
|
|
|
584
600
|
title: normalizeTitle(title),
|
|
585
601
|
messages: [],
|
|
586
602
|
ownerId,
|
|
587
|
-
tenantId
|
|
603
|
+
tenantId,
|
|
588
604
|
createdAt: now,
|
|
589
605
|
updatedAt: now,
|
|
590
606
|
};
|
|
@@ -770,6 +786,7 @@ type ConversationMeta = {
|
|
|
770
786
|
updatedAt: number;
|
|
771
787
|
createdAt?: number;
|
|
772
788
|
ownerId: string;
|
|
789
|
+
tenantId?: string | null;
|
|
773
790
|
parentConversationId?: string;
|
|
774
791
|
messageCount?: number;
|
|
775
792
|
hasPendingApprovals?: boolean;
|
|
@@ -887,10 +904,10 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
887
904
|
}
|
|
888
905
|
}
|
|
889
906
|
|
|
890
|
-
async list(ownerId?: string): Promise<Conversation[]> {
|
|
907
|
+
async list(ownerId?: string, tenantId?: string | null): Promise<Conversation[]> {
|
|
891
908
|
const kv = await this.client();
|
|
892
909
|
if (!kv) {
|
|
893
|
-
return await this.memoryFallback.list(ownerId);
|
|
910
|
+
return await this.memoryFallback.list(ownerId, tenantId);
|
|
894
911
|
}
|
|
895
912
|
if (!ownerId) {
|
|
896
913
|
return [];
|
|
@@ -903,16 +920,19 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
903
920
|
for (const raw of rawValues) {
|
|
904
921
|
if (!raw) continue;
|
|
905
922
|
try {
|
|
906
|
-
|
|
923
|
+
const conv = JSON.parse(raw) as Conversation;
|
|
924
|
+
if (tenantId === undefined || conv.tenantId === tenantId) {
|
|
925
|
+
conversations.push(conv);
|
|
926
|
+
}
|
|
907
927
|
} catch { /* skip invalid records */ }
|
|
908
928
|
}
|
|
909
929
|
return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
910
930
|
}
|
|
911
931
|
|
|
912
|
-
async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
|
|
932
|
+
async listSummaries(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]> {
|
|
913
933
|
const kv = await this.client();
|
|
914
934
|
if (!kv) {
|
|
915
|
-
return await this.memoryFallback.listSummaries(ownerId);
|
|
935
|
+
return await this.memoryFallback.listSummaries(ownerId, tenantId);
|
|
916
936
|
}
|
|
917
937
|
if (!ownerId) {
|
|
918
938
|
return [];
|
|
@@ -926,13 +946,14 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
926
946
|
if (!raw) continue;
|
|
927
947
|
try {
|
|
928
948
|
const meta = JSON.parse(raw) as ConversationMeta;
|
|
929
|
-
if (meta.ownerId === ownerId) {
|
|
949
|
+
if (meta.ownerId === ownerId && (tenantId === undefined || (meta.tenantId ?? null) === tenantId)) {
|
|
930
950
|
summaries.push({
|
|
931
951
|
conversationId: meta.conversationId,
|
|
932
952
|
title: meta.title,
|
|
933
953
|
updatedAt: meta.updatedAt,
|
|
934
954
|
createdAt: meta.createdAt,
|
|
935
955
|
ownerId: meta.ownerId,
|
|
956
|
+
tenantId: meta.tenantId,
|
|
936
957
|
parentConversationId: meta.parentConversationId,
|
|
937
958
|
messageCount: meta.messageCount,
|
|
938
959
|
hasPendingApprovals: meta.hasPendingApprovals,
|
|
@@ -960,14 +981,14 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
960
981
|
}
|
|
961
982
|
}
|
|
962
983
|
|
|
963
|
-
async create(ownerId = DEFAULT_OWNER, title?: string): Promise<Conversation> {
|
|
984
|
+
async create(ownerId = DEFAULT_OWNER, title?: string, tenantId: string | null = null): Promise<Conversation> {
|
|
964
985
|
const now = Date.now();
|
|
965
986
|
const conversation: Conversation = {
|
|
966
987
|
conversationId: globalThis.crypto?.randomUUID?.() ?? `${now}-${Math.random()}`,
|
|
967
988
|
title: normalizeTitle(title),
|
|
968
989
|
messages: [],
|
|
969
990
|
ownerId,
|
|
970
|
-
tenantId
|
|
991
|
+
tenantId,
|
|
971
992
|
createdAt: now,
|
|
972
993
|
updatedAt: now,
|
|
973
994
|
};
|
|
@@ -997,6 +1018,7 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
|
|
|
997
1018
|
updatedAt: nextConversation.updatedAt,
|
|
998
1019
|
createdAt: nextConversation.createdAt,
|
|
999
1020
|
ownerId: nextConversation.ownerId,
|
|
1021
|
+
tenantId: nextConversation.tenantId,
|
|
1000
1022
|
parentConversationId: nextConversation.parentConversationId,
|
|
1001
1023
|
messageCount: nextConversation.messages.length,
|
|
1002
1024
|
hasPendingApprovals: Array.isArray(nextConversation.pendingApprovals) && nextConversation.pendingApprovals.length > 0,
|
package/src/subagent-manager.ts
CHANGED
package/src/subagent-tools.ts
CHANGED
package/src/telemetry.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { AgentEvent } from "@poncho-ai/sdk";
|
|
2
2
|
|
|
3
3
|
const MAX_FIELD_LENGTH = 200;
|
|
4
|
+
const OMIT_FROM_LOG = new Set(["continuationMessages", "_harnessMessages", "messages", "compactedHistory"]);
|
|
4
5
|
|
|
5
6
|
function sanitizeEventForLog(event: AgentEvent): string {
|
|
6
|
-
return JSON.stringify(event, (
|
|
7
|
+
return JSON.stringify(event, (key, value) => {
|
|
8
|
+
if (OMIT_FROM_LOG.has(key) && Array.isArray(value)) {
|
|
9
|
+
return `[${value.length} messages]`;
|
|
10
|
+
}
|
|
7
11
|
if (typeof value === "string" && value.length > MAX_FIELD_LENGTH) {
|
|
8
12
|
return `${value.slice(0, 80)}...[${value.length} chars]`;
|
|
9
13
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jwtVerify, type JWTPayload } from "jose";
|
|
2
|
+
|
|
3
|
+
export interface TenantTokenPayload {
|
|
4
|
+
tenantId: string;
|
|
5
|
+
metadata?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Verify a tenant JWT (HS256) signed with the given key.
|
|
10
|
+
* Returns the decoded payload on success, or undefined on any failure.
|
|
11
|
+
*/
|
|
12
|
+
export async function verifyTenantToken(
|
|
13
|
+
signingKey: string,
|
|
14
|
+
token: string,
|
|
15
|
+
): Promise<TenantTokenPayload | undefined> {
|
|
16
|
+
try {
|
|
17
|
+
const secret = new TextEncoder().encode(signingKey);
|
|
18
|
+
const { payload } = await jwtVerify(token, secret, {
|
|
19
|
+
algorithms: ["HS256"],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const tenantId = payload.sub;
|
|
23
|
+
if (!tenantId || typeof tenantId !== "string") {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const metadata = extractMetadata(payload);
|
|
28
|
+
return { tenantId, metadata };
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function extractMetadata(
|
|
35
|
+
payload: JWTPayload,
|
|
36
|
+
): Record<string, unknown> | undefined {
|
|
37
|
+
const meta = payload.meta;
|
|
38
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
39
|
+
return meta as Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|