@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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* its key architectural invariants:
|
|
6
6
|
*
|
|
7
7
|
* 1. All decision paths route through `applyCanonicalGuardianDecision`
|
|
8
|
-
* 2.
|
|
8
|
+
* 2. Principal-based authorization is enforced before decisions are applied
|
|
9
9
|
* 3. Stale/expired/already-resolved decisions are rejected
|
|
10
10
|
* 4. Code-only messages return clarification (not auto-approve)
|
|
11
11
|
* 5. Disambiguation with multiple pending requests stays fail-closed
|
|
@@ -88,11 +88,14 @@ afterAll(() => {
|
|
|
88
88
|
// Helpers
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
90
|
|
|
91
|
+
/** Consistent test principal used across all test actors and requests. */
|
|
92
|
+
const TEST_PRINCIPAL_ID = 'test-principal-id';
|
|
93
|
+
|
|
91
94
|
function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
|
|
92
95
|
return {
|
|
93
96
|
externalUserId: 'guardian-1',
|
|
94
97
|
channel: 'telegram',
|
|
95
|
-
|
|
98
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
96
99
|
...overrides,
|
|
97
100
|
};
|
|
98
101
|
}
|
|
@@ -101,7 +104,7 @@ function trustedActor(overrides: Partial<ActorContext> = {}): ActorContext {
|
|
|
101
104
|
return {
|
|
102
105
|
externalUserId: undefined,
|
|
103
106
|
channel: 'desktop',
|
|
104
|
-
|
|
107
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
105
108
|
...overrides,
|
|
106
109
|
};
|
|
107
110
|
}
|
|
@@ -223,25 +226,26 @@ describe('routing invariant: all decision paths reference applyCanonicalGuardian
|
|
|
223
226
|
});
|
|
224
227
|
|
|
225
228
|
// ===========================================================================
|
|
226
|
-
// SECTION 2:
|
|
229
|
+
// SECTION 2: Principal-based authorization invariants
|
|
227
230
|
// ===========================================================================
|
|
228
231
|
|
|
229
|
-
describe('routing invariant:
|
|
232
|
+
describe('routing invariant: principal-based authorization enforced before decisions', () => {
|
|
230
233
|
beforeEach(() => resetTables());
|
|
231
234
|
|
|
232
|
-
test('
|
|
235
|
+
test('mismatching actor principal is rejected by canonical primitive', async () => {
|
|
233
236
|
const req = createCanonicalGuardianRequest({
|
|
234
237
|
kind: 'tool_approval',
|
|
235
238
|
sourceType: 'channel',
|
|
236
239
|
conversationId: 'conv-1',
|
|
237
240
|
guardianExternalUserId: 'guardian-1',
|
|
241
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
238
242
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
239
243
|
});
|
|
240
244
|
|
|
241
245
|
const result = await applyCanonicalGuardianDecision({
|
|
242
246
|
requestId: req.id,
|
|
243
247
|
action: 'approve_once',
|
|
244
|
-
actorContext: guardianActor({
|
|
248
|
+
actorContext: guardianActor({ guardianPrincipalId: 'wrong-principal' }),
|
|
245
249
|
});
|
|
246
250
|
|
|
247
251
|
expect(result.applied).toBe(false);
|
|
@@ -253,12 +257,13 @@ describe('routing invariant: identity checks enforced before decisions', () => {
|
|
|
253
257
|
expect(unchanged!.status).toBe('pending');
|
|
254
258
|
});
|
|
255
259
|
|
|
256
|
-
test('
|
|
260
|
+
test('matching principal authorizes desktop actor', async () => {
|
|
257
261
|
const req = createCanonicalGuardianRequest({
|
|
258
262
|
kind: 'tool_approval',
|
|
259
263
|
sourceType: 'desktop',
|
|
260
264
|
conversationId: 'conv-1',
|
|
261
265
|
guardianExternalUserId: 'guardian-1',
|
|
266
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
262
267
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
263
268
|
});
|
|
264
269
|
|
|
@@ -271,19 +276,19 @@ describe('routing invariant: identity checks enforced before decisions', () => {
|
|
|
271
276
|
expect(result.applied).toBe(true);
|
|
272
277
|
});
|
|
273
278
|
|
|
274
|
-
test('
|
|
279
|
+
test('actor without guardianPrincipalId is rejected', async () => {
|
|
275
280
|
const req = createCanonicalGuardianRequest({
|
|
276
281
|
kind: 'tool_approval',
|
|
277
282
|
sourceType: 'channel',
|
|
278
283
|
conversationId: 'conv-1',
|
|
279
|
-
|
|
284
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
280
285
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
281
286
|
});
|
|
282
287
|
|
|
283
288
|
const result = await applyCanonicalGuardianDecision({
|
|
284
289
|
requestId: req.id,
|
|
285
290
|
action: 'approve_once',
|
|
286
|
-
actorContext: guardianActor({
|
|
291
|
+
actorContext: guardianActor({ guardianPrincipalId: undefined }),
|
|
287
292
|
});
|
|
288
293
|
|
|
289
294
|
expect(result.applied).toBe(false);
|
|
@@ -291,12 +296,13 @@ describe('routing invariant: identity checks enforced before decisions', () => {
|
|
|
291
296
|
expect(result.reason).toBe('identity_mismatch');
|
|
292
297
|
});
|
|
293
298
|
|
|
294
|
-
test('
|
|
299
|
+
test('principal mismatch on code-only message blocks detail leakage', async () => {
|
|
295
300
|
createCanonicalGuardianRequest({
|
|
296
301
|
kind: 'tool_approval',
|
|
297
302
|
sourceType: 'channel',
|
|
298
303
|
conversationId: 'conv-1',
|
|
299
304
|
guardianExternalUserId: 'guardian-1',
|
|
305
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
300
306
|
requestCode: 'ABC123',
|
|
301
307
|
toolName: 'shell',
|
|
302
308
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -304,7 +310,7 @@ describe('routing invariant: identity checks enforced before decisions', () => {
|
|
|
304
310
|
|
|
305
311
|
const result = await routeGuardianReply(replyCtx({
|
|
306
312
|
messageText: 'ABC123',
|
|
307
|
-
actor: guardianActor({
|
|
313
|
+
actor: guardianActor({ guardianPrincipalId: 'wrong-principal' }),
|
|
308
314
|
conversationId: 'conv-1',
|
|
309
315
|
}));
|
|
310
316
|
|
|
@@ -329,6 +335,7 @@ describe('routing invariant: stale/expired/already-resolved decisions rejected',
|
|
|
329
335
|
sourceType: 'channel',
|
|
330
336
|
conversationId: 'conv-1',
|
|
331
337
|
guardianExternalUserId: 'guardian-1',
|
|
338
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
332
339
|
expiresAt: new Date(Date.now() - 10_000).toISOString(), // already expired
|
|
333
340
|
});
|
|
334
341
|
|
|
@@ -349,6 +356,7 @@ describe('routing invariant: stale/expired/already-resolved decisions rejected',
|
|
|
349
356
|
sourceType: 'channel',
|
|
350
357
|
conversationId: 'conv-1',
|
|
351
358
|
guardianExternalUserId: 'guardian-1',
|
|
359
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
352
360
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
353
361
|
});
|
|
354
362
|
|
|
@@ -393,6 +401,7 @@ describe('routing invariant: stale/expired/already-resolved decisions rejected',
|
|
|
393
401
|
sourceType: 'channel',
|
|
394
402
|
conversationId: 'conv-1',
|
|
395
403
|
guardianExternalUserId: 'guardian-1',
|
|
404
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
396
405
|
requestCode: 'ABC123',
|
|
397
406
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
398
407
|
});
|
|
@@ -423,6 +432,7 @@ describe('routing invariant: stale/expired/already-resolved decisions rejected',
|
|
|
423
432
|
sourceType: 'channel',
|
|
424
433
|
conversationId: 'conv-1',
|
|
425
434
|
guardianExternalUserId: 'guardian-1',
|
|
435
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
426
436
|
expiresAt: new Date(Date.now() - 10_000).toISOString(), // already expired
|
|
427
437
|
});
|
|
428
438
|
|
|
@@ -451,6 +461,7 @@ describe('routing invariant: code-only messages return clarification', () => {
|
|
|
451
461
|
sourceType: 'channel',
|
|
452
462
|
conversationId: 'conv-1',
|
|
453
463
|
guardianExternalUserId: 'guardian-1',
|
|
464
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
454
465
|
requestCode: 'A1B2C3',
|
|
455
466
|
toolName: 'shell',
|
|
456
467
|
questionText: 'Run shell command: ls -la',
|
|
@@ -482,6 +493,7 @@ describe('routing invariant: code-only messages return clarification', () => {
|
|
|
482
493
|
sourceChannel: 'voice',
|
|
483
494
|
conversationId: 'conv-1',
|
|
484
495
|
guardianExternalUserId: 'guardian-1',
|
|
496
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
485
497
|
callSessionId: 'call-1',
|
|
486
498
|
pendingQuestionId: 'pq-1',
|
|
487
499
|
requestCode: 'A2B3C4',
|
|
@@ -513,6 +525,7 @@ describe('routing invariant: code-only messages return clarification', () => {
|
|
|
513
525
|
sourceChannel: 'voice',
|
|
514
526
|
conversationId: 'conv-1',
|
|
515
527
|
guardianExternalUserId: 'guardian-1',
|
|
528
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
516
529
|
callSessionId: 'call-2',
|
|
517
530
|
pendingQuestionId: 'pq-2',
|
|
518
531
|
requestCode: 'B2C3D4',
|
|
@@ -544,6 +557,7 @@ describe('routing invariant: code-only messages return clarification', () => {
|
|
|
544
557
|
sourceType: 'channel',
|
|
545
558
|
conversationId: 'conv-1',
|
|
546
559
|
guardianExternalUserId: 'guardian-1',
|
|
560
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
547
561
|
requestCode: 'A1B2C3',
|
|
548
562
|
toolName: 'shell',
|
|
549
563
|
inputDigest: 'sha256:abc',
|
|
@@ -570,6 +584,7 @@ describe('routing invariant: code-only messages return clarification', () => {
|
|
|
570
584
|
sourceType: 'channel',
|
|
571
585
|
conversationId: 'conv-1',
|
|
572
586
|
guardianExternalUserId: 'guardian-1',
|
|
587
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
573
588
|
requestCode: 'D4E5F6',
|
|
574
589
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
575
590
|
});
|
|
@@ -601,6 +616,7 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
601
616
|
sourceType: 'channel',
|
|
602
617
|
conversationId: 'conv-1',
|
|
603
618
|
guardianExternalUserId: 'guardian-1',
|
|
619
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
604
620
|
requestCode: 'DDD444',
|
|
605
621
|
toolName: 'shell',
|
|
606
622
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -628,6 +644,7 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
628
644
|
sourceType: 'channel',
|
|
629
645
|
conversationId: 'conv-1',
|
|
630
646
|
guardianExternalUserId: 'guardian-1',
|
|
647
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
631
648
|
requestCode: 'GGG777',
|
|
632
649
|
toolName: 'shell',
|
|
633
650
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -648,12 +665,13 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
648
665
|
expect(unchanged!.status).toBe('pending');
|
|
649
666
|
});
|
|
650
667
|
|
|
651
|
-
test('explicit empty pendingRequestIds hint stays fail-closed for
|
|
668
|
+
test('explicit empty pendingRequestIds hint stays fail-closed for desktop actors', async () => {
|
|
652
669
|
createCanonicalGuardianRequest({
|
|
653
670
|
kind: 'tool_approval',
|
|
654
671
|
sourceType: 'channel',
|
|
655
672
|
conversationId: 'conv-other',
|
|
656
673
|
guardianExternalUserId: 'guardian-1',
|
|
674
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
657
675
|
requestCode: 'HHH888',
|
|
658
676
|
toolName: 'shell',
|
|
659
677
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -678,6 +696,7 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
678
696
|
sourceType: 'channel',
|
|
679
697
|
conversationId: 'conv-1',
|
|
680
698
|
guardianExternalUserId: 'guardian-1',
|
|
699
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
681
700
|
requestCode: 'EEE555',
|
|
682
701
|
toolName: 'shell',
|
|
683
702
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -688,6 +707,7 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
688
707
|
sourceType: 'channel',
|
|
689
708
|
conversationId: 'conv-1',
|
|
690
709
|
guardianExternalUserId: 'guardian-1',
|
|
710
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
691
711
|
requestCode: 'FFF666',
|
|
692
712
|
toolName: 'file_write',
|
|
693
713
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -719,6 +739,7 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
719
739
|
sourceType: 'channel',
|
|
720
740
|
conversationId: 'conv-1',
|
|
721
741
|
guardianExternalUserId: 'guardian-1',
|
|
742
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
722
743
|
requestCode: 'AAA111',
|
|
723
744
|
toolName: 'shell',
|
|
724
745
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -729,6 +750,7 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
729
750
|
sourceType: 'channel',
|
|
730
751
|
conversationId: 'conv-1',
|
|
731
752
|
guardianExternalUserId: 'guardian-1',
|
|
753
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
732
754
|
requestCode: 'BBB222',
|
|
733
755
|
toolName: 'file_write',
|
|
734
756
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -771,6 +793,7 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
771
793
|
sourceChannel: 'voice',
|
|
772
794
|
conversationId: 'conv-1',
|
|
773
795
|
guardianExternalUserId: 'guardian-1',
|
|
796
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
774
797
|
callSessionId: 'call-answer',
|
|
775
798
|
pendingQuestionId: 'pq-answer',
|
|
776
799
|
requestCode: 'ABC123',
|
|
@@ -784,6 +807,7 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
784
807
|
sourceChannel: 'voice',
|
|
785
808
|
conversationId: 'conv-1',
|
|
786
809
|
guardianExternalUserId: 'guardian-1',
|
|
810
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
787
811
|
callSessionId: 'call-approval',
|
|
788
812
|
pendingQuestionId: 'pq-approval',
|
|
789
813
|
requestCode: 'DEF456',
|
|
@@ -815,6 +839,7 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
815
839
|
sourceType: 'channel',
|
|
816
840
|
conversationId: 'conv-1',
|
|
817
841
|
guardianExternalUserId: 'guardian-1',
|
|
842
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
818
843
|
requestCode: 'CCC333',
|
|
819
844
|
toolName: 'shell',
|
|
820
845
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -849,6 +874,7 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
849
874
|
sourceType: 'channel',
|
|
850
875
|
conversationId: 'conv-1',
|
|
851
876
|
guardianExternalUserId: 'guardian-1',
|
|
877
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
852
878
|
toolName: 'shell',
|
|
853
879
|
requestCode: 'GO1234',
|
|
854
880
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -876,6 +902,7 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
876
902
|
sourceType: 'channel',
|
|
877
903
|
conversationId: 'conv-1',
|
|
878
904
|
guardianExternalUserId: 'guardian-1',
|
|
905
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
879
906
|
requestCode: '111AAA',
|
|
880
907
|
toolName: 'shell',
|
|
881
908
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -885,6 +912,7 @@ describe('routing invariant: disambiguation stays fail-closed', () => {
|
|
|
885
912
|
sourceType: 'channel',
|
|
886
913
|
conversationId: 'conv-2',
|
|
887
914
|
guardianExternalUserId: 'guardian-1',
|
|
915
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
888
916
|
requestCode: '222BBB',
|
|
889
917
|
toolName: 'shell',
|
|
890
918
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -951,6 +979,7 @@ describe('routing invariant: approve_always downgraded for guardian-on-behalf',
|
|
|
951
979
|
sourceType: 'channel',
|
|
952
980
|
conversationId: 'conv-1',
|
|
953
981
|
guardianExternalUserId: 'guardian-1',
|
|
982
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
954
983
|
toolName: 'shell',
|
|
955
984
|
inputDigest: 'sha256:abc',
|
|
956
985
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -983,6 +1012,7 @@ describe('routing invariant: callback buttons route through canonical primitive'
|
|
|
983
1012
|
sourceType: 'channel',
|
|
984
1013
|
conversationId: 'conv-1',
|
|
985
1014
|
guardianExternalUserId: 'guardian-1',
|
|
1015
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
986
1016
|
toolName: 'shell',
|
|
987
1017
|
inputDigest: 'sha256:abc',
|
|
988
1018
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -1009,6 +1039,7 @@ describe('routing invariant: callback buttons route through canonical primitive'
|
|
|
1009
1039
|
sourceType: 'channel',
|
|
1010
1040
|
conversationId: 'conv-1',
|
|
1011
1041
|
guardianExternalUserId: 'guardian-1',
|
|
1042
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
1012
1043
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
1013
1044
|
});
|
|
1014
1045
|
registerPendingToolApprovalInteraction(req.id, 'conv-1', 'shell');
|
|
@@ -1032,6 +1063,7 @@ describe('routing invariant: callback buttons route through canonical primitive'
|
|
|
1032
1063
|
sourceType: 'channel',
|
|
1033
1064
|
conversationId: 'conv-other',
|
|
1034
1065
|
guardianExternalUserId: 'guardian-1',
|
|
1066
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
1035
1067
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
1036
1068
|
});
|
|
1037
1069
|
registerPendingToolApprovalInteraction(req.id, 'conv-other', 'shell');
|
|
@@ -1044,8 +1076,8 @@ describe('routing invariant: callback buttons route through canonical primitive'
|
|
|
1044
1076
|
|
|
1045
1077
|
// Should be consumed — conversationId scoping was removed because in
|
|
1046
1078
|
// cross-channel flows the guardian's conversation differs from the
|
|
1047
|
-
// requester's.
|
|
1048
|
-
//
|
|
1079
|
+
// requester's. Principal validation in the canonical decision primitive
|
|
1080
|
+
// is the correct security boundary.
|
|
1049
1081
|
expect(result.consumed).toBe(true);
|
|
1050
1082
|
expect(result.decisionApplied).toBe(true);
|
|
1051
1083
|
|
|
@@ -1056,14 +1088,14 @@ describe('routing invariant: callback buttons route through canonical primitive'
|
|
|
1056
1088
|
});
|
|
1057
1089
|
|
|
1058
1090
|
// ===========================================================================
|
|
1059
|
-
// SECTION 9: Destination hints do not bypass
|
|
1091
|
+
// SECTION 9: Destination hints do not bypass principal binding for tool_approval
|
|
1060
1092
|
// ===========================================================================
|
|
1061
1093
|
|
|
1062
|
-
describe('routing invariant: destination hints do not bypass tool_approval
|
|
1094
|
+
describe('routing invariant: destination hints do not bypass tool_approval principal binding', () => {
|
|
1063
1095
|
beforeEach(() => resetTables());
|
|
1064
1096
|
|
|
1065
|
-
test('explicit pendingRequestIds still fail closed when
|
|
1066
|
-
// Voice-originated tool approval with
|
|
1097
|
+
test('explicit pendingRequestIds still fail closed when guardianPrincipalId does not match', async () => {
|
|
1098
|
+
// Voice-originated tool approval with a different principal than the actor.
|
|
1067
1099
|
const req = createCanonicalGuardianRequest({
|
|
1068
1100
|
kind: 'tool_approval',
|
|
1069
1101
|
sourceType: 'voice',
|
|
@@ -1071,8 +1103,8 @@ describe('routing invariant: destination hints do not bypass tool_approval ident
|
|
|
1071
1103
|
conversationId: 'conv-voice-1',
|
|
1072
1104
|
toolName: 'shell',
|
|
1073
1105
|
requestCode: 'NL1234',
|
|
1106
|
+
guardianPrincipalId: 'request-principal',
|
|
1074
1107
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
1075
|
-
// guardianExternalUserId intentionally omitted
|
|
1076
1108
|
});
|
|
1077
1109
|
registerPendingToolApprovalInteraction(req.id, 'conv-voice-1', 'shell');
|
|
1078
1110
|
|
|
@@ -1081,7 +1113,7 @@ describe('routing invariant: destination hints do not bypass tool_approval ident
|
|
|
1081
1113
|
const result = await routeGuardianReply(replyCtx({
|
|
1082
1114
|
messageText: 'approve',
|
|
1083
1115
|
channel: 'telegram',
|
|
1084
|
-
actor: guardianActor({
|
|
1116
|
+
actor: guardianActor({ guardianPrincipalId: 'different-principal' }),
|
|
1085
1117
|
conversationId: 'conv-guardian-chat',
|
|
1086
1118
|
pendingRequestIds: [req.id],
|
|
1087
1119
|
approvalConversationGenerator: undefined,
|
|
@@ -1095,8 +1127,8 @@ describe('routing invariant: destination hints do not bypass tool_approval ident
|
|
|
1095
1127
|
expect(resolved!.status).toBe('pending');
|
|
1096
1128
|
});
|
|
1097
1129
|
|
|
1098
|
-
test('without destination hints,
|
|
1099
|
-
// Voice-originated request:
|
|
1130
|
+
test('without destination hints, unbound principal means no pending requests found', async () => {
|
|
1131
|
+
// Voice-originated request: different principal
|
|
1100
1132
|
const req = createCanonicalGuardianRequest({
|
|
1101
1133
|
kind: 'tool_approval',
|
|
1102
1134
|
sourceType: 'voice',
|
|
@@ -1104,6 +1136,7 @@ describe('routing invariant: destination hints do not bypass tool_approval ident
|
|
|
1104
1136
|
conversationId: 'conv-voice-2',
|
|
1105
1137
|
toolName: 'shell',
|
|
1106
1138
|
requestCode: 'NL5678',
|
|
1139
|
+
guardianPrincipalId: 'voice-principal',
|
|
1107
1140
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
1108
1141
|
});
|
|
1109
1142
|
|
|
@@ -1143,6 +1176,7 @@ describe('routing invariant: invite handoff bypass for access requests', () => {
|
|
|
1143
1176
|
sourceChannel: 'telegram',
|
|
1144
1177
|
conversationId: 'conv-access-1',
|
|
1145
1178
|
guardianExternalUserId: 'guardian-1',
|
|
1179
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
1146
1180
|
requestCode: 'INV001',
|
|
1147
1181
|
toolName: 'ingress_access_request',
|
|
1148
1182
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -1171,6 +1205,7 @@ describe('routing invariant: invite handoff bypass for access requests', () => {
|
|
|
1171
1205
|
sourceType: 'channel',
|
|
1172
1206
|
sourceChannel: 'telegram',
|
|
1173
1207
|
guardianExternalUserId: 'guardian-1',
|
|
1208
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
1174
1209
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
1175
1210
|
});
|
|
1176
1211
|
|
|
@@ -1193,6 +1228,7 @@ describe('routing invariant: invite handoff bypass for access requests', () => {
|
|
|
1193
1228
|
sourceType: 'channel',
|
|
1194
1229
|
conversationId: 'conv-1',
|
|
1195
1230
|
guardianExternalUserId: 'guardian-1',
|
|
1231
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
1196
1232
|
requestCode: 'TAP001',
|
|
1197
1233
|
toolName: 'shell',
|
|
1198
1234
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -1219,6 +1255,7 @@ describe('routing invariant: invite handoff bypass for access requests', () => {
|
|
|
1219
1255
|
sourceChannel: 'telegram',
|
|
1220
1256
|
conversationId: 'conv-access-2',
|
|
1221
1257
|
guardianExternalUserId: 'guardian-1',
|
|
1258
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
1222
1259
|
requestCode: 'A00B01',
|
|
1223
1260
|
toolName: 'ingress_access_request',
|
|
1224
1261
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -1239,13 +1276,14 @@ describe('routing invariant: invite handoff bypass for access requests', () => {
|
|
|
1239
1276
|
expect(resolved!.status).toBe('approved');
|
|
1240
1277
|
});
|
|
1241
1278
|
|
|
1242
|
-
test('
|
|
1279
|
+
test('desktop access-request approval returns a verification code reply', async () => {
|
|
1243
1280
|
const req = createCanonicalGuardianRequest({
|
|
1244
1281
|
kind: 'access_request',
|
|
1245
1282
|
sourceType: 'channel',
|
|
1246
1283
|
sourceChannel: 'telegram',
|
|
1247
1284
|
conversationId: 'conv-access-desktop',
|
|
1248
1285
|
guardianExternalUserId: 'guardian-1',
|
|
1286
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
1249
1287
|
requestCode: 'C0D3A5',
|
|
1250
1288
|
toolName: 'ingress_access_request',
|
|
1251
1289
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
@@ -1276,6 +1314,7 @@ describe('routing invariant: invite handoff bypass for access requests', () => {
|
|
|
1276
1314
|
sourceChannel: 'telegram',
|
|
1277
1315
|
conversationId: 'conv-access-desktop-nl',
|
|
1278
1316
|
guardianExternalUserId: 'guardian-1',
|
|
1317
|
+
guardianPrincipalId: TEST_PRINCIPAL_ID,
|
|
1279
1318
|
requesterExternalUserId: 'requester-1',
|
|
1280
1319
|
requesterChatId: 'chat-1',
|
|
1281
1320
|
requestCode: 'A1B2C3',
|
|
@@ -211,10 +211,10 @@ describe('inbound-message-handler trusted-contact interactivity', () => {
|
|
|
211
211
|
body: JSON.stringify({
|
|
212
212
|
sourceChannel: 'telegram',
|
|
213
213
|
interface: 'telegram',
|
|
214
|
-
|
|
214
|
+
conversationExternalId: 'chat-123',
|
|
215
215
|
externalMessageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
216
216
|
content: 'hello',
|
|
217
|
-
|
|
217
|
+
actorExternalId: 'telegram-user-default',
|
|
218
218
|
replyCallbackUrl: 'https://gateway.test/deliver/telegram',
|
|
219
219
|
...overrides,
|
|
220
220
|
}),
|
|
@@ -374,8 +374,8 @@ describe('inbound-message-handler trusted-contact interactivity', () => {
|
|
|
374
374
|
|
|
375
375
|
const req = makeInboundRequest({
|
|
376
376
|
externalMessageId: `msg-unknown-${Date.now()}`,
|
|
377
|
-
// No
|
|
378
|
-
|
|
377
|
+
// No actorExternalId => unknown trust class
|
|
378
|
+
actorExternalId: undefined,
|
|
379
379
|
});
|
|
380
380
|
|
|
381
381
|
const res = await handleChannelInbound(req, processMessage as any, 'test-token');
|
|
@@ -213,7 +213,7 @@ describe('handleUserMessage pending-confirmation reply interception', () => {
|
|
|
213
213
|
assistantMessageChannel: 'vellum',
|
|
214
214
|
userMessageInterface: 'macos',
|
|
215
215
|
assistantMessageInterface: 'macos',
|
|
216
|
-
|
|
216
|
+
provenanceTrustClass: 'guardian',
|
|
217
217
|
}),
|
|
218
218
|
);
|
|
219
219
|
expect(addMessageMock).toHaveBeenCalledWith(
|
|
@@ -225,7 +225,7 @@ describe('handleUserMessage pending-confirmation reply interception', () => {
|
|
|
225
225
|
assistantMessageChannel: 'vellum',
|
|
226
226
|
userMessageInterface: 'macos',
|
|
227
227
|
assistantMessageInterface: 'macos',
|
|
228
|
-
|
|
228
|
+
provenanceTrustClass: 'guardian',
|
|
229
229
|
}),
|
|
230
230
|
);
|
|
231
231
|
expect(sent.map((msg) => msg.type)).toEqual([
|
|
@@ -118,12 +118,12 @@ function buildInboundRequest(overrides: Record<string, unknown> = {}): Request {
|
|
|
118
118
|
const body: Record<string, unknown> = {
|
|
119
119
|
sourceChannel: 'telegram',
|
|
120
120
|
interface: 'telegram',
|
|
121
|
-
|
|
121
|
+
conversationExternalId: 'chat-invite-test',
|
|
122
122
|
externalMessageId: `msg-invite-${Date.now()}-${msgCounter}`,
|
|
123
123
|
content: '/start iv_sometoken',
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
actorExternalId: 'user-invite-123',
|
|
125
|
+
actorDisplayName: 'Invite User',
|
|
126
|
+
actorUsername: 'invite_user',
|
|
127
127
|
replyCallbackUrl: 'http://localhost:7830/deliver/telegram',
|
|
128
128
|
sourceMetadata: {
|
|
129
129
|
commandIntent: { type: 'start', payload: 'iv_sometoken' },
|
|
@@ -309,8 +309,8 @@ describe('inbound invite redemption intercept', () => {
|
|
|
309
309
|
// Active member sends a normal message (no invite token)
|
|
310
310
|
const req = buildInboundRequest({
|
|
311
311
|
content: 'Hello, just a normal message!',
|
|
312
|
-
|
|
313
|
-
|
|
312
|
+
actorExternalId: 'user-active-member',
|
|
313
|
+
conversationExternalId: 'chat-active',
|
|
314
314
|
sourceMetadata: {},
|
|
315
315
|
});
|
|
316
316
|
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
@@ -353,7 +353,7 @@ describe('inbound invite redemption intercept', () => {
|
|
|
353
353
|
});
|
|
354
354
|
|
|
355
355
|
const req = buildInviteRequest(rawToken, {
|
|
356
|
-
|
|
356
|
+
actorExternalId: 'user-already-active',
|
|
357
357
|
});
|
|
358
358
|
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
359
359
|
const json = await resp.json() as Record<string, unknown>;
|
|
@@ -378,7 +378,7 @@ describe('inbound invite redemption intercept', () => {
|
|
|
378
378
|
});
|
|
379
379
|
|
|
380
380
|
const req = buildInviteRequest(rawToken, {
|
|
381
|
-
|
|
381
|
+
actorDisplayName: 'Noa Flaherty',
|
|
382
382
|
});
|
|
383
383
|
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
384
384
|
const json = await resp.json() as Record<string, unknown>;
|
|
@@ -31,16 +31,13 @@ mock.module('../util/logger.js', () => ({
|
|
|
31
31
|
}),
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
let semanticSearchDelayMs = 0;
|
|
34
|
+
// Counter for semantic search invocations — used to verify early termination
|
|
35
|
+
// skips the call entirely rather than relying on flaky wall-clock comparisons.
|
|
36
|
+
let semanticSearchCallCount = 0;
|
|
38
37
|
|
|
39
38
|
mock.module('../memory/search/semantic.js', () => ({
|
|
40
39
|
semanticSearch: async () => {
|
|
41
|
-
|
|
42
|
-
await Bun.sleep(semanticSearchDelayMs);
|
|
43
|
-
}
|
|
40
|
+
semanticSearchCallCount++;
|
|
44
41
|
return [];
|
|
45
42
|
},
|
|
46
43
|
isQdrantConnectionError: () => false,
|
|
@@ -305,15 +302,11 @@ describe('Memory retrieval benchmark', () => {
|
|
|
305
302
|
expect(recall.selectedCount).toBeGreaterThan(0);
|
|
306
303
|
});
|
|
307
304
|
|
|
308
|
-
test('early termination
|
|
309
|
-
const conversationId = 'conv-bench-et-
|
|
305
|
+
test('early termination skips semantic search entirely', async () => {
|
|
306
|
+
const conversationId = 'conv-bench-et-skip';
|
|
310
307
|
const now = 1_700_500_000_000;
|
|
311
308
|
seedMemoryItems(conversationId, 500, now);
|
|
312
309
|
|
|
313
|
-
// Simulate the Qdrant network round-trip that ET is designed to skip.
|
|
314
|
-
// Use 100ms to dominate over variable CPU-bound work on slower hosts.
|
|
315
|
-
semanticSearchDelayMs = 100;
|
|
316
|
-
|
|
317
310
|
const query = 'What do we know about topic-5 and keyword-3?';
|
|
318
311
|
|
|
319
312
|
const etConfig: AssistantConfig = {
|
|
@@ -363,40 +356,22 @@ describe('Memory retrieval benchmark', () => {
|
|
|
363
356
|
},
|
|
364
357
|
};
|
|
365
358
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const baselineRecall = await buildMemoryRecall(query, conversationId, noEtConfig);
|
|
383
|
-
baselineTimes.push(performance.now() - t1);
|
|
384
|
-
expect(baselineRecall.earlyTerminated).toBe(false);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
etTimes.sort((a, b) => a - b);
|
|
388
|
-
baselineTimes.sort((a, b) => a - b);
|
|
389
|
-
const medianEt = etTimes[Math.floor(iterations / 2)];
|
|
390
|
-
const medianBaseline = baselineTimes[Math.floor(iterations / 2)];
|
|
391
|
-
|
|
392
|
-
// ET skips the mocked network delay, so it should be measurably faster.
|
|
393
|
-
// Use a 15% threshold to tolerate slower CI hosts where CPU-bound work
|
|
394
|
-
// takes longer relative to the fixed mock delay.
|
|
395
|
-
const speedup = 1 - medianEt / medianBaseline;
|
|
396
|
-
expect(speedup).toBeGreaterThanOrEqual(0.15);
|
|
397
|
-
} finally {
|
|
398
|
-
semanticSearchDelayMs = 0;
|
|
399
|
-
}
|
|
359
|
+
// Run with ET enabled — semantic search should be skipped
|
|
360
|
+
semanticSearchCallCount = 0;
|
|
361
|
+
const etRecall = await buildMemoryRecall(query, conversationId, etConfig);
|
|
362
|
+
const etCalls = semanticSearchCallCount;
|
|
363
|
+
|
|
364
|
+
expect(etRecall.earlyTerminated).toBe(true);
|
|
365
|
+
expect(etRecall.semanticHits).toBe(0);
|
|
366
|
+
expect(etCalls).toBe(0);
|
|
367
|
+
|
|
368
|
+
// Run without ET — semantic search should be invoked
|
|
369
|
+
semanticSearchCallCount = 0;
|
|
370
|
+
const baselineRecall = await buildMemoryRecall(query, conversationId, noEtConfig);
|
|
371
|
+
const baselineCalls = semanticSearchCallCount;
|
|
372
|
+
|
|
373
|
+
expect(baselineRecall.earlyTerminated).toBe(false);
|
|
374
|
+
expect(baselineCalls).toBeGreaterThan(0);
|
|
400
375
|
});
|
|
401
376
|
|
|
402
377
|
test('recall.latencyMs tracks wall-clock within 50% tolerance', async () => {
|