@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.
Files changed (90) hide show
  1. package/ARCHITECTURE.md +4 -4
  2. package/README.md +6 -6
  3. package/bun.lock +6 -2
  4. package/docs/architecture/memory.md +4 -4
  5. package/package.json +2 -2
  6. package/src/__tests__/actor-token-service.test.ts +5 -2
  7. package/src/__tests__/assistant-feature-flags-integration.test.ts +1 -0
  8. package/src/__tests__/call-controller.test.ts +78 -0
  9. package/src/__tests__/call-domain.test.ts +148 -10
  10. package/src/__tests__/call-pointer-message-composer.test.ts +39 -49
  11. package/src/__tests__/call-pointer-messages.test.ts +105 -43
  12. package/src/__tests__/canonical-guardian-store.test.ts +44 -10
  13. package/src/__tests__/channel-approval-routes.test.ts +67 -65
  14. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +1 -0
  15. package/src/__tests__/conversation-attention-telegram.test.ts +2 -2
  16. package/src/__tests__/deterministic-verification-control-plane.test.ts +6 -6
  17. package/src/__tests__/guardian-actions-endpoint.test.ts +7 -6
  18. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +57 -12
  19. package/src/__tests__/guardian-grant-minting.test.ts +24 -24
  20. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +205 -0
  21. package/src/__tests__/guardian-routing-invariants.test.ts +64 -25
  22. package/src/__tests__/guardian-routing-state.test.ts +4 -4
  23. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -2
  24. package/src/__tests__/inbound-invite-redemption.test.ts +8 -8
  25. package/src/__tests__/memory-retrieval.benchmark.test.ts +22 -47
  26. package/src/__tests__/no-is-trusted-guard.test.ts +77 -0
  27. package/src/__tests__/non-member-access-request.test.ts +50 -47
  28. package/src/__tests__/relay-server.test.ts +71 -0
  29. package/src/__tests__/send-endpoint-busy.test.ts +6 -0
  30. package/src/__tests__/session-tool-setup-tools-disabled.test.ts +155 -0
  31. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -0
  32. package/src/__tests__/skill-projection.benchmark.test.ts +66 -2
  33. package/src/__tests__/system-prompt.test.ts +1 -0
  34. package/src/__tests__/tool-approval-handler.test.ts +1 -1
  35. package/src/__tests__/tool-grant-request-escalation.test.ts +9 -2
  36. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +8 -1
  37. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +22 -22
  38. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -4
  39. package/src/__tests__/trusted-contact-verification.test.ts +10 -10
  40. package/src/approvals/guardian-decision-primitive.ts +29 -25
  41. package/src/approvals/guardian-request-resolvers.ts +9 -5
  42. package/src/calls/call-pointer-message-composer.ts +27 -85
  43. package/src/calls/call-pointer-messages.ts +54 -21
  44. package/src/calls/guardian-dispatch.ts +30 -0
  45. package/src/calls/relay-server.ts +13 -13
  46. package/src/config/system-prompt.ts +10 -3
  47. package/src/config/templates/BOOTSTRAP.md +6 -5
  48. package/src/config/templates/USER.md +1 -0
  49. package/src/config/user-reference.ts +44 -0
  50. package/src/daemon/handlers/guardian-actions.ts +5 -2
  51. package/src/daemon/handlers/sessions.ts +8 -3
  52. package/src/daemon/lifecycle.ts +109 -3
  53. package/src/daemon/server.ts +32 -24
  54. package/src/daemon/session-agent-loop.ts +4 -3
  55. package/src/daemon/session-lifecycle.ts +1 -9
  56. package/src/daemon/session-process.ts +2 -2
  57. package/src/daemon/session-runtime-assembly.ts +2 -0
  58. package/src/daemon/session-tool-setup.ts +10 -0
  59. package/src/daemon/session.ts +1 -0
  60. package/src/memory/canonical-guardian-store.ts +40 -0
  61. package/src/memory/conversation-crud.ts +26 -0
  62. package/src/memory/conversation-store.ts +1 -0
  63. package/src/memory/db-init.ts +8 -0
  64. package/src/memory/guardian-bindings.ts +4 -0
  65. package/src/memory/job-handlers/backfill.ts +2 -9
  66. package/src/memory/migrations/125-guardian-principal-id-columns.ts +19 -0
  67. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +210 -0
  68. package/src/memory/migrations/index.ts +2 -0
  69. package/src/memory/migrations/registry.ts +5 -0
  70. package/src/memory/schema.ts +3 -0
  71. package/src/notifications/copy-composer.ts +2 -2
  72. package/src/runtime/access-request-helper.ts +43 -28
  73. package/src/runtime/actor-trust-resolver.ts +19 -14
  74. package/src/runtime/channel-guardian-service.ts +6 -0
  75. package/src/runtime/guardian-context-resolver.ts +6 -2
  76. package/src/runtime/guardian-reply-router.ts +33 -16
  77. package/src/runtime/guardian-vellum-migration.ts +29 -5
  78. package/src/runtime/http-types.ts +0 -13
  79. package/src/runtime/local-actor-identity.ts +19 -13
  80. package/src/runtime/middleware/actor-token.ts +2 -2
  81. package/src/runtime/routes/channel-delivery-routes.ts +5 -5
  82. package/src/runtime/routes/conversation-routes.ts +45 -35
  83. package/src/runtime/routes/guardian-action-routes.ts +7 -1
  84. package/src/runtime/routes/guardian-approval-interception.ts +52 -52
  85. package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -0
  86. package/src/runtime/routes/inbound-conversation.ts +7 -7
  87. package/src/runtime/routes/inbound-message-handler.ts +105 -94
  88. package/src/runtime/tool-grant-request-helper.ts +1 -0
  89. package/src/util/logger.ts +10 -0
  90. package/src/daemon/call-pointer-generators.ts +0 -59
