@vellumai/assistant 0.4.2 → 0.4.3

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 (84) hide show
  1. package/ARCHITECTURE.md +84 -7
  2. package/docs/trusted-contact-access.md +20 -0
  3. package/package.json +1 -1
  4. package/src/__tests__/access-request-decision.test.ts +0 -1
  5. package/src/__tests__/assistant-id-boundary-guard.test.ts +290 -0
  6. package/src/__tests__/call-routes-http.test.ts +0 -25
  7. package/src/__tests__/channel-guardian.test.ts +6 -5
  8. package/src/__tests__/config-schema.test.ts +2 -0
  9. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  10. package/src/__tests__/guardian-actions-endpoint.test.ts +21 -0
  11. package/src/__tests__/guardian-outbound-http.test.ts +0 -1
  12. package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
  13. package/src/__tests__/ingress-routes-http.test.ts +55 -0
  14. package/src/__tests__/non-member-access-request.test.ts +28 -1
  15. package/src/__tests__/notification-decision-strategy.test.ts +44 -0
  16. package/src/__tests__/relay-server.test.ts +644 -4
  17. package/src/__tests__/session-init.benchmark.test.ts +0 -1
  18. package/src/__tests__/session-runtime-assembly.test.ts +4 -1
  19. package/src/__tests__/session-surfaces-task-progress.test.ts +43 -0
  20. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
  21. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  22. package/src/__tests__/trusted-contact-verification.test.ts +0 -1
  23. package/src/__tests__/twilio-routes.test.ts +4 -3
  24. package/src/__tests__/update-bulletin.test.ts +0 -1
  25. package/src/approvals/guardian-decision-primitive.ts +2 -1
  26. package/src/approvals/guardian-request-resolvers.ts +42 -3
  27. package/src/calls/call-constants.ts +8 -0
  28. package/src/calls/call-controller.ts +2 -1
  29. package/src/calls/call-domain.ts +5 -4
  30. package/src/calls/relay-server.ts +513 -116
  31. package/src/calls/twilio-routes.ts +3 -5
  32. package/src/calls/types.ts +1 -1
  33. package/src/calls/voice-session-bridge.ts +4 -3
  34. package/src/cli/core-commands.ts +7 -4
  35. package/src/config/bundled-skills/app-builder/SKILL.md +164 -1
  36. package/src/config/bundled-skills/vercel-token-setup/SKILL.md +214 -0
  37. package/src/config/calls-schema.ts +12 -0
  38. package/src/config/feature-flag-registry.json +0 -8
  39. package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -2
  40. package/src/daemon/handlers/config-channels.ts +5 -7
  41. package/src/daemon/handlers/config-inbox.ts +2 -0
  42. package/src/daemon/handlers/index.ts +2 -1
  43. package/src/daemon/handlers/publish.ts +11 -46
  44. package/src/daemon/handlers/sessions.ts +11 -2
  45. package/src/daemon/ipc-contract/apps.ts +1 -0
  46. package/src/daemon/ipc-contract/inbox.ts +4 -0
  47. package/src/daemon/ipc-contract/integrations.ts +3 -1
  48. package/src/daemon/server.ts +2 -1
  49. package/src/daemon/session-agent-loop.ts +2 -1
  50. package/src/daemon/session-runtime-assembly.ts +3 -1
  51. package/src/daemon/session-surfaces.ts +29 -1
  52. package/src/memory/conversation-crud.ts +2 -1
  53. package/src/memory/conversation-title-service.ts +16 -2
  54. package/src/memory/db-init.ts +4 -0
  55. package/src/memory/delivery-crud.ts +2 -1
  56. package/src/memory/guardian-action-store.ts +2 -1
  57. package/src/memory/guardian-approvals.ts +3 -2
  58. package/src/memory/ingress-invite-store.ts +12 -2
  59. package/src/memory/ingress-member-store.ts +4 -3
  60. package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
  61. package/src/memory/migrations/index.ts +1 -0
  62. package/src/memory/schema.ts +10 -5
  63. package/src/notifications/copy-composer.ts +11 -1
  64. package/src/notifications/emit-signal.ts +2 -1
  65. package/src/runtime/access-request-helper.ts +11 -3
  66. package/src/runtime/actor-trust-resolver.ts +2 -2
  67. package/src/runtime/assistant-scope.ts +10 -0
  68. package/src/runtime/guardian-outbound-actions.ts +5 -4
  69. package/src/runtime/http-server.ts +11 -20
  70. package/src/runtime/ingress-service.ts +14 -0
  71. package/src/runtime/invite-redemption-service.ts +2 -1
  72. package/src/runtime/middleware/twilio-validation.ts +2 -4
  73. package/src/runtime/routes/call-routes.ts +2 -1
  74. package/src/runtime/routes/channel-route-shared.ts +3 -3
  75. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  76. package/src/runtime/routes/conversation-routes.ts +2 -1
  77. package/src/runtime/routes/events-routes.ts +2 -3
  78. package/src/runtime/routes/inbound-conversation.ts +4 -3
  79. package/src/runtime/routes/inbound-message-handler.ts +4 -3
  80. package/src/runtime/routes/ingress-routes.ts +2 -0
  81. package/src/tools/calls/call-start.ts +2 -1
  82. package/src/tools/terminal/parser.ts +12 -0
  83. package/src/tools/tool-approval-handler.ts +2 -1
  84. package/src/workspace/git-service.ts +19 -0
