@vellumai/assistant 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/ARCHITECTURE.md +27 -10
  2. package/README.md +6 -6
  3. package/bun.lock +57 -2
  4. package/docs/architecture/memory.md +4 -4
  5. package/docs/trusted-contact-access.md +8 -0
  6. package/package.json +3 -2
  7. package/src/__tests__/actor-token-service.test.ts +9 -6
  8. package/src/__tests__/assistant-feature-flags-integration.test.ts +1 -0
  9. package/src/__tests__/call-controller.test.ts +115 -0
  10. package/src/__tests__/call-domain.test.ts +148 -10
  11. package/src/__tests__/call-pointer-message-composer.test.ts +39 -49
  12. package/src/__tests__/call-pointer-messages.test.ts +105 -43
  13. package/src/__tests__/canonical-guardian-store.test.ts +44 -10
  14. package/src/__tests__/channel-approval-routes.test.ts +67 -65
  15. package/src/__tests__/channel-delivery-store.test.ts +2 -2
  16. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +1 -0
  17. package/src/__tests__/conversation-attention-telegram.test.ts +2 -2
  18. package/src/__tests__/deterministic-verification-control-plane.test.ts +6 -6
  19. package/src/__tests__/gateway-client-managed-outbound.test.ts +147 -0
  20. package/src/__tests__/guardian-actions-endpoint.test.ts +7 -6
  21. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +57 -12
  22. package/src/__tests__/guardian-dispatch.test.ts +39 -1
  23. package/src/__tests__/guardian-grant-minting.test.ts +24 -24
  24. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +205 -0
  25. package/src/__tests__/guardian-routing-invariants.test.ts +64 -25
  26. package/src/__tests__/guardian-routing-state.test.ts +10 -32
  27. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -2
  28. package/src/__tests__/inbound-invite-redemption.test.ts +8 -8
  29. package/src/__tests__/memory-retrieval.benchmark.test.ts +22 -47
  30. package/src/__tests__/no-is-trusted-guard.test.ts +77 -0
  31. package/src/__tests__/non-member-access-request.test.ts +57 -47
  32. package/src/__tests__/notification-decision-fallback.test.ts +232 -0
  33. package/src/__tests__/notification-decision-strategy.test.ts +304 -8
  34. package/src/__tests__/notification-guardian-path.test.ts +38 -1
  35. package/src/__tests__/relay-server.test.ts +136 -5
  36. package/src/__tests__/send-endpoint-busy.test.ts +35 -1
  37. package/src/__tests__/session-tool-setup-tools-disabled.test.ts +155 -0
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -0
  39. package/src/__tests__/skill-projection.benchmark.test.ts +66 -2
  40. package/src/__tests__/system-prompt.test.ts +1 -0
  41. package/src/__tests__/tool-approval-handler.test.ts +1 -1
  42. package/src/__tests__/tool-grant-request-escalation.test.ts +10 -2
  43. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +14 -1
  44. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +24 -24
  45. package/src/__tests__/trusted-contact-multichannel.test.ts +5 -5
  46. package/src/__tests__/trusted-contact-verification.test.ts +10 -10
  47. package/src/approvals/guardian-decision-primitive.ts +29 -25
  48. package/src/approvals/guardian-request-resolvers.ts +9 -5
  49. package/src/calls/call-controller.ts +15 -0
  50. package/src/calls/call-pointer-message-composer.ts +27 -85
  51. package/src/calls/call-pointer-messages.ts +54 -21
  52. package/src/calls/guardian-dispatch.ts +30 -0
  53. package/src/calls/relay-server.ts +58 -24
  54. package/src/calls/types.ts +1 -0
  55. package/src/config/system-prompt.ts +10 -3
  56. package/src/config/templates/BOOTSTRAP.md +6 -5
  57. package/src/config/templates/USER.md +1 -0
  58. package/src/config/user-reference.ts +44 -0
  59. package/src/daemon/handlers/guardian-actions.ts +5 -2
  60. package/src/daemon/handlers/sessions.ts +8 -3
  61. package/src/daemon/lifecycle.ts +109 -3
  62. package/src/daemon/providers-setup.ts +0 -8
  63. package/src/daemon/server.ts +32 -24
  64. package/src/daemon/session-agent-loop.ts +4 -3
  65. package/src/daemon/session-lifecycle.ts +1 -9
  66. package/src/daemon/session-process.ts +2 -2
  67. package/src/daemon/session-runtime-assembly.ts +2 -0
  68. package/src/daemon/session-slash.ts +35 -2
  69. package/src/daemon/session-tool-setup.ts +10 -0
  70. package/src/daemon/session.ts +1 -0
  71. package/src/memory/canonical-guardian-store.ts +40 -0
  72. package/src/memory/conversation-crud.ts +26 -0
  73. package/src/memory/conversation-store.ts +1 -0
  74. package/src/memory/db-init.ts +12 -0
  75. package/src/memory/guardian-bindings.ts +4 -0
  76. package/src/memory/job-handlers/backfill.ts +2 -9
  77. package/src/memory/migrations/039-actor-refresh-token-records.ts +51 -0
  78. package/src/memory/migrations/125-guardian-principal-id-columns.ts +19 -0
  79. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +210 -0
  80. package/src/memory/migrations/index.ts +3 -0
  81. package/src/memory/migrations/registry.ts +5 -0
  82. package/src/memory/schema.ts +22 -0
  83. package/src/notifications/README.md +8 -1
  84. package/src/notifications/copy-composer.ts +160 -30
  85. package/src/notifications/decision-engine.ts +98 -1
  86. package/src/runtime/access-request-helper.ts +43 -28
  87. package/src/runtime/actor-refresh-token-service.ts +309 -0
  88. package/src/runtime/actor-refresh-token-store.ts +157 -0
  89. package/src/runtime/actor-token-service.ts +3 -3
  90. package/src/runtime/actor-trust-resolver.ts +19 -14
  91. package/src/runtime/channel-guardian-service.ts +6 -0
  92. package/src/runtime/gateway-client.ts +239 -0
  93. package/src/runtime/guardian-context-resolver.ts +6 -2
  94. package/src/runtime/guardian-reply-router.ts +33 -16
  95. package/src/runtime/guardian-vellum-migration.ts +29 -5
  96. package/src/runtime/http-server.ts +2 -0
  97. package/src/runtime/http-types.ts +0 -13
  98. package/src/runtime/local-actor-identity.ts +19 -13
  99. package/src/runtime/middleware/actor-token.ts +2 -2
  100. package/src/runtime/routes/channel-delivery-routes.ts +5 -5
  101. package/src/runtime/routes/conversation-routes.ts +45 -35
  102. package/src/runtime/routes/guardian-action-routes.ts +7 -1
  103. package/src/runtime/routes/guardian-approval-interception.ts +52 -52
  104. package/src/runtime/routes/guardian-bootstrap-routes.ts +11 -24
  105. package/src/runtime/routes/guardian-refresh-routes.ts +53 -0
  106. package/src/runtime/routes/inbound-conversation.ts +7 -7
  107. package/src/runtime/routes/inbound-message-handler.ts +105 -94
  108. package/src/runtime/routes/pairing-routes.ts +60 -50
  109. package/src/runtime/tool-grant-request-helper.ts +1 -0
  110. package/src/types/qrcode.d.ts +10 -0
  111. package/src/util/logger.ts +10 -0
  112. package/src/daemon/call-pointer-generators.ts +0 -59