@@ -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>;
@@ -118,6 +118,7 @@ function makeCanonicalRequest(overrides: Record<string, unknown> = {}) {
118
118
  conversationId: 'conv-1',
119
119
  requesterExternalUserId: 'requester-1',
120
120
  guardianExternalUserId: 'guardian-1',
121
+ guardianPrincipalId: 'test-principal-id',
121
122
  toolName: 'bash',
122
123
  status: 'pending',
123
124
  requestCode: generateCanonicalRequestCode(),
@@ -119,8 +119,8 @@ function makeInboundRequest(overrides: Record<string, unknown> = {}): Request {
119
119
  const body = {
120
120
  sourceChannel: 'telegram',
121
121
  interface: 'telegram',
122
- externalChatId: 'chat-123',
123
- senderExternalUserId: 'telegram-user-default',
122
+ conversationExternalId: 'chat-123',
123
+ actorExternalId: 'telegram-user-default',
124
124
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
125
125
  content: 'hello',
126
126
  replyCallbackUrl: 'https://gateway.test/deliver',
@@ -257,11 +257,11 @@ describe('Verification control messages are deterministic (guard)', () => {
257
257
  body: JSON.stringify({
258
258
  sourceChannel: 'telegram',
259
259
  interface: 'telegram',
260
- externalChatId: 'chat-123',
260
+ conversationExternalId: 'chat-123',
261
261
  externalMessageId: `msg-guard-${Date.now()}`,
262
262
  content: secret,
263
- senderExternalUserId: 'user-123',
264
- senderName: 'Test User',
263
+ actorExternalId: 'user-123',
264
+ actorDisplayName: 'Test User',
265
265
  replyCallbackUrl: 'http://localhost/callback',
266
266
  }),
267
267
  });
@@ -330,11 +330,11 @@ describe('Verification control messages are deterministic (guard)', () => {
330
330
  body: JSON.stringify({
331
331
  sourceChannel: 'telegram',
332
332
  interface: 'telegram',
333
- externalChatId: 'chat-bootstrap-123',
333
+ conversationExternalId: 'chat-bootstrap-123',
334
334
  externalMessageId: `msg-bootstrap-${Date.now()}`,
335
335
  content: `/start gv_${bootstrapToken}`,
336
- senderExternalUserId: 'user-bootstrap-123',
337
- senderName: 'Bootstrap User',
336
+ actorExternalId: 'user-bootstrap-123',
337
+ actorDisplayName: 'Bootstrap User',
338
338
  replyCallbackUrl: 'http://localhost/callback',
339
339
  sourceMetadata: {
340
340
  commandIntent: { type: 'start', payload: `gv_${bootstrapToken}` },
@@ -94,6 +94,7 @@ function createTestCanonicalRequest(overrides: {
94
94
  kind?: string;
95
95
  toolName?: string;
96
96
  guardianExternalUserId?: string;
97
+ guardianPrincipalId?: string;
97
98
  questionText?: string;
98
99
  expiresAt?: string;
99
100
  }) {
@@ -105,6 +106,7 @@ function createTestCanonicalRequest(overrides: {
105
106
  sourceChannel: 'vellum',
106
107
  conversationId: overrides.conversationId,
107
108
  guardianExternalUserId: overrides.guardianExternalUserId,
109
+ guardianPrincipalId: overrides.guardianPrincipalId ?? 'test-principal',
108
110
  toolName: overrides.toolName ?? 'bash',
109
111
  questionText: overrides.questionText,
110
112
  requestCode: generateCanonicalRequestCode(),
@@ -304,7 +306,7 @@ describe('HTTP handleGuardianActionDecision', () => {
304
306
  expect(body.requestId).toBe('req-stale-1');
305
307
  });
306
308
 
307
- test('passes actorContext with undefined externalUserId (unauthenticated endpoint)', async () => {
309
+ test('passes actorContext with vellum channel and guardianPrincipalId', async () => {
308
310
  createTestCanonicalRequest({ conversationId: 'conv-actor', requestId: 'req-actor-1' });
309
311
  mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-actor-1', grantMinted: false });
310
312
 
@@ -315,9 +317,8 @@ describe('HTTP handleGuardianActionDecision', () => {
315
317
  await handleGuardianActionDecision(req, mockLoopbackServer);
316
318
  const call = mockApplyCanonicalGuardianDecision.mock.calls[0]![0] as Record<string, unknown>;
317
319
  const actorContext = call.actorContext as Record<string, unknown>;
318
- expect(actorContext.externalUserId).toBeUndefined();
319
320
  expect(actorContext.channel).toBe('vellum');
320
- expect(actorContext.isTrusted).toBe(true);
321
+ expect(actorContext.guardianPrincipalId).toBeDefined();
321
322
  });
322
323
  });
323
324
 
@@ -373,6 +374,7 @@ describe('listGuardianDecisionPrompts', () => {
373
374
  sourceType: 'desktop',
374
375
  sourceChannel: 'vellum',
375
376
  conversationId: 'conv-expired',
377
+ guardianPrincipalId: 'test-principal',
376
378
  toolName: 'bash',
377
379
  requestCode: generateCanonicalRequestCode(),
378
380
  status: 'pending',
@@ -597,7 +599,7 @@ describe('IPC guardian_action_decision', () => {
597
599
  expect(sent[0].reason).toBe('already_resolved');
598
600
  });
599
601
 
600
- test('passes actorContext with undefined externalUserId (unauthenticated endpoint)', async () => {
602
+ test('passes actorContext with vellum channel and guardianPrincipalId', async () => {
601
603
  createTestCanonicalRequest({ conversationId: 'conv-ipc-actor', requestId: 'req-ipc-actor' });
602
604
  mockApplyCanonicalGuardianDecision.mockResolvedValueOnce({ applied: true, requestId: 'req-ipc-actor', grantMinted: false });
603
605
 
@@ -609,9 +611,8 @@ describe('IPC guardian_action_decision', () => {
609
611
  );
610
612
  const call = mockApplyCanonicalGuardianDecision.mock.calls[0]![0] as Record<string, unknown>;
611
613
  const actorContext = call.actorContext as Record<string, unknown>;
612
- expect(actorContext.externalUserId).toBeUndefined();
613
614
  expect(actorContext.channel).toBe('vellum');
614
- expect(actorContext.isTrusted).toBe(true);
615
+ expect(actorContext.guardianPrincipalId).toBeDefined();
615
616
  });
616
617
  });
617
618