@@ -492,6 +492,8 @@ describe('voice invite HTTP routes', () => {
492
492
  body: JSON.stringify({
493
493
  sourceChannel: 'voice',
494
494
  expectedExternalUserId: '+15551234567',
495
+ friendName: 'Alice',
496
+ guardianName: 'Bob',
495
497
  maxUses: 3,
496
498
  }),
497
499
  });
@@ -514,6 +516,9 @@ describe('voice invite HTTP routes', () => {
514
516
  expect(invite.voiceCodeDigits).toBe(6);
515
517
  // expectedExternalUserId should be recorded
516
518
  expect(invite.expectedExternalUserId).toBe('+15551234567');
519
+ // friendName and guardianName should be recorded
520
+ expect(invite.friendName).toBe('Alice');
521
+ expect(invite.guardianName).toBe('Bob');
517
522
  });
518
523
 
519
524
  test('voice invite creation requires expectedExternalUserId', async () => {
@@ -522,6 +527,8 @@ describe('voice invite HTTP routes', () => {
522
527
  headers: { 'Content-Type': 'application/json' },
523
528
  body: JSON.stringify({
524
529
  sourceChannel: 'voice',
530
+ friendName: 'Alice',
531
+ guardianName: 'Bob',
525
532
  }),
526
533
  });
527
534
 
@@ -540,6 +547,8 @@ describe('voice invite HTTP routes', () => {
540
547
  body: JSON.stringify({
541
548
  sourceChannel: 'voice',
542
549
  expectedExternalUserId: 'not-a-phone-number',
550
+ friendName: 'Alice',
551
+ guardianName: 'Bob',
543
552
  }),
544
553
  });
545
554
 
@@ -551,6 +560,44 @@ describe('voice invite HTTP routes', () => {
551
560
  expect(body.error).toContain('E.164');
552
561
  });
553
562
 
563
+ test('voice invite creation requires friendName', async () => {
564
+ const req = new Request('http://localhost/v1/ingress/invites', {
565
+ method: 'POST',
566
+ headers: { 'Content-Type': 'application/json' },
567
+ body: JSON.stringify({
568
+ sourceChannel: 'voice',
569
+ expectedExternalUserId: '+15551234567',
570
+ guardianName: 'Bob',
571
+ }),
572
+ });
573
+
574
+ const res = await handleCreateInvite(req);
575
+ const body = await res.json() as Record<string, unknown>;
576
+
577
+ expect(res.status).toBe(400);
578
+ expect(body.ok).toBe(false);
579
+ expect(body.error).toContain('friendName');
580
+ });
581
+
582
+ test('voice invite creation requires guardianName', async () => {
583
+ const req = new Request('http://localhost/v1/ingress/invites', {
584
+ method: 'POST',
585
+ headers: { 'Content-Type': 'application/json' },
586
+ body: JSON.stringify({
587
+ sourceChannel: 'voice',
588
+ expectedExternalUserId: '+15551234567',
589
+ friendName: 'Alice',
590
+ }),
591
+ });
592
+
593
+ const res = await handleCreateInvite(req);
594
+ const body = await res.json() as Record<string, unknown>;
595
+
596
+ expect(res.status).toBe(400);
597
+ expect(body.ok).toBe(false);
598
+ expect(body.error).toContain('guardianName');
599
+ });
600
+
554
601
  test('voiceCodeDigits is always 6 — custom values are ignored', async () => {
555
602
  const req = new Request('http://localhost/v1/ingress/invites', {
556
603
  method: 'POST',
@@ -558,6 +605,8 @@ describe('voice invite HTTP routes', () => {
558
605
  body: JSON.stringify({
559
606
  sourceChannel: 'voice',
560
607
  expectedExternalUserId: '+15551234567',
608
+ friendName: 'Alice',
609
+ guardianName: 'Bob',
561
610
  voiceCodeDigits: 8,
562
611
  }),
563
612
  });
@@ -579,6 +628,8 @@ describe('voice invite HTTP routes', () => {
579
628
  body: JSON.stringify({
580
629
  sourceChannel: 'voice',
581
630
  expectedExternalUserId: '+15551234567',
631
+ friendName: 'Alice',
632
+ guardianName: 'Bob',
582
633
  }),
583
634
  });
584
635
 
@@ -600,6 +651,8 @@ describe('voice invite HTTP routes', () => {
600
651
  body: JSON.stringify({
601
652
  sourceChannel: 'voice',
602
653
  expectedExternalUserId: '+15551234567',
654
+ friendName: 'Alice',
655
+ guardianName: 'Bob',
603
656
  maxUses: 1,
604
657
  }),
605
658
  }));
@@ -648,6 +701,8 @@ describe('voice invite HTTP routes', () => {
648
701
  body: JSON.stringify({
649
702
  sourceChannel: 'voice',
650
703
  expectedExternalUserId: '+15551234567',
704
+ friendName: 'Alice',
705
+ guardianName: 'Bob',
651
706
  maxUses: 1,
652
707
  }),
653
708
  }));
@@ -30,7 +30,6 @@ mock.module('../util/platform.js', () => ({
30
30
  getDbPath: () => join(testDir, 'test.db'),
31
31
  getLogPath: () => join(testDir, 'test.log'),
32
32
  ensureDataDir: () => {},
33
- normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
34
33
  readHttpToken: () => 'test-bearer-token',
35
34
  }));
36
35
 
@@ -437,6 +436,34 @@ describe('access-request-helper unit tests', () => {
437
436
  expect(payload.guardianBindingChannel).toBe('telegram');
438
437
  });
439
438
 
439
+ test('notifyGuardianOfAccessRequest for voice channel includes senderName in contextPayload', () => {
440
+ const result = notifyGuardianOfAccessRequest({
441
+ canonicalAssistantId: 'self',
442
+ sourceChannel: 'voice',
443
+ externalChatId: '+15559998888',
444
+ senderExternalUserId: '+15559998888',
445
+ senderName: 'Alice Caller',
446
+ });
447
+
448
+ expect(result.notified).toBe(true);
449
+ expect(emitSignalCalls.length).toBe(1);
450
+
451
+ const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
452
+ expect(payload.sourceChannel).toBe('voice');
453
+ expect(payload.senderName).toBe('Alice Caller');
454
+ expect(payload.senderExternalUserId).toBe('+15559998888');
455
+ expect(payload.senderIdentifier).toBe('Alice Caller');
456
+
457
+ // Canonical request should exist
458
+ const pending = listCanonicalGuardianRequests({
459
+ status: 'pending',
460
+ requesterExternalUserId: '+15559998888',
461
+ sourceChannel: 'voice',
462
+ kind: 'access_request',
463
+ });
464
+ expect(pending.length).toBe(1);
465
+ });
466
+
440
467
  test('notifyGuardianOfAccessRequest includes requestCode in contextPayload', () => {
441
468
  const result = notifyGuardianOfAccessRequest({
442
469
  canonicalAssistantId: 'self',
@@ -196,6 +196,50 @@ describe('notification decision strategy', () => {
196
196
  expect(copy.vellum!.body).toContain('open invite flow');
197
197
  });
198
198
 
199
+ test('ingress.access_request template includes caller name for voice-originated requests', () => {
200
+ // In production, senderIdentifier resolves to senderName for voice
201
+ // calls (senderName || senderUsername || senderExternalUserId), so
202
+ // both values are the caller's name. The phone number arrives via
203
+ // senderExternalUserId and should appear in the parenthetical.
204
+ const signal = makeSignal({
205
+ sourceEventName: 'ingress.access_request',
206
+ contextPayload: {
207
+ senderIdentifier: 'Alice Smith',
208
+ senderName: 'Alice Smith',
209
+ senderExternalUserId: '+15559998888',
210
+ sourceChannel: 'voice',
211
+ requestCode: 'V1C2E3',
212
+ },
213
+ });
214
+
215
+ const copy = composeFallbackCopy(signal, channels);
216
+ expect(copy.vellum).toBeDefined();
217
+ expect(copy.vellum!.title).toBe('Access Request');
218
+ // Voice-originated requests should include the caller name and phone number in parentheses
219
+ expect(copy.vellum!.body).toContain('Alice Smith');
220
+ expect(copy.vellum!.body).toContain('(+15559998888)');
221
+ expect(copy.vellum!.body).toContain('calling');
222
+ });
223
+
224
+ test('ingress.access_request template falls back to non-voice copy when sourceChannel is not voice', () => {
225
+ const signal = makeSignal({
226
+ sourceEventName: 'ingress.access_request',
227
+ contextPayload: {
228
+ senderIdentifier: 'user-123',
229
+ senderName: 'Bob Jones',
230
+ sourceChannel: 'telegram',
231
+ requestCode: 'T1G2M3',
232
+ },
233
+ });
234
+
235
+ const copy = composeFallbackCopy(signal, channels);
236
+ expect(copy.vellum).toBeDefined();
237
+ // Non-voice should use the standard "requesting access" text, not "calling"
238
+ expect(copy.vellum!.body).toContain('user-123');
239
+ expect(copy.vellum!.body).toContain('requesting access');
240
+ expect(copy.vellum!.body).not.toContain('calling');
241
+ });
242
+
199
243
  test('ingress.access_request Telegram deliveryText is concise', () => {
200
244
  const signal = makeSignal({
201
245
  sourceEventName: 'ingress.access_request',