@@ -41,6 +41,10 @@ import { getDb, initializeDb, resetDb } from '../memory/db.js';
41
41
 
42
42
  initializeDb();
43
43
 
44
+ // All decisionable kinds (tool_approval, pending_question, access_request)
45
+ // require a guardianPrincipalId. Use a constant for test fixtures.
46
+ const TEST_PRINCIPAL = 'test-principal-id';
47
+
44
48
  function resetTables(): void {
45
49
  const db = getDb();
46
50
  db.run('DELETE FROM canonical_guardian_deliveries');
@@ -71,6 +75,7 @@ describe('canonical-guardian-store', () => {
71
75
  conversationId: 'conv-1',
72
76
  requesterExternalUserId: 'user-1',
73
77
  guardianExternalUserId: 'guardian-1',
78
+ guardianPrincipalId: TEST_PRINCIPAL,
74
79
  callSessionId: 'session-1',
75
80
  pendingQuestionId: 'pq-1',
76
81
  questionText: 'Can I run this tool?',
@@ -94,6 +99,7 @@ describe('canonical-guardian-store', () => {
94
99
  const req = createCanonicalGuardianRequest({
95
100
  kind: 'access_request',
96
101
  sourceType: 'channel',
102
+ guardianPrincipalId: TEST_PRINCIPAL,
97
103
  });
98
104
 
99
105
  expect(req.id).toBeTruthy();
@@ -111,6 +117,7 @@ describe('canonical-guardian-store', () => {
111
117
  const created = createCanonicalGuardianRequest({
112
118
  kind: 'tool_approval',
113
119
  sourceType: 'voice',
120
+ guardianPrincipalId: TEST_PRINCIPAL,
114
121
  });
115
122
 
116
123
  const fetched = getCanonicalGuardianRequest(created.id);
@@ -127,16 +134,16 @@ describe('canonical-guardian-store', () => {
127
134
  // ── listCanonicalGuardianRequests ─────────────────────────────────
128
135
 
129
136
  test('lists all requests with no filters', () => {
130
- createCanonicalGuardianRequest({ kind: 'tool_approval', sourceType: 'voice' });
131
- createCanonicalGuardianRequest({ kind: 'access_request', sourceType: 'channel' });
137
+ createCanonicalGuardianRequest({ kind: 'tool_approval', sourceType: 'voice', guardianPrincipalId: TEST_PRINCIPAL });
138
+ createCanonicalGuardianRequest({ kind: 'access_request', sourceType: 'channel', guardianPrincipalId: TEST_PRINCIPAL });
132
139
 
133
140
  const all = listCanonicalGuardianRequests();
134
141
  expect(all).toHaveLength(2);
135
142
  });
136
143
 
137
144
  test('filters by status', () => {
138
- createCanonicalGuardianRequest({ kind: 'tool_approval', sourceType: 'voice' });
139
- const req2 = createCanonicalGuardianRequest({ kind: 'access_request', sourceType: 'channel' });
145
+ createCanonicalGuardianRequest({ kind: 'tool_approval', sourceType: 'voice', guardianPrincipalId: TEST_PRINCIPAL });
146
+ const req2 = createCanonicalGuardianRequest({ kind: 'access_request', sourceType: 'channel', guardianPrincipalId: TEST_PRINCIPAL });
140
147
  updateCanonicalGuardianRequest(req2.id, { status: 'approved' });
141
148
 
142
149
  const pending = listCanonicalGuardianRequests({ status: 'pending' });
@@ -153,11 +160,13 @@ describe('canonical-guardian-store', () => {
153
160
  kind: 'tool_approval',
154
161
  sourceType: 'voice',
155
162
  guardianExternalUserId: 'guardian-A',
163
+ guardianPrincipalId: TEST_PRINCIPAL,
156
164
  });
157
165
  createCanonicalGuardianRequest({
158
166
  kind: 'tool_approval',
159
167
  sourceType: 'voice',
160
168
  guardianExternalUserId: 'guardian-B',
169
+ guardianPrincipalId: TEST_PRINCIPAL,
161
170
  });
162
171
 
163
172
  const filtered = listCanonicalGuardianRequests({ guardianExternalUserId: 'guardian-A' });
@@ -170,11 +179,13 @@ describe('canonical-guardian-store', () => {
170
179
  kind: 'tool_approval',
171
180
  sourceType: 'voice',
172
181
  conversationId: 'conv-X',
182
+ guardianPrincipalId: TEST_PRINCIPAL,
173
183
  });
174
184
  createCanonicalGuardianRequest({
175
185
  kind: 'tool_approval',
176
186
  sourceType: 'voice',
177
187
  conversationId: 'conv-Y',
188
+ guardianPrincipalId: TEST_PRINCIPAL,
178
189
  });
179
190
 
180
191
  const filtered = listCanonicalGuardianRequests({ conversationId: 'conv-X' });
@@ -182,18 +193,18 @@ describe('canonical-guardian-store', () => {
182
193
  });
183
194
 
184
195
  test('filters by sourceType', () => {
185
- createCanonicalGuardianRequest({ kind: 'tool_approval', sourceType: 'voice' });
186
- createCanonicalGuardianRequest({ kind: 'tool_approval', sourceType: 'channel' });
187
- createCanonicalGuardianRequest({ kind: 'tool_approval', sourceType: 'desktop' });
196
+ createCanonicalGuardianRequest({ kind: 'tool_approval', sourceType: 'voice', guardianPrincipalId: TEST_PRINCIPAL });
197
+ createCanonicalGuardianRequest({ kind: 'tool_approval', sourceType: 'channel', guardianPrincipalId: TEST_PRINCIPAL });
198
+ createCanonicalGuardianRequest({ kind: 'tool_approval', sourceType: 'desktop', guardianPrincipalId: TEST_PRINCIPAL });
188
199
 
189
200
  const voiceOnly = listCanonicalGuardianRequests({ sourceType: 'voice' });
190
201
  expect(voiceOnly).toHaveLength(1);
191
202
  });
192
203
 
193
204
  test('filters by kind', () => {
194
- createCanonicalGuardianRequest({ kind: 'tool_approval', sourceType: 'voice' });
195
- createCanonicalGuardianRequest({ kind: 'pending_question', sourceType: 'voice' });
196
- createCanonicalGuardianRequest({ kind: 'access_request', sourceType: 'channel' });
205
+ createCanonicalGuardianRequest({ kind: 'tool_approval', sourceType: 'voice', guardianPrincipalId: TEST_PRINCIPAL });
206
+ createCanonicalGuardianRequest({ kind: 'pending_question', sourceType: 'voice', guardianPrincipalId: TEST_PRINCIPAL });
207
+ createCanonicalGuardianRequest({ kind: 'access_request', sourceType: 'channel', guardianPrincipalId: TEST_PRINCIPAL });
197
208
 
198
209
  const toolOnly = listCanonicalGuardianRequests({ kind: 'tool_approval' });
199
210
  expect(toolOnly).toHaveLength(1);
@@ -204,16 +215,19 @@ describe('canonical-guardian-store', () => {
204
215
  kind: 'tool_approval',
205
216
  sourceType: 'voice',
206
217
  guardianExternalUserId: 'guardian-A',
218
+ guardianPrincipalId: TEST_PRINCIPAL,
207
219
  });
208
220
  createCanonicalGuardianRequest({
209
221
  kind: 'tool_approval',
210
222
  sourceType: 'channel',
211
223
  guardianExternalUserId: 'guardian-A',
224
+ guardianPrincipalId: TEST_PRINCIPAL,
212
225
  });
213
226
  createCanonicalGuardianRequest({
214
227
  kind: 'access_request',
215
228
  sourceType: 'voice',
216
229
  guardianExternalUserId: 'guardian-A',
230
+ guardianPrincipalId: TEST_PRINCIPAL,
217
231
  });
218
232
 
219
233
  const filtered = listCanonicalGuardianRequests({
@@ -230,6 +244,7 @@ describe('canonical-guardian-store', () => {
230
244
  const req = createCanonicalGuardianRequest({
231
245
  kind: 'tool_approval',
232
246
  sourceType: 'voice',
247
+ guardianPrincipalId: TEST_PRINCIPAL,
233
248
  });
234
249
 
235
250
  const updated = updateCanonicalGuardianRequest(req.id, {
@@ -260,6 +275,7 @@ describe('canonical-guardian-store', () => {
260
275
  const req = createCanonicalGuardianRequest({
261
276
  kind: 'tool_approval',
262
277
  sourceType: 'voice',
278
+ guardianPrincipalId: TEST_PRINCIPAL,
263
279
  });
264
280
 
265
281
  const resolved = resolveCanonicalGuardianRequest(req.id, 'pending', {
@@ -278,6 +294,7 @@ describe('canonical-guardian-store', () => {
278
294
  const req = createCanonicalGuardianRequest({
279
295
  kind: 'tool_approval',
280
296
  sourceType: 'channel',
297
+ guardianPrincipalId: TEST_PRINCIPAL,
281
298
  });
282
299
 
283
300
  const resolved = resolveCanonicalGuardianRequest(req.id, 'pending', {
@@ -293,6 +310,7 @@ describe('canonical-guardian-store', () => {
293
310
  const req = createCanonicalGuardianRequest({
294
311
  kind: 'tool_approval',
295
312
  sourceType: 'voice',
313
+ guardianPrincipalId: TEST_PRINCIPAL,
296
314
  });
297
315
 
298
316
  // Try to resolve with wrong expected status
@@ -311,6 +329,7 @@ describe('canonical-guardian-store', () => {
311
329
  const req = createCanonicalGuardianRequest({
312
330
  kind: 'tool_approval',
313
331
  sourceType: 'voice',
332
+ guardianPrincipalId: TEST_PRINCIPAL,
314
333
  });
315
334
 
316
335
  // First resolve succeeds
@@ -353,6 +372,7 @@ describe('canonical-guardian-store', () => {
353
372
  sourceChannel: 'twilio',
354
373
  conversationId: 'conv-voice-1',
355
374
  guardianExternalUserId: 'guardian-phone',
375
+ guardianPrincipalId: TEST_PRINCIPAL,
356
376
  callSessionId: 'call-123',
357
377
  pendingQuestionId: 'pq-456',
358
378
  questionText: 'What is the gate code?',
@@ -374,6 +394,7 @@ describe('canonical-guardian-store', () => {
374
394
  conversationId: 'conv-tg-1',
375
395
  requesterExternalUserId: 'requester-tg-user',
376
396
  guardianExternalUserId: 'guardian-tg-user',
397
+ guardianPrincipalId: TEST_PRINCIPAL,
377
398
  toolName: 'execute_code',
378
399
  inputDigest: 'sha256:abcdef',
379
400
  expiresAt: new Date(Date.now() + 120_000).toISOString(),
@@ -394,6 +415,7 @@ describe('canonical-guardian-store', () => {
394
415
  sourceType: 'desktop',
395
416
  conversationId: 'conv-desktop-1',
396
417
  guardianExternalUserId: 'guardian-desktop',
418
+ guardianPrincipalId: TEST_PRINCIPAL,
397
419
  questionText: 'User wants to access settings',
398
420
  });
399
421
 
@@ -408,6 +430,7 @@ describe('canonical-guardian-store', () => {
408
430
  const req = createCanonicalGuardianRequest({
409
431
  kind: 'tool_approval',
410
432
  sourceType: 'voice',
433
+ guardianPrincipalId: TEST_PRINCIPAL,
411
434
  });
412
435
 
413
436
  const d1 = createCanonicalGuardianDelivery({
@@ -436,6 +459,7 @@ describe('canonical-guardian-store', () => {
436
459
  const req = createCanonicalGuardianRequest({
437
460
  kind: 'tool_approval',
438
461
  sourceType: 'voice',
462
+ guardianPrincipalId: TEST_PRINCIPAL,
439
463
  });
440
464
 
441
465
  const deliveries = listCanonicalGuardianDeliveries(req.id);
@@ -446,10 +470,12 @@ describe('canonical-guardian-store', () => {
446
470
  const pendingReq = createCanonicalGuardianRequest({
447
471
  kind: 'pending_question',
448
472
  sourceType: 'voice',
473
+ guardianPrincipalId: TEST_PRINCIPAL,
449
474
  });
450
475
  const resolvedReq = createCanonicalGuardianRequest({
451
476
  kind: 'pending_question',
452
477
  sourceType: 'voice',
478
+ guardianPrincipalId: TEST_PRINCIPAL,
453
479
  });
454
480
  updateCanonicalGuardianRequest(resolvedReq.id, { status: 'approved' });
455
481
 
@@ -476,6 +502,7 @@ describe('canonical-guardian-store', () => {
476
502
  const req = createCanonicalGuardianRequest({
477
503
  kind: 'pending_question',
478
504
  sourceType: 'voice',
505
+ guardianPrincipalId: TEST_PRINCIPAL,
479
506
  });
480
507
 
481
508
  createCanonicalGuardianDelivery({
@@ -498,6 +525,7 @@ describe('canonical-guardian-store', () => {
498
525
  const req = createCanonicalGuardianRequest({
499
526
  kind: 'tool_approval',
500
527
  sourceType: 'voice',
528
+ guardianPrincipalId: TEST_PRINCIPAL,
501
529
  });
502
530
  const delivery = createCanonicalGuardianDelivery({
503
531
  requestId: req.id,
@@ -525,6 +553,7 @@ describe('canonical-guardian-store', () => {
525
553
  const req = createCanonicalGuardianRequest({
526
554
  kind: 'pending_question',
527
555
  sourceType: 'voice',
556
+ guardianPrincipalId: TEST_PRINCIPAL,
528
557
  });
529
558
  createCanonicalGuardianDelivery({
530
559
  requestId: req.id,
@@ -544,10 +573,12 @@ describe('canonical-guardian-store', () => {
544
573
  const pendingReq = createCanonicalGuardianRequest({
545
574
  kind: 'pending_question',
546
575
  sourceType: 'voice',
576
+ guardianPrincipalId: TEST_PRINCIPAL,
547
577
  });
548
578
  const resolvedReq = createCanonicalGuardianRequest({
549
579
  kind: 'pending_question',
550
580
  sourceType: 'voice',
581
+ guardianPrincipalId: TEST_PRINCIPAL,
551
582
  });
552
583
  updateCanonicalGuardianRequest(resolvedReq.id, { status: 'approved' });
553
584
 
@@ -574,6 +605,7 @@ describe('canonical-guardian-store', () => {
574
605
  const req = createCanonicalGuardianRequest({
575
606
  kind: 'pending_question',
576
607
  sourceType: 'voice',
608
+ guardianPrincipalId: TEST_PRINCIPAL,
577
609
  });
578
610
 
579
611
  // Two delivery rows targeting the same chat for the same request
@@ -602,6 +634,7 @@ describe('canonical-guardian-store', () => {
602
634
  const req = createCanonicalGuardianRequest({
603
635
  kind: 'pending_question',
604
636
  sourceType: 'voice',
637
+ guardianPrincipalId: TEST_PRINCIPAL,
605
638
  });
606
639
  createCanonicalGuardianDelivery({
607
640
  requestId: req.id,
@@ -620,6 +653,7 @@ describe('canonical-guardian-store', () => {
620
653
  const req = createCanonicalGuardianRequest({
621
654
  kind: 'pending_question',
622
655
  sourceType: 'voice',
656
+ guardianPrincipalId: TEST_PRINCIPAL,
623
657
  });
624
658
  createCanonicalGuardianDelivery({
625
659
  requestId: req.id,
@@ -181,8 +181,8 @@ const TEST_BEARER_TOKEN = 'token';
181
181
  function makeInboundRequest(overrides: Record<string, unknown> = {}): Request {
182
182
  const body: Record<string, unknown> = {
183
183
  sourceChannel: 'telegram',
184
- externalChatId: 'chat-123',
185
- senderExternalUserId: 'telegram-user-default',
184
+ conversationExternalId: 'chat-123',
185
+ actorExternalId: 'telegram-user-default',
186
186
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
187
187
  content: 'hello',
188
188
  replyCallbackUrl: 'https://gateway.test/deliver',
@@ -516,11 +516,11 @@ describe('empty content with callbackData bypasses validation', () => {
516
516
  const reqBody = {
517
517
  sourceChannel: 'telegram',
518
518
  interface: 'telegram',
519
- externalChatId: 'chat-123',
519
+ conversationExternalId: 'chat-123',
520
520
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
521
521
  callbackData: 'apr:req-empty-2:approve_once',
522
522
  replyCallbackUrl: 'https://gateway.test/deliver',
523
- senderExternalUserId: 'telegram-user-default',
523
+ actorExternalId: 'telegram-user-default',
524
524
  };
525
525
  const req = new Request('http://localhost/channels/inbound', {
526
526
  method: 'POST',
@@ -780,8 +780,8 @@ describe('SMS channel approval decisions', () => {
780
780
  const body = {
781
781
  sourceChannel: 'sms',
782
782
  interface: 'sms',
783
- externalChatId: 'sms-chat-123',
784
- senderExternalUserId: 'sms-user-default',
783
+ conversationExternalId: 'sms-chat-123',
784
+ actorExternalId: 'sms-user-default',
785
785
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
786
786
  content: 'hello',
787
787
  replyCallbackUrl: 'https://gateway.test/deliver',
@@ -904,10 +904,10 @@ describe('SMS guardian verify intercept', () => {
904
904
  body: JSON.stringify({
905
905
  sourceChannel: 'sms',
906
906
  interface: 'sms',
907
- externalChatId: 'sms-chat-verify',
907
+ conversationExternalId: 'sms-chat-verify',
908
908
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
909
909
  content: secret,
910
- senderExternalUserId: 'sms-user-42',
910
+ actorExternalId: 'sms-user-42',
911
911
  replyCallbackUrl: 'https://gateway.test/deliver',
912
912
  }),
913
913
  });
@@ -945,10 +945,10 @@ describe('SMS guardian verify intercept', () => {
945
945
  body: JSON.stringify({
946
946
  sourceChannel: 'sms',
947
947
  interface: 'sms',
948
- externalChatId: 'sms-chat-verify-fail',
948
+ conversationExternalId: 'sms-chat-verify-fail',
949
949
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
950
950
  content: '000000',
951
- senderExternalUserId: 'sms-user-43',
951
+ actorExternalId: 'sms-user-43',
952
952
  replyCallbackUrl: 'https://gateway.test/deliver',
953
953
  }),
954
954
  });
@@ -998,10 +998,10 @@ describe('SMS guardian verify intercept', () => {
998
998
  body: JSON.stringify({
999
999
  sourceChannel: 'sms',
1000
1000
  interface: 'sms',
1001
- externalChatId: 'sms-chat-hex-message',
1001
+ conversationExternalId: 'sms-chat-hex-message',
1002
1002
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
1003
1003
  content: secret,
1004
- senderExternalUserId: 'sms-user-hex',
1004
+ actorExternalId: 'sms-user-hex',
1005
1005
  replyCallbackUrl: 'https://gateway.test/deliver',
1006
1006
  }),
1007
1007
  });
@@ -1067,9 +1067,9 @@ describe('guardian decision scoping — multiple pending approvals', () => {
1067
1067
  // The guardian clicks the approval button for the OLDER request
1068
1068
  const req = makeInboundRequest({
1069
1069
  content: '',
1070
- externalChatId: 'guardian-scope-chat',
1070
+ conversationExternalId: 'guardian-scope-chat',
1071
1071
  callbackData: 'apr:req-older:approve_once',
1072
- senderExternalUserId: 'guardian-scope-user',
1072
+ actorExternalId: 'guardian-scope-user',
1073
1073
  });
1074
1074
 
1075
1075
  const res = await handleChannelInbound(req, noopProcessMessage, 'token');
@@ -1145,8 +1145,8 @@ describe('ambiguous plain-text decision with multiple pending requests', () => {
1145
1145
  // Guardian sends plain-text "yes" — ambiguous because two approvals are pending
1146
1146
  const req = makeInboundRequest({
1147
1147
  content: 'yes',
1148
- externalChatId: 'guardian-ambig-chat',
1149
- senderExternalUserId: 'guardian-ambig-user',
1148
+ conversationExternalId: 'guardian-ambig-chat',
1149
+ actorExternalId: 'guardian-ambig-user',
1150
1150
  });
1151
1151
 
1152
1152
  const res = await handleChannelInbound(
@@ -1307,7 +1307,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1307
1307
 
1308
1308
  const req = makeInboundRequest({
1309
1309
  content: secret,
1310
- senderExternalUserId: 'user-default-asst',
1310
+ actorExternalId: 'user-default-asst',
1311
1311
  });
1312
1312
 
1313
1313
  const res = await handleChannelInbound(req, noopProcessMessage, 'token');
@@ -1330,7 +1330,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1330
1330
 
1331
1331
  const req = makeInboundRequest({
1332
1332
  content: secret,
1333
- senderExternalUserId: 'user-for-asst-x',
1333
+ actorExternalId: 'user-for-asst-x',
1334
1334
  });
1335
1335
 
1336
1336
  const res = await handleChannelInbound(req, noopProcessMessage, 'token', 'asst-route-X');
@@ -1356,7 +1356,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1356
1356
 
1357
1357
  const req = makeInboundRequest({
1358
1358
  content: secret,
1359
- senderExternalUserId: 'user-cross-test',
1359
+ actorExternalId: 'user-cross-test',
1360
1360
  });
1361
1361
 
1362
1362
  const res = await handleChannelInbound(req, noopProcessMessage, 'token', 'asst-B-cross');
@@ -1384,7 +1384,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1384
1384
 
1385
1385
  const req = makeInboundRequest({
1386
1386
  content: 'hello from non-self assistant',
1387
- senderExternalUserId: 'incoming-user',
1387
+ actorExternalId: 'incoming-user',
1388
1388
  });
1389
1389
 
1390
1390
  const res = await handleChannelInbound(req, noopProcessMessage, 'token', 'asst-non-self');
@@ -1467,7 +1467,7 @@ describe('handleChannelInbound gatewayOriginSecret integration', () => {
1467
1467
  body: JSON.stringify({
1468
1468
  sourceChannel: 'telegram',
1469
1469
  interface: 'telegram',
1470
- externalChatId: 'chat-gw-secret-test',
1470
+ conversationExternalId: 'chat-gw-secret-test',
1471
1471
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
1472
1472
  content: 'hello',
1473
1473
  }),
@@ -1494,10 +1494,10 @@ describe('handleChannelInbound gatewayOriginSecret integration', () => {
1494
1494
  body: JSON.stringify({
1495
1495
  sourceChannel: 'telegram',
1496
1496
  interface: 'telegram',
1497
- externalChatId: 'chat-gw-secret-pass',
1497
+ conversationExternalId: 'chat-gw-secret-pass',
1498
1498
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
1499
1499
  content: 'hello',
1500
- senderExternalUserId: 'telegram-user-default',
1500
+ actorExternalId: 'telegram-user-default',
1501
1501
  }),
1502
1502
  });
1503
1503
 
@@ -1521,10 +1521,10 @@ describe('handleChannelInbound gatewayOriginSecret integration', () => {
1521
1521
  body: JSON.stringify({
1522
1522
  sourceChannel: 'telegram',
1523
1523
  interface: 'telegram',
1524
- externalChatId: 'chat-gw-fallback',
1524
+ conversationExternalId: 'chat-gw-fallback',
1525
1525
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
1526
1526
  content: 'hello',
1527
- senderExternalUserId: 'telegram-user-default',
1527
+ actorExternalId: 'telegram-user-default',
1528
1528
  }),
1529
1529
  });
1530
1530
 
@@ -1745,8 +1745,8 @@ describe('guardian conversational approval via conversation engine', () => {
1745
1745
 
1746
1746
  const req = makeInboundRequest({
1747
1747
  content: 'hmm what does this do?',
1748
- externalChatId: 'guardian-conv-chat',
1749
- senderExternalUserId: 'guardian-conv-user',
1748
+ conversationExternalId: 'guardian-conv-chat',
1749
+ actorExternalId: 'guardian-conv-user',
1750
1750
  });
1751
1751
 
1752
1752
  const res = await handleChannelInbound(
@@ -1809,8 +1809,8 @@ describe('guardian conversational approval via conversation engine', () => {
1809
1809
 
1810
1810
  const req = makeInboundRequest({
1811
1811
  content: 'yes go ahead and run it',
1812
- externalChatId: 'guardian-nlp-chat',
1813
- senderExternalUserId: 'guardian-nlp-user',
1812
+ conversationExternalId: 'guardian-nlp-chat',
1813
+ actorExternalId: 'guardian-nlp-user',
1814
1814
  });
1815
1815
 
1816
1816
  const res = await handleChannelInbound(
@@ -1867,9 +1867,9 @@ describe('guardian conversational approval via conversation engine', () => {
1867
1867
  // Guardian clicks approve_always via callback button
1868
1868
  const req = makeInboundRequest({
1869
1869
  content: '',
1870
- externalChatId: 'guardian-dg-chat',
1870
+ conversationExternalId: 'guardian-dg-chat',
1871
1871
  callbackData: 'apr:req-gdg-1:approve_always',
1872
- senderExternalUserId: 'guardian-dg-user',
1872
+ actorExternalId: 'guardian-dg-user',
1873
1873
  });
1874
1874
 
1875
1875
  const res = await handleChannelInbound(
@@ -1937,8 +1937,8 @@ describe('guardian conversational approval via conversation engine', () => {
1937
1937
 
1938
1938
  const req = makeInboundRequest({
1939
1939
  content: 'approve it',
1940
- externalChatId: 'guardian-multi-chat',
1941
- senderExternalUserId: 'guardian-multi-user',
1940
+ conversationExternalId: 'guardian-multi-chat',
1941
+ actorExternalId: 'guardian-multi-user',
1942
1942
  });
1943
1943
 
1944
1944
  const res = await handleChannelInbound(
@@ -2058,8 +2058,8 @@ describe('keep_pending remains conversational — guardian path', () => {
2058
2058
 
2059
2059
  const guardianReq = makeInboundRequest({
2060
2060
  content: 'yes',
2061
- externalChatId: 'guardian-chat-fb',
2062
- senderExternalUserId: 'guardian-user-fb',
2061
+ conversationExternalId: 'guardian-chat-fb',
2062
+ actorExternalId: 'guardian-user-fb',
2063
2063
  });
2064
2064
  const res = await handleChannelInbound(
2065
2065
  guardianReq, noopProcessMessage, 'token', 'self', undefined,
@@ -2100,8 +2100,8 @@ describe('requester cancel of guardian-gated pending request', () => {
2100
2100
  // Create requester conversation
2101
2101
  const initReq = makeInboundRequest({
2102
2102
  content: 'init',
2103
- externalChatId: 'requester-cancel-chat',
2104
- senderExternalUserId: 'requester-cancel-user',
2103
+ conversationExternalId: 'requester-cancel-chat',
2104
+ actorExternalId: 'requester-cancel-user',
2105
2105
  });
2106
2106
  await handleChannelInbound(initReq, noopProcessMessage, 'token');
2107
2107
 
@@ -2135,8 +2135,8 @@ describe('requester cancel of guardian-gated pending request', () => {
2135
2135
 
2136
2136
  const req = makeInboundRequest({
2137
2137
  content: 'deny',
2138
- externalChatId: 'requester-cancel-chat',
2139
- senderExternalUserId: 'requester-cancel-user',
2138
+ conversationExternalId: 'requester-cancel-chat',
2139
+ actorExternalId: 'requester-cancel-user',
2140
2140
  });
2141
2141
  const res = await handleChannelInbound(
2142
2142
  req, noopProcessMessage, 'token', 'self', undefined,
@@ -2168,8 +2168,8 @@ describe('requester cancel of guardian-gated pending request', () => {
2168
2168
 
2169
2169
  const initReq = makeInboundRequest({
2170
2170
  content: 'init',
2171
- externalChatId: 'requester-cancel-chat',
2172
- senderExternalUserId: 'requester-cancel-user',
2171
+ conversationExternalId: 'requester-cancel-chat',
2172
+ actorExternalId: 'requester-cancel-user',
2173
2173
  });
2174
2174
  await handleChannelInbound(initReq, noopProcessMessage, 'token');
2175
2175
 
@@ -2203,8 +2203,8 @@ describe('requester cancel of guardian-gated pending request', () => {
2203
2203
 
2204
2204
  const req = makeInboundRequest({
2205
2205
  content: 'actually never mind, cancel it',
2206
- externalChatId: 'requester-cancel-chat',
2207
- senderExternalUserId: 'requester-cancel-user',
2206
+ conversationExternalId: 'requester-cancel-chat',
2207
+ actorExternalId: 'requester-cancel-user',
2208
2208
  });
2209
2209
  const res = await handleChannelInbound(
2210
2210
  req, noopProcessMessage, 'token', 'self', undefined,
@@ -2229,8 +2229,8 @@ describe('requester cancel of guardian-gated pending request', () => {
2229
2229
 
2230
2230
  const initReq = makeInboundRequest({
2231
2231
  content: 'init',
2232
- externalChatId: 'requester-cancel-chat',
2233
- senderExternalUserId: 'requester-cancel-user',
2232
+ conversationExternalId: 'requester-cancel-chat',
2233
+ actorExternalId: 'requester-cancel-user',
2234
2234
  });
2235
2235
  await handleChannelInbound(initReq, noopProcessMessage, 'token');
2236
2236
 
@@ -2264,8 +2264,8 @@ describe('requester cancel of guardian-gated pending request', () => {
2264
2264
 
2265
2265
  const req = makeInboundRequest({
2266
2266
  content: 'what is happening?',
2267
- externalChatId: 'requester-cancel-chat',
2268
- senderExternalUserId: 'requester-cancel-user',
2267
+ conversationExternalId: 'requester-cancel-chat',
2268
+ actorExternalId: 'requester-cancel-user',
2269
2269
  });
2270
2270
  const res = await handleChannelInbound(
2271
2271
  req, noopProcessMessage, 'token', 'self', undefined,
@@ -2290,8 +2290,8 @@ describe('requester cancel of guardian-gated pending request', () => {
2290
2290
 
2291
2291
  const initReq = makeInboundRequest({
2292
2292
  content: 'init',
2293
- externalChatId: 'requester-cancel-chat',
2294
- senderExternalUserId: 'requester-cancel-user',
2293
+ conversationExternalId: 'requester-cancel-chat',
2294
+ actorExternalId: 'requester-cancel-user',
2295
2295
  });
2296
2296
  await handleChannelInbound(initReq, noopProcessMessage, 'token');
2297
2297
 
@@ -2321,8 +2321,8 @@ describe('requester cancel of guardian-gated pending request', () => {
2321
2321
  // Requester tries to self-approve while guardian approval is pending.
2322
2322
  const req = makeInboundRequest({
2323
2323
  content: 'approve',
2324
- externalChatId: 'requester-cancel-chat',
2325
- senderExternalUserId: 'requester-cancel-user',
2324
+ conversationExternalId: 'requester-cancel-chat',
2325
+ actorExternalId: 'requester-cancel-user',
2326
2326
  });
2327
2327
  const res = await handleChannelInbound(req, noopProcessMessage, 'token');
2328
2328
  const body = await res.json() as Record<string, unknown>;
@@ -2443,8 +2443,8 @@ describe('engine decision race condition — guardian path', () => {
2443
2443
 
2444
2444
  const guardianReq = makeInboundRequest({
2445
2445
  content: 'approve it',
2446
- externalChatId: 'guardian-race-chat',
2447
- senderExternalUserId: 'guardian-race-user',
2446
+ conversationExternalId: 'guardian-race-chat',
2447
+ actorExternalId: 'guardian-race-user',
2448
2448
  });
2449
2449
  const res = await handleChannelInbound(
2450
2450
  guardianReq, noopProcessMessage, 'token', 'self', undefined,
@@ -2789,6 +2789,7 @@ describe('NL approval routing via destination-scoped canonical requests', () =>
2789
2789
  sourceChannel: 'twilio',
2790
2790
  conversationId: 'conv-voice-nl-1',
2791
2791
  toolName: 'shell',
2792
+ guardianPrincipalId: 'test-principal-id',
2792
2793
  expiresAt: new Date(Date.now() + 60_000).toISOString(),
2793
2794
  // guardianExternalUserId intentionally omitted
2794
2795
  });
@@ -2806,8 +2807,8 @@ describe('NL approval routing via destination-scoped canonical requests', () =>
2806
2807
  // Send inbound guardian text reply "yes" from that chat
2807
2808
  const req = makeInboundRequest({
2808
2809
  sourceChannel: 'telegram',
2809
- externalChatId: guardianChatId,
2810
- senderExternalUserId: guardianUserId,
2810
+ conversationExternalId: guardianChatId,
2811
+ actorExternalId: guardianUserId,
2811
2812
  content: 'yes',
2812
2813
  externalMessageId: `msg-nl-approve-${Date.now()}`,
2813
2814
  });
@@ -2842,6 +2843,7 @@ describe('NL approval routing via destination-scoped canonical requests', () =>
2842
2843
  sourceType: 'voice',
2843
2844
  sourceChannel: 'twilio',
2844
2845
  toolName: 'shell',
2846
+ guardianPrincipalId: 'test-principal-id',
2845
2847
  expiresAt: new Date(Date.now() + 60_000).toISOString(),
2846
2848
  });
2847
2849
 
@@ -2855,8 +2857,8 @@ describe('NL approval routing via destination-scoped canonical requests', () =>
2855
2857
  // Send from differentChatId — delivery-scoped lookup should not match
2856
2858
  const req = makeInboundRequest({
2857
2859
  sourceChannel: 'telegram',
2858
- externalChatId: differentChatId,
2859
- senderExternalUserId: guardianUserId,
2860
+ conversationExternalId: differentChatId,
2861
+ actorExternalId: guardianUserId,
2860
2862
  content: 'approve',
2861
2863
  externalMessageId: `msg-nl-mismatch-${Date.now()}`,
2862
2864
  });
@@ -2897,8 +2899,8 @@ describe('trusted-contact self-approval blocked before guardian approval row exi
2897
2899
  // Create the requester conversation (different user than guardian)
2898
2900
  const initReq = makeInboundRequest({
2899
2901
  content: 'init',
2900
- externalChatId: 'tc-selfapproval-chat',
2901
- senderExternalUserId: 'tc-selfapproval-user',
2902
+ conversationExternalId: 'tc-selfapproval-chat',
2903
+ actorExternalId: 'tc-selfapproval-user',
2902
2904
  });
2903
2905
  await handleChannelInbound(initReq, noopProcessMessage, 'token');
2904
2906
 
@@ -2925,8 +2927,8 @@ describe('trusted-contact self-approval blocked before guardian approval row exi
2925
2927
  // Trusted contact sends "yes" to try to self-approve
2926
2928
  const req = makeInboundRequest({
2927
2929
  content: 'yes',
2928
- externalChatId: 'tc-selfapproval-chat',
2929
- senderExternalUserId: 'tc-selfapproval-user',
2930
+ conversationExternalId: 'tc-selfapproval-chat',
2931
+ actorExternalId: 'tc-selfapproval-user',
2930
2932
  });
2931
2933
  const res = await handleChannelInbound(
2932
2934
  req, noopProcessMessage, 'token', 'self', undefined,
@@ -2953,8 +2955,8 @@ describe('trusted-contact self-approval blocked before guardian approval row exi
2953
2955
 
2954
2956
  const initReq = makeInboundRequest({
2955
2957
  content: 'init',
2956
- externalChatId: 'tc-selfapproval-chat',
2957
- senderExternalUserId: 'tc-selfapproval-user',
2958
+ conversationExternalId: 'tc-selfapproval-chat',
2959
+ actorExternalId: 'tc-selfapproval-user',
2958
2960
  });
2959
2961
  await handleChannelInbound(initReq, noopProcessMessage, 'token');
2960
2962
 
@@ -2972,8 +2974,8 @@ describe('trusted-contact self-approval blocked before guardian approval row exi
2972
2974
  // "approve" would normally be parsed as an approval decision.
2973
2975
  const req = makeInboundRequest({
2974
2976
  content: 'approve',
2975
- externalChatId: 'tc-selfapproval-chat',
2976
- senderExternalUserId: 'tc-selfapproval-user',
2977
+ conversationExternalId: 'tc-selfapproval-chat',
2978
+ actorExternalId: 'tc-selfapproval-user',
2977
2979
  });
2978
2980
  const res = await handleChannelInbound(req, noopProcessMessage, 'token');
2979
2981
  const body = await res.json() as Record<string, unknown>;