@vellumai/assistant 0.4.4 → 0.4.6
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/ARCHITECTURE.md +4 -4
- package/README.md +6 -6
- package/bun.lock +6 -2
- package/docs/architecture/memory.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/actor-token-service.test.ts +5 -2
- package/src/__tests__/assistant-feature-flags-integration.test.ts +1 -0
- package/src/__tests__/call-controller.test.ts +78 -0
- package/src/__tests__/call-domain.test.ts +148 -10
- package/src/__tests__/call-pointer-message-composer.test.ts +39 -49
- package/src/__tests__/call-pointer-messages.test.ts +105 -43
- package/src/__tests__/canonical-guardian-store.test.ts +44 -10
- package/src/__tests__/channel-approval-routes.test.ts +67 -65
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +1 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +2 -2
- package/src/__tests__/deterministic-verification-control-plane.test.ts +6 -6
- package/src/__tests__/guardian-actions-endpoint.test.ts +7 -6
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +57 -12
- package/src/__tests__/guardian-grant-minting.test.ts +24 -24
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +205 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +64 -25
- package/src/__tests__/guardian-routing-state.test.ts +4 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -2
- package/src/__tests__/inbound-invite-redemption.test.ts +8 -8
- package/src/__tests__/memory-retrieval.benchmark.test.ts +22 -47
- package/src/__tests__/no-is-trusted-guard.test.ts +77 -0
- package/src/__tests__/non-member-access-request.test.ts +50 -47
- package/src/__tests__/relay-server.test.ts +71 -0
- package/src/__tests__/send-endpoint-busy.test.ts +6 -0
- package/src/__tests__/session-tool-setup-tools-disabled.test.ts +155 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +66 -2
- package/src/__tests__/system-prompt.test.ts +1 -0
- package/src/__tests__/tool-approval-handler.test.ts +1 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +9 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +8 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +22 -22
- package/src/__tests__/trusted-contact-multichannel.test.ts +4 -4
- package/src/__tests__/trusted-contact-verification.test.ts +10 -10
- package/src/approvals/guardian-decision-primitive.ts +29 -25
- package/src/approvals/guardian-request-resolvers.ts +9 -5
- package/src/calls/call-pointer-message-composer.ts +27 -85
- package/src/calls/call-pointer-messages.ts +54 -21
- package/src/calls/guardian-dispatch.ts +30 -0
- package/src/calls/relay-server.ts +13 -13
- package/src/config/system-prompt.ts +10 -3
- package/src/config/templates/BOOTSTRAP.md +6 -5
- package/src/config/templates/USER.md +1 -0
- package/src/config/user-reference.ts +44 -0
- package/src/daemon/handlers/guardian-actions.ts +5 -2
- package/src/daemon/handlers/sessions.ts +8 -3
- package/src/daemon/lifecycle.ts +109 -3
- package/src/daemon/server.ts +32 -24
- package/src/daemon/session-agent-loop.ts +4 -3
- package/src/daemon/session-lifecycle.ts +1 -9
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-runtime-assembly.ts +2 -0
- package/src/daemon/session-tool-setup.ts +10 -0
- package/src/daemon/session.ts +1 -0
- package/src/memory/canonical-guardian-store.ts +40 -0
- package/src/memory/conversation-crud.ts +26 -0
- package/src/memory/conversation-store.ts +1 -0
- package/src/memory/db-init.ts +8 -0
- package/src/memory/guardian-bindings.ts +4 -0
- package/src/memory/job-handlers/backfill.ts +2 -9
- package/src/memory/migrations/125-guardian-principal-id-columns.ts +19 -0
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +210 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/schema.ts +3 -0
- package/src/notifications/copy-composer.ts +2 -2
- package/src/runtime/access-request-helper.ts +43 -28
- package/src/runtime/actor-trust-resolver.ts +19 -14
- package/src/runtime/channel-guardian-service.ts +6 -0
- package/src/runtime/guardian-context-resolver.ts +6 -2
- package/src/runtime/guardian-reply-router.ts +33 -16
- package/src/runtime/guardian-vellum-migration.ts +29 -5
- package/src/runtime/http-types.ts +0 -13
- package/src/runtime/local-actor-identity.ts +19 -13
- package/src/runtime/middleware/actor-token.ts +2 -2
- package/src/runtime/routes/channel-delivery-routes.ts +5 -5
- package/src/runtime/routes/conversation-routes.ts +45 -35
- package/src/runtime/routes/guardian-action-routes.ts +7 -1
- package/src/runtime/routes/guardian-approval-interception.ts +52 -52
- package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -0
- package/src/runtime/routes/inbound-conversation.ts +7 -7
- package/src/runtime/routes/inbound-message-handler.ts +105 -94
- package/src/runtime/tool-grant-request-helper.ts +1 -0
- package/src/util/logger.ts +10 -0
- package/src/daemon/call-pointer-generators.ts +0 -59
|
@@ -64,11 +64,14 @@ afterAll(() => {
|
|
|
64
64
|
// Helpers
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
66
|
|
|
67
|
+
/** Consistent test principal used across all test actors and requests. */
|
|
68
|
+
const TEST_PRINCIPAL_ID = 'test-principal-id';
|
|
69
|
+
|
|
67
70
|
function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
|
|
68
71
|
return {
|
|
69
72
|
externalUserId: 'guardian-1',
|
|
70
73
|
channel: 'telegram',
|
|
71
|
-
|
|
74
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
72
75
|
...overrides,
|
|
73
76
|
};
|
|
74
77
|
}
|
|
@@ -77,7 +80,7 @@ function trustedActor(overrides: Partial<ActorContext> = {}): ActorContext {
|
|
|
77
80
|
return {
|
|
78
81
|
externalUserId: undefined,
|
|
79
82
|
channel: 'desktop',
|
|
80
|
-
|
|
83
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
81
84
|
...overrides,
|
|
82
85
|
};
|
|
83
86
|
}
|
|
@@ -120,6 +123,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
120
123
|
sourceChannel: 'telegram',
|
|
121
124
|
conversationId: 'conv-1',
|
|
122
125
|
guardianExternalUserId: 'guardian-1',
|
|
126
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
123
127
|
toolName: 'shell',
|
|
124
128
|
inputDigest: 'sha256:abc',
|
|
125
129
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -153,6 +157,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
153
157
|
sourceChannel: 'telegram',
|
|
154
158
|
conversationId: 'conv-1',
|
|
155
159
|
guardianExternalUserId: 'guardian-1',
|
|
160
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
156
161
|
toolName: 'shell',
|
|
157
162
|
inputDigest: 'sha256:abc',
|
|
158
163
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -178,6 +183,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
178
183
|
sourceType: 'voice',
|
|
179
184
|
sourceChannel: 'twilio',
|
|
180
185
|
guardianExternalUserId: 'guardian-1',
|
|
186
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
181
187
|
callSessionId: 'call-1',
|
|
182
188
|
pendingQuestionId: 'pq-1',
|
|
183
189
|
questionText: 'What is the gate code?',
|
|
@@ -199,21 +205,22 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
199
205
|
expect(resolved!.answerText).toBe('1234');
|
|
200
206
|
});
|
|
201
207
|
|
|
202
|
-
// ──
|
|
208
|
+
// ── Principal mismatch ──────────────────────────────────────────────
|
|
203
209
|
|
|
204
|
-
test('rejects decision when actor does not match
|
|
210
|
+
test('rejects decision when actor principal does not match request principal', async () => {
|
|
205
211
|
const req = createCanonicalGuardianRequest({
|
|
206
212
|
kind: 'tool_approval',
|
|
207
213
|
sourceType: 'channel',
|
|
208
214
|
conversationId: 'conv-1',
|
|
209
215
|
guardianExternalUserId: 'guardian-1',
|
|
216
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
210
217
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
211
218
|
});
|
|
212
219
|
|
|
213
220
|
const result = await applyCanonicalGuardianDecision({
|
|
214
221
|
requestId: req.id,
|
|
215
222
|
action: 'approve_once',
|
|
216
|
-
actorContext: guardianActor({
|
|
223
|
+
actorContext: guardianActor({ guardianPrincipalId: 'wrong-principal' }),
|
|
217
224
|
});
|
|
218
225
|
|
|
219
226
|
expect(result.applied).toBe(false);
|
|
@@ -225,12 +232,13 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
225
232
|
expect(unchanged!.status).toBe('pending');
|
|
226
233
|
});
|
|
227
234
|
|
|
228
|
-
test('
|
|
235
|
+
test('matching principal authorizes decision (cross-channel same principal)', async () => {
|
|
229
236
|
const req = createCanonicalGuardianRequest({
|
|
230
237
|
kind: 'tool_approval',
|
|
231
238
|
sourceType: 'desktop',
|
|
232
239
|
conversationId: 'conv-1',
|
|
233
240
|
guardianExternalUserId: 'guardian-1',
|
|
241
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
234
242
|
toolName: 'shell',
|
|
235
243
|
inputDigest: 'sha256:abc',
|
|
236
244
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -243,24 +251,48 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
243
251
|
});
|
|
244
252
|
|
|
245
253
|
expect(result.applied).toBe(true);
|
|
246
|
-
// No grant minted because trusted actor has no externalUserId
|
|
247
254
|
if (!result.applied) return;
|
|
255
|
+
// No grant minted because trusted actor has no externalUserId
|
|
248
256
|
expect(result.grantMinted).toBe(false);
|
|
249
257
|
});
|
|
250
258
|
|
|
251
|
-
test('rejects
|
|
259
|
+
test('rejects decision when request has no guardianPrincipalId', async () => {
|
|
260
|
+
// unknown_kind is not in DECISIONABLE_KINDS so it can be created without
|
|
261
|
+
// guardianPrincipalId, but the decision primitive still rejects because
|
|
262
|
+
// the request is missing its principal binding.
|
|
263
|
+
const req = createCanonicalGuardianRequest({
|
|
264
|
+
kind: 'unknown_kind',
|
|
265
|
+
sourceType: 'channel',
|
|
266
|
+
conversationId: 'conv-1',
|
|
267
|
+
guardianExternalUserId: 'guardian-1',
|
|
268
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const result = await applyCanonicalGuardianDecision({
|
|
272
|
+
requestId: req.id,
|
|
273
|
+
action: 'approve_once',
|
|
274
|
+
actorContext: guardianActor({ guardianPrincipalId: 'some-principal' }),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
expect(result.applied).toBe(false);
|
|
278
|
+
if (result.applied) return;
|
|
279
|
+
expect(result.reason).toBe('identity_mismatch');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('rejects decision when actor has no guardianPrincipalId', async () => {
|
|
252
283
|
const req = createCanonicalGuardianRequest({
|
|
253
284
|
kind: 'tool_approval',
|
|
254
285
|
sourceType: 'channel',
|
|
255
286
|
conversationId: 'conv-1',
|
|
256
|
-
|
|
287
|
+
guardianExternalUserId: 'guardian-1',
|
|
288
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
257
289
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
258
290
|
});
|
|
259
291
|
|
|
260
292
|
const result = await applyCanonicalGuardianDecision({
|
|
261
293
|
requestId: req.id,
|
|
262
294
|
action: 'approve_once',
|
|
263
|
-
actorContext: guardianActor({
|
|
295
|
+
actorContext: guardianActor({ guardianPrincipalId: undefined }),
|
|
264
296
|
});
|
|
265
297
|
|
|
266
298
|
expect(result.applied).toBe(false);
|
|
@@ -276,6 +308,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
276
308
|
sourceType: 'channel',
|
|
277
309
|
conversationId: 'conv-1',
|
|
278
310
|
guardianExternalUserId: 'guardian-1',
|
|
311
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
279
312
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
280
313
|
});
|
|
281
314
|
|
|
@@ -324,6 +357,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
324
357
|
sourceType: 'channel',
|
|
325
358
|
conversationId: 'conv-1',
|
|
326
359
|
guardianExternalUserId: 'guardian-1',
|
|
360
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
327
361
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
328
362
|
});
|
|
329
363
|
|
|
@@ -351,6 +385,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
351
385
|
sourceChannel: 'telegram',
|
|
352
386
|
conversationId: 'conv-1',
|
|
353
387
|
guardianExternalUserId: 'guardian-1',
|
|
388
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
354
389
|
toolName: 'shell',
|
|
355
390
|
inputDigest: 'sha256:abc',
|
|
356
391
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -385,6 +420,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
385
420
|
sourceType: 'channel',
|
|
386
421
|
conversationId: 'conv-1',
|
|
387
422
|
guardianExternalUserId: 'guardian-1',
|
|
423
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
388
424
|
expiresAt: new Date(Date.now() - 10_000).toISOString(), // already expired
|
|
389
425
|
});
|
|
390
426
|
|
|
@@ -405,6 +441,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
405
441
|
sourceType: 'channel',
|
|
406
442
|
conversationId: 'conv-1',
|
|
407
443
|
guardianExternalUserId: 'guardian-1',
|
|
444
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
408
445
|
// No expiresAt
|
|
409
446
|
});
|
|
410
447
|
|
|
@@ -426,6 +463,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
426
463
|
sourceChannel: 'telegram',
|
|
427
464
|
conversationId: 'conv-1',
|
|
428
465
|
guardianExternalUserId: 'guardian-1',
|
|
466
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
429
467
|
toolName: 'file_read',
|
|
430
468
|
inputDigest: 'sha256:def',
|
|
431
469
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -446,6 +484,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
446
484
|
sourceType: 'voice',
|
|
447
485
|
sourceChannel: 'twilio',
|
|
448
486
|
guardianExternalUserId: 'guardian-1',
|
|
487
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
449
488
|
callSessionId: 'call-99',
|
|
450
489
|
pendingQuestionId: 'pq-99',
|
|
451
490
|
questionText: 'What is the password?',
|
|
@@ -464,12 +503,13 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
464
503
|
expect(resolved!.answerText).toBe('secret123');
|
|
465
504
|
});
|
|
466
505
|
|
|
467
|
-
test('succeeds
|
|
506
|
+
test('succeeds for non-decisionable kind with matching principal', async () => {
|
|
468
507
|
const req = createCanonicalGuardianRequest({
|
|
469
508
|
kind: 'unknown_kind',
|
|
470
509
|
sourceType: 'channel',
|
|
471
510
|
conversationId: 'conv-1',
|
|
472
511
|
guardianExternalUserId: 'guardian-1',
|
|
512
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
473
513
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
474
514
|
});
|
|
475
515
|
|
|
@@ -485,7 +525,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
485
525
|
expect(resolved!.status).toBe('approved');
|
|
486
526
|
});
|
|
487
527
|
|
|
488
|
-
test('
|
|
528
|
+
test('desktop actor with matching principal mints scoped grant for approved canonical request', async () => {
|
|
489
529
|
const req = createCanonicalGuardianRequest({
|
|
490
530
|
kind: 'unknown_kind',
|
|
491
531
|
sourceType: 'voice',
|
|
@@ -494,6 +534,7 @@ describe('applyCanonicalGuardianDecision', () => {
|
|
|
494
534
|
callSessionId: 'call-voice-1',
|
|
495
535
|
toolName: 'host_bash',
|
|
496
536
|
inputDigest: 'sha256:voice-digest-1',
|
|
537
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
497
538
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
498
539
|
});
|
|
499
540
|
|
|
@@ -530,6 +571,7 @@ describe('mintCanonicalRequestGrant', () => {
|
|
|
530
571
|
sourceType: 'channel',
|
|
531
572
|
sourceChannel: 'telegram',
|
|
532
573
|
conversationId: 'conv-1',
|
|
574
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
533
575
|
toolName: 'shell',
|
|
534
576
|
inputDigest: 'sha256:abc',
|
|
535
577
|
});
|
|
@@ -549,6 +591,7 @@ describe('mintCanonicalRequestGrant', () => {
|
|
|
549
591
|
sourceType: 'channel',
|
|
550
592
|
sourceChannel: 'telegram',
|
|
551
593
|
conversationId: 'conv-2',
|
|
594
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
552
595
|
toolName: 'shell',
|
|
553
596
|
inputDigest: 'sha256:xyz',
|
|
554
597
|
});
|
|
@@ -570,6 +613,7 @@ describe('mintCanonicalRequestGrant', () => {
|
|
|
570
613
|
const req = createCanonicalGuardianRequest({
|
|
571
614
|
kind: 'pending_question',
|
|
572
615
|
sourceType: 'voice',
|
|
616
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
573
617
|
// No toolName or inputDigest
|
|
574
618
|
});
|
|
575
619
|
|
|
@@ -586,6 +630,7 @@ describe('mintCanonicalRequestGrant', () => {
|
|
|
586
630
|
const req = createCanonicalGuardianRequest({
|
|
587
631
|
kind: 'tool_approval',
|
|
588
632
|
sourceType: 'channel',
|
|
633
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
589
634
|
toolName: 'shell',
|
|
590
635
|
// No inputDigest
|
|
591
636
|
});
|
|
@@ -188,9 +188,9 @@ describe('guardian grant minting on tool-approval decisions', () => {
|
|
|
188
188
|
conversationId: 'guardian-conv-1',
|
|
189
189
|
callbackData: `apr:${requestId}:approve_once`,
|
|
190
190
|
content: '',
|
|
191
|
-
|
|
191
|
+
conversationExternalId: GUARDIAN_CHAT,
|
|
192
192
|
sourceChannel: 'telegram',
|
|
193
|
-
|
|
193
|
+
actorExternalId: GUARDIAN_USER,
|
|
194
194
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
195
195
|
guardianCtx: makeGuardianContext(),
|
|
196
196
|
assistantId: ASSISTANT_ID,
|
|
@@ -236,9 +236,9 @@ describe('guardian grant minting on tool-approval decisions', () => {
|
|
|
236
236
|
conversationId: 'guardian-conv-2',
|
|
237
237
|
callbackData: `apr:${requestId}:approve_once`,
|
|
238
238
|
content: '',
|
|
239
|
-
|
|
239
|
+
conversationExternalId: GUARDIAN_CHAT,
|
|
240
240
|
sourceChannel: 'telegram',
|
|
241
|
-
|
|
241
|
+
actorExternalId: GUARDIAN_USER,
|
|
242
242
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
243
243
|
guardianCtx: makeGuardianContext(),
|
|
244
244
|
assistantId: ASSISTANT_ID,
|
|
@@ -267,9 +267,9 @@ describe('guardian grant minting on tool-approval decisions', () => {
|
|
|
267
267
|
conversationId: 'guardian-conv-2b',
|
|
268
268
|
callbackData: `apr:${requestId}:approve_once`,
|
|
269
269
|
content: '',
|
|
270
|
-
|
|
270
|
+
conversationExternalId: GUARDIAN_CHAT,
|
|
271
271
|
sourceChannel: 'telegram',
|
|
272
|
-
|
|
272
|
+
actorExternalId: GUARDIAN_USER,
|
|
273
273
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
274
274
|
guardianCtx: makeGuardianContext(),
|
|
275
275
|
assistantId: ASSISTANT_ID,
|
|
@@ -306,9 +306,9 @@ describe('guardian grant minting on tool-approval decisions', () => {
|
|
|
306
306
|
conversationId: 'guardian-conv-3',
|
|
307
307
|
callbackData: `apr:${requestId}:reject`,
|
|
308
308
|
content: '',
|
|
309
|
-
|
|
309
|
+
conversationExternalId: GUARDIAN_CHAT,
|
|
310
310
|
sourceChannel: 'telegram',
|
|
311
|
-
|
|
311
|
+
actorExternalId: GUARDIAN_USER,
|
|
312
312
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
313
313
|
guardianCtx: makeGuardianContext(),
|
|
314
314
|
assistantId: ASSISTANT_ID,
|
|
@@ -335,9 +335,9 @@ describe('guardian grant minting on tool-approval decisions', () => {
|
|
|
335
335
|
conversationId: 'guardian-conv-4',
|
|
336
336
|
callbackData: `apr:${requestId}:approve_once`,
|
|
337
337
|
content: '',
|
|
338
|
-
|
|
338
|
+
conversationExternalId: GUARDIAN_CHAT,
|
|
339
339
|
sourceChannel: 'telegram',
|
|
340
|
-
|
|
340
|
+
actorExternalId: 'wrong-guardian-user',
|
|
341
341
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
342
342
|
guardianCtx: makeGuardianContext(),
|
|
343
343
|
assistantId: ASSISTANT_ID,
|
|
@@ -366,9 +366,9 @@ describe('guardian grant minting on tool-approval decisions', () => {
|
|
|
366
366
|
conversationId: 'guardian-conv-5',
|
|
367
367
|
callbackData: `apr:${requestId}:approve_once`,
|
|
368
368
|
content: '',
|
|
369
|
-
|
|
369
|
+
conversationExternalId: GUARDIAN_CHAT,
|
|
370
370
|
sourceChannel: 'telegram',
|
|
371
|
-
|
|
371
|
+
actorExternalId: GUARDIAN_USER,
|
|
372
372
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
373
373
|
guardianCtx: makeGuardianContext(),
|
|
374
374
|
assistantId: ASSISTANT_ID,
|
|
@@ -400,9 +400,9 @@ describe('guardian grant minting on tool-approval decisions', () => {
|
|
|
400
400
|
const result = await handleApprovalInterception({
|
|
401
401
|
conversationId: 'guardian-conv-6',
|
|
402
402
|
content: 'yes, approve it',
|
|
403
|
-
|
|
403
|
+
conversationExternalId: GUARDIAN_CHAT,
|
|
404
404
|
sourceChannel: 'telegram',
|
|
405
|
-
|
|
405
|
+
actorExternalId: GUARDIAN_USER,
|
|
406
406
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
407
407
|
guardianCtx: makeGuardianContext(),
|
|
408
408
|
assistantId: ASSISTANT_ID,
|
|
@@ -441,9 +441,9 @@ describe('guardian grant minting on tool-approval decisions', () => {
|
|
|
441
441
|
const result = await handleApprovalInterception({
|
|
442
442
|
conversationId: 'guardian-conv-7',
|
|
443
443
|
content: 'no, deny it',
|
|
444
|
-
|
|
444
|
+
conversationExternalId: GUARDIAN_CHAT,
|
|
445
445
|
sourceChannel: 'telegram',
|
|
446
|
-
|
|
446
|
+
actorExternalId: GUARDIAN_USER,
|
|
447
447
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
448
448
|
guardianCtx: makeGuardianContext(),
|
|
449
449
|
assistantId: ASSISTANT_ID,
|
|
@@ -471,9 +471,9 @@ describe('guardian grant minting on tool-approval decisions', () => {
|
|
|
471
471
|
const result = await handleApprovalInterception({
|
|
472
472
|
conversationId: 'guardian-conv-8',
|
|
473
473
|
content: 'yes',
|
|
474
|
-
|
|
474
|
+
conversationExternalId: GUARDIAN_CHAT,
|
|
475
475
|
sourceChannel: 'telegram',
|
|
476
|
-
|
|
476
|
+
actorExternalId: GUARDIAN_USER,
|
|
477
477
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
478
478
|
guardianCtx: makeGuardianContext(),
|
|
479
479
|
assistantId: ASSISTANT_ID,
|
|
@@ -507,9 +507,9 @@ describe('guardian grant minting on tool-approval decisions', () => {
|
|
|
507
507
|
conversationId: 'guardian-conv-9',
|
|
508
508
|
callbackData: `apr:${requestId}:approve_once`,
|
|
509
509
|
content: '',
|
|
510
|
-
|
|
510
|
+
conversationExternalId: GUARDIAN_CHAT,
|
|
511
511
|
sourceChannel: 'telegram',
|
|
512
|
-
|
|
512
|
+
actorExternalId: GUARDIAN_USER,
|
|
513
513
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
514
514
|
guardianCtx: makeGuardianContext(),
|
|
515
515
|
assistantId: ASSISTANT_ID,
|
|
@@ -550,9 +550,9 @@ describe('approval interception trust-class regression coverage', () => {
|
|
|
550
550
|
const result = await handleApprovalInterception({
|
|
551
551
|
conversationId: CONVERSATION_ID,
|
|
552
552
|
content: 'approve',
|
|
553
|
-
|
|
553
|
+
conversationExternalId: REQUESTER_CHAT,
|
|
554
554
|
sourceChannel: 'telegram',
|
|
555
|
-
|
|
555
|
+
actorExternalId: 'intruder-user-1',
|
|
556
556
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
557
557
|
guardianCtx: {
|
|
558
558
|
trustClass: 'unknown',
|
|
@@ -576,9 +576,9 @@ describe('approval interception trust-class regression coverage', () => {
|
|
|
576
576
|
const result = await handleApprovalInterception({
|
|
577
577
|
conversationId: CONVERSATION_ID,
|
|
578
578
|
content: 'approve',
|
|
579
|
-
|
|
579
|
+
conversationExternalId: REQUESTER_CHAT,
|
|
580
580
|
sourceChannel: 'telegram',
|
|
581
|
-
|
|
581
|
+
actorExternalId: undefined,
|
|
582
582
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
583
583
|
guardianCtx: {
|
|
584
584
|
trustClass: 'unknown',
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
6
|
+
|
|
7
|
+
const testDir = mkdtempSync(join(tmpdir(), 'guardian-principal-id-roundtrip-test-'));
|
|
8
|
+
|
|
9
|
+
mock.module('../util/platform.js', () => ({
|
|
10
|
+
getDataDir: () => testDir,
|
|
11
|
+
isMacOS: () => process.platform === 'darwin',
|
|
12
|
+
isLinux: () => process.platform === 'linux',
|
|
13
|
+
isWindows: () => process.platform === 'win32',
|
|
14
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
15
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
16
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
17
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
18
|
+
ensureDataDir: () => {},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
mock.module('../util/logger.js', () => ({
|
|
22
|
+
getLogger: () =>
|
|
23
|
+
new Proxy({} as Record<string, unknown>, {
|
|
24
|
+
get: () => () => {},
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
createCanonicalGuardianRequest,
|
|
30
|
+
getCanonicalGuardianRequest,
|
|
31
|
+
resolveCanonicalGuardianRequest,
|
|
32
|
+
updateCanonicalGuardianRequest,
|
|
33
|
+
} from '../memory/canonical-guardian-store.js';
|
|
34
|
+
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
35
|
+
import {
|
|
36
|
+
createBinding,
|
|
37
|
+
getActiveBinding,
|
|
38
|
+
} from '../memory/guardian-bindings.js';
|
|
39
|
+
|
|
40
|
+
initializeDb();
|
|
41
|
+
|
|
42
|
+
function resetTables(): void {
|
|
43
|
+
const db = getDb();
|
|
44
|
+
db.run('DELETE FROM canonical_guardian_deliveries');
|
|
45
|
+
db.run('DELETE FROM canonical_guardian_requests');
|
|
46
|
+
db.run('DELETE FROM channel_guardian_bindings');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('guardianPrincipalId roundtrip', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
resetTables();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterAll(() => {
|
|
55
|
+
resetDb();
|
|
56
|
+
try {
|
|
57
|
+
rmSync(testDir, { recursive: true });
|
|
58
|
+
} catch {
|
|
59
|
+
// best-effort cleanup
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ── channel_guardian_bindings ────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
describe('channel_guardian_bindings', () => {
|
|
66
|
+
test('creates binding with guardianPrincipalId and reads it back', () => {
|
|
67
|
+
const binding = createBinding({
|
|
68
|
+
assistantId: 'self',
|
|
69
|
+
channel: 'telegram',
|
|
70
|
+
guardianExternalUserId: 'tg-user-123',
|
|
71
|
+
guardianDeliveryChatId: 'tg-chat-123',
|
|
72
|
+
guardianPrincipalId: 'principal-abc-def',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(binding.guardianPrincipalId).toBe('principal-abc-def');
|
|
76
|
+
|
|
77
|
+
const fetched = getActiveBinding('self', 'telegram');
|
|
78
|
+
expect(fetched).not.toBeNull();
|
|
79
|
+
expect(fetched!.guardianPrincipalId).toBe('principal-abc-def');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('creates binding without guardianPrincipalId (defaults to null)', () => {
|
|
83
|
+
const binding = createBinding({
|
|
84
|
+
assistantId: 'self',
|
|
85
|
+
channel: 'sms',
|
|
86
|
+
guardianExternalUserId: 'sms-user-456',
|
|
87
|
+
guardianDeliveryChatId: 'sms-chat-456',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(binding.guardianPrincipalId).toBeNull();
|
|
91
|
+
|
|
92
|
+
const fetched = getActiveBinding('self', 'sms');
|
|
93
|
+
expect(fetched).not.toBeNull();
|
|
94
|
+
expect(fetched!.guardianPrincipalId).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── canonical_guardian_requests ──────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
describe('canonical_guardian_requests', () => {
|
|
101
|
+
test('creates request with guardianPrincipalId and reads it back', () => {
|
|
102
|
+
const req = createCanonicalGuardianRequest({
|
|
103
|
+
kind: 'tool_approval',
|
|
104
|
+
sourceType: 'channel',
|
|
105
|
+
sourceChannel: 'telegram',
|
|
106
|
+
guardianExternalUserId: 'guardian-tg-1',
|
|
107
|
+
guardianPrincipalId: 'principal-123',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(req.guardianPrincipalId).toBe('principal-123');
|
|
111
|
+
expect(req.decidedByPrincipalId).toBeNull();
|
|
112
|
+
|
|
113
|
+
const fetched = getCanonicalGuardianRequest(req.id);
|
|
114
|
+
expect(fetched).not.toBeNull();
|
|
115
|
+
expect(fetched!.guardianPrincipalId).toBe('principal-123');
|
|
116
|
+
expect(fetched!.decidedByPrincipalId).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('access_request requires guardianPrincipalId (decisionable kind)', () => {
|
|
120
|
+
// access_request is now decisionable — creating one without a principal
|
|
121
|
+
// should throw IntegrityError.
|
|
122
|
+
expect(() => createCanonicalGuardianRequest({
|
|
123
|
+
kind: 'access_request',
|
|
124
|
+
sourceType: 'desktop',
|
|
125
|
+
})).toThrow('guardianPrincipalId');
|
|
126
|
+
|
|
127
|
+
// With a principal, creation succeeds
|
|
128
|
+
const req = createCanonicalGuardianRequest({
|
|
129
|
+
kind: 'access_request',
|
|
130
|
+
sourceType: 'desktop',
|
|
131
|
+
guardianPrincipalId: 'access-req-principal',
|
|
132
|
+
});
|
|
133
|
+
expect(req.guardianPrincipalId).toBe('access-req-principal');
|
|
134
|
+
expect(req.decidedByPrincipalId).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('creates request with decidedByPrincipalId', () => {
|
|
138
|
+
const req = createCanonicalGuardianRequest({
|
|
139
|
+
kind: 'tool_approval',
|
|
140
|
+
sourceType: 'voice',
|
|
141
|
+
guardianPrincipalId: 'guardian-principal-1',
|
|
142
|
+
decidedByPrincipalId: 'decider-principal-1',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(req.decidedByPrincipalId).toBe('decider-principal-1');
|
|
146
|
+
expect(req.guardianPrincipalId).toBe('guardian-principal-1');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('updates decidedByPrincipalId via updateCanonicalGuardianRequest', () => {
|
|
150
|
+
const req = createCanonicalGuardianRequest({
|
|
151
|
+
kind: 'tool_approval',
|
|
152
|
+
sourceType: 'channel',
|
|
153
|
+
guardianPrincipalId: 'principal-for-update-test',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const updated = updateCanonicalGuardianRequest(req.id, {
|
|
157
|
+
status: 'approved',
|
|
158
|
+
decidedByPrincipalId: 'principal-decider-abc',
|
|
159
|
+
decidedByExternalUserId: 'ext-user-1',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(updated).not.toBeNull();
|
|
163
|
+
expect(updated!.decidedByPrincipalId).toBe('principal-decider-abc');
|
|
164
|
+
expect(updated!.decidedByExternalUserId).toBe('ext-user-1');
|
|
165
|
+
expect(updated!.status).toBe('approved');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('resolveCanonicalGuardianRequest writes decidedByPrincipalId', () => {
|
|
169
|
+
const req = createCanonicalGuardianRequest({
|
|
170
|
+
kind: 'tool_approval',
|
|
171
|
+
sourceType: 'voice',
|
|
172
|
+
guardianPrincipalId: 'guardian-principal-xyz',
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const resolved = resolveCanonicalGuardianRequest(req.id, 'pending', {
|
|
176
|
+
status: 'approved',
|
|
177
|
+
answerText: 'Approved',
|
|
178
|
+
decidedByExternalUserId: 'guardian-ext-1',
|
|
179
|
+
decidedByPrincipalId: 'guardian-principal-xyz',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(resolved).not.toBeNull();
|
|
183
|
+
expect(resolved!.status).toBe('approved');
|
|
184
|
+
expect(resolved!.decidedByPrincipalId).toBe('guardian-principal-xyz');
|
|
185
|
+
expect(resolved!.decidedByExternalUserId).toBe('guardian-ext-1');
|
|
186
|
+
expect(resolved!.guardianPrincipalId).toBe('guardian-principal-xyz');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('resolve without decidedByPrincipalId leaves it null', () => {
|
|
190
|
+
const req = createCanonicalGuardianRequest({
|
|
191
|
+
kind: 'tool_approval',
|
|
192
|
+
sourceType: 'channel',
|
|
193
|
+
guardianPrincipalId: 'principal-for-resolve-test',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const resolved = resolveCanonicalGuardianRequest(req.id, 'pending', {
|
|
197
|
+
status: 'denied',
|
|
198
|
+
decidedByExternalUserId: 'guardian-ext-2',
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(resolved).not.toBeNull();
|
|
202
|
+
expect(resolved!.decidedByPrincipalId).toBeNull();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|