@vellumai/assistant 0.3.4 → 0.3.5

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 (122) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +37 -2
  3. package/package.json +1 -1
  4. package/scripts/ipc/generate-swift.ts +13 -0
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
  6. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  7. package/src/__tests__/approval-message-composer.test.ts +253 -0
  8. package/src/__tests__/call-domain.test.ts +12 -2
  9. package/src/__tests__/call-orchestrator.test.ts +70 -1
  10. package/src/__tests__/call-routes-http.test.ts +27 -2
  11. package/src/__tests__/channel-approval-routes.test.ts +21 -17
  12. package/src/__tests__/channel-approvals.test.ts +48 -1
  13. package/src/__tests__/channel-guardian.test.ts +74 -22
  14. package/src/__tests__/channel-readiness-service.test.ts +257 -0
  15. package/src/__tests__/config-schema.test.ts +2 -1
  16. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  17. package/src/__tests__/daemon-lifecycle.test.ts +13 -12
  18. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  19. package/src/__tests__/entity-search.test.ts +615 -0
  20. package/src/__tests__/handlers-twilio-config.test.ts +407 -0
  21. package/src/__tests__/ipc-snapshot.test.ts +63 -0
  22. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  23. package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
  24. package/src/__tests__/run-orchestrator.test.ts +22 -0
  25. package/src/__tests__/session-runtime-assembly.test.ts +85 -1
  26. package/src/__tests__/sms-messaging-provider.test.ts +125 -0
  27. package/src/__tests__/twilio-routes.test.ts +39 -3
  28. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  29. package/src/__tests__/web-search.test.ts +1 -1
  30. package/src/__tests__/work-item-output.test.ts +110 -0
  31. package/src/calls/call-domain.ts +8 -5
  32. package/src/calls/call-orchestrator.ts +22 -11
  33. package/src/calls/twilio-config.ts +17 -11
  34. package/src/calls/twilio-rest.ts +276 -0
  35. package/src/calls/twilio-routes.ts +39 -1
  36. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  37. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  38. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  39. package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
  40. package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
  41. package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
  42. package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
  43. package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
  44. package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
  45. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
  46. package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
  47. package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
  48. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
  49. package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
  50. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
  51. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  52. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  53. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
  54. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  55. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
  56. package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
  57. package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
  58. package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
  59. package/src/config/bundled-skills/messaging/SKILL.md +21 -6
  60. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  61. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  62. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  63. package/src/config/defaults.ts +2 -1
  64. package/src/config/schema.ts +9 -3
  65. package/src/config/system-prompt.ts +24 -0
  66. package/src/config/templates/IDENTITY.md +2 -2
  67. package/src/config/vellum-skills/catalog.json +6 -0
  68. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
  69. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
  70. package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
  71. package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
  72. package/src/daemon/handlers/config.ts +783 -9
  73. package/src/daemon/handlers/dictation.ts +182 -0
  74. package/src/daemon/handlers/identity.ts +14 -23
  75. package/src/daemon/handlers/index.ts +2 -0
  76. package/src/daemon/handlers/sessions.ts +2 -0
  77. package/src/daemon/handlers/shared.ts +3 -0
  78. package/src/daemon/handlers/work-items.ts +15 -7
  79. package/src/daemon/ipc-contract-inventory.json +10 -0
  80. package/src/daemon/ipc-contract.ts +108 -4
  81. package/src/daemon/lifecycle.ts +2 -0
  82. package/src/daemon/ride-shotgun-handler.ts +1 -1
  83. package/src/daemon/server.ts +6 -2
  84. package/src/daemon/session-agent-loop.ts +5 -1
  85. package/src/daemon/session-runtime-assembly.ts +55 -0
  86. package/src/daemon/session-tool-setup.ts +2 -0
  87. package/src/daemon/session.ts +11 -1
  88. package/src/inbound/public-ingress-urls.ts +3 -3
  89. package/src/memory/channel-guardian-store.ts +2 -1
  90. package/src/memory/db-init.ts +144 -0
  91. package/src/memory/job-handlers/media-processing.ts +100 -0
  92. package/src/memory/jobs-store.ts +2 -1
  93. package/src/memory/jobs-worker.ts +4 -0
  94. package/src/memory/media-store.ts +759 -0
  95. package/src/memory/retriever.ts +6 -1
  96. package/src/memory/schema.ts +98 -0
  97. package/src/memory/search/entity.ts +208 -25
  98. package/src/memory/search/ranking.ts +6 -1
  99. package/src/memory/search/types.ts +24 -0
  100. package/src/messaging/provider-types.ts +2 -0
  101. package/src/messaging/providers/sms/adapter.ts +204 -0
  102. package/src/messaging/providers/sms/client.ts +93 -0
  103. package/src/messaging/providers/sms/types.ts +7 -0
  104. package/src/permissions/checker.ts +16 -2
  105. package/src/runtime/approval-message-composer.ts +143 -0
  106. package/src/runtime/channel-approvals.ts +12 -4
  107. package/src/runtime/channel-guardian-service.ts +44 -18
  108. package/src/runtime/channel-readiness-service.ts +292 -0
  109. package/src/runtime/channel-readiness-types.ts +29 -0
  110. package/src/runtime/http-server.ts +53 -27
  111. package/src/runtime/http-types.ts +3 -0
  112. package/src/runtime/routes/call-routes.ts +2 -1
  113. package/src/runtime/routes/channel-routes.ts +67 -21
  114. package/src/runtime/run-orchestrator.ts +35 -2
  115. package/src/tools/assets/materialize.ts +2 -2
  116. package/src/tools/calls/call-start.ts +1 -0
  117. package/src/tools/credentials/vault.ts +1 -1
  118. package/src/tools/execution-target.ts +11 -1
  119. package/src/tools/network/web-search.ts +1 -1
  120. package/src/tools/types.ts +2 -0
  121. package/src/twitter/router.ts +1 -1
  122. package/src/util/platform.ts +35 -0
@@ -523,6 +523,54 @@ describe('Twilio config handler', () => {
523
523
  }
524
524
  });
525
525
 
526
+ test('getTwilioConfig with assistantId prefers assistant-scoped mapping over global phone number', () => {
527
+ secureKeyStore['credential:twilio:account_sid'] = 'AC1234567890abcdef1234567890abcdef';
528
+ secureKeyStore['credential:twilio:auth_token'] = 'test_auth_token';
529
+ rawConfigStore = {
530
+ sms: {
531
+ phoneNumber: '+15551234567',
532
+ assistantPhoneNumbers: {
533
+ 'ast-alpha': '+15550000001',
534
+ },
535
+ },
536
+ ingress: { enabled: true, publicBaseUrl: 'https://test.ngrok.io' },
537
+ };
538
+
539
+ const savedEnv = process.env.TWILIO_PHONE_NUMBER;
540
+ delete process.env.TWILIO_PHONE_NUMBER;
541
+
542
+ try {
543
+ const config = getTwilioConfig('ast-alpha');
544
+ expect(config.phoneNumber).toBe('+15550000001');
545
+ } finally {
546
+ if (savedEnv !== undefined) process.env.TWILIO_PHONE_NUMBER = savedEnv;
547
+ }
548
+ });
549
+
550
+ test('getTwilioConfig with assistantId falls back to global number when mapping is missing', () => {
551
+ secureKeyStore['credential:twilio:account_sid'] = 'AC1234567890abcdef1234567890abcdef';
552
+ secureKeyStore['credential:twilio:auth_token'] = 'test_auth_token';
553
+ rawConfigStore = {
554
+ sms: {
555
+ phoneNumber: '+15551234567',
556
+ assistantPhoneNumbers: {
557
+ 'ast-alpha': '+15550000001',
558
+ },
559
+ },
560
+ ingress: { enabled: true, publicBaseUrl: 'https://test.ngrok.io' },
561
+ };
562
+
563
+ const savedEnv = process.env.TWILIO_PHONE_NUMBER;
564
+ delete process.env.TWILIO_PHONE_NUMBER;
565
+
566
+ try {
567
+ const config = getTwilioConfig('ast-beta');
568
+ expect(config.phoneNumber).toBe('+15551234567');
569
+ } finally {
570
+ if (savedEnv !== undefined) process.env.TWILIO_PHONE_NUMBER = savedEnv;
571
+ }
572
+ });
573
+
526
574
  // ── assign_number ───────────────────────────────────────────────────
527
575
 
528
576
  test('assign_number persists phone number to config', async () => {
@@ -1389,4 +1437,363 @@ describe('Twilio config handler', () => {
1389
1437
  expect(responseStr).not.toContain('AC_secret_account_sid_12345');
1390
1438
  expect(responseStr).not.toContain('secret_auth_token_67890');
1391
1439
  });
1440
+
1441
+ // ── sms_compliance_status ───────────────────────────────────────────
1442
+
1443
+ test('sms_compliance_status returns structured compliance data for toll-free number', async () => {
1444
+ secureKeyStore['credential:twilio:account_sid'] = 'AC1234567890abcdef1234567890abcdef';
1445
+ secureKeyStore['credential:twilio:auth_token'] = 'test_auth_token';
1446
+ rawConfigStore = { sms: { phoneNumber: '+18001234567' } };
1447
+
1448
+ globalThis.fetch = (async (url: string | URL | Request) => {
1449
+ const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url;
1450
+ // Phone number SID lookup
1451
+ if (urlStr.includes('IncomingPhoneNumbers.json') && urlStr.includes('PhoneNumber=')) {
1452
+ return new Response(JSON.stringify({
1453
+ incoming_phone_numbers: [{ sid: 'PN123abc', phone_number: '+18001234567' }],
1454
+ }), { status: 200 });
1455
+ }
1456
+ // Toll-free verification lookup
1457
+ if (urlStr.includes('Tollfree/Verifications')) {
1458
+ return new Response(JSON.stringify({
1459
+ verifications: [{
1460
+ sid: 'TF_VER_001',
1461
+ status: 'TWILIO_APPROVED',
1462
+ rejection_reason: null,
1463
+ edit_allowed: false,
1464
+ }],
1465
+ }), { status: 200 });
1466
+ }
1467
+ return originalFetch(url);
1468
+ }) as typeof fetch;
1469
+
1470
+ const msg: TwilioConfigRequest = {
1471
+ type: 'twilio_config',
1472
+ action: 'sms_compliance_status',
1473
+ };
1474
+
1475
+ const { ctx, sent } = createTestContext();
1476
+ await handleTwilioConfig(msg, {} as net.Socket, ctx);
1477
+
1478
+ expect(sent).toHaveLength(1);
1479
+ const res = sent[0] as { type: string; success: boolean; compliance?: Record<string, unknown> };
1480
+ expect(res.success).toBe(true);
1481
+ expect(res.compliance).toBeDefined();
1482
+ expect(res.compliance!.numberType).toBe('toll_free');
1483
+ expect(res.compliance!.verificationSid).toBe('TF_VER_001');
1484
+ expect(res.compliance!.verificationStatus).toBe('TWILIO_APPROVED');
1485
+ });
1486
+
1487
+ test('sms_compliance_status returns local_10dlc type for non-toll-free number without remote check', async () => {
1488
+ secureKeyStore['credential:twilio:account_sid'] = 'AC1234567890abcdef1234567890abcdef';
1489
+ secureKeyStore['credential:twilio:auth_token'] = 'test_auth_token';
1490
+ rawConfigStore = { sms: { phoneNumber: '+15551234567' } };
1491
+
1492
+ const msg: TwilioConfigRequest = {
1493
+ type: 'twilio_config',
1494
+ action: 'sms_compliance_status',
1495
+ };
1496
+
1497
+ const { ctx, sent } = createTestContext();
1498
+ await handleTwilioConfig(msg, {} as net.Socket, ctx);
1499
+
1500
+ expect(sent).toHaveLength(1);
1501
+ const res = sent[0] as { type: string; success: boolean; compliance?: Record<string, unknown> };
1502
+ expect(res.success).toBe(true);
1503
+ expect(res.compliance).toBeDefined();
1504
+ expect(res.compliance!.numberType).toBe('local_10dlc');
1505
+ // No verification fields for non-toll-free
1506
+ expect(res.compliance!.verificationSid).toBeUndefined();
1507
+ });
1508
+
1509
+ test('sms_compliance_status returns error when no credentials', async () => {
1510
+ const msg: TwilioConfigRequest = {
1511
+ type: 'twilio_config',
1512
+ action: 'sms_compliance_status',
1513
+ };
1514
+
1515
+ const { ctx, sent } = createTestContext();
1516
+ await handleTwilioConfig(msg, {} as net.Socket, ctx);
1517
+
1518
+ expect(sent).toHaveLength(1);
1519
+ const res = sent[0] as { type: string; success: boolean; error?: string };
1520
+ expect(res.success).toBe(false);
1521
+ expect(res.error).toContain('Twilio credentials not configured');
1522
+ });
1523
+
1524
+ // ── sms_submit_tollfree_verification ────────────────────────────────
1525
+
1526
+ test('sms_submit_tollfree_verification validates required fields', async () => {
1527
+ secureKeyStore['credential:twilio:account_sid'] = 'AC1234567890abcdef1234567890abcdef';
1528
+ secureKeyStore['credential:twilio:auth_token'] = 'test_auth_token';
1529
+
1530
+ const msg: TwilioConfigRequest = {
1531
+ type: 'twilio_config',
1532
+ action: 'sms_submit_tollfree_verification',
1533
+ verificationParams: {
1534
+ // Missing all required fields
1535
+ businessName: 'Test Biz',
1536
+ },
1537
+ };
1538
+
1539
+ const { ctx, sent } = createTestContext();
1540
+ await handleTwilioConfig(msg, {} as net.Socket, ctx);
1541
+
1542
+ expect(sent).toHaveLength(1);
1543
+ const res = sent[0] as { type: string; success: boolean; error?: string };
1544
+ expect(res.success).toBe(false);
1545
+ expect(res.error).toContain('Missing required verification fields');
1546
+ expect(res.error).toContain('tollfreePhoneNumberSid');
1547
+ });
1548
+
1549
+ test('sms_submit_tollfree_verification defaults businessType to SOLE_PROPRIETOR', async () => {
1550
+ secureKeyStore['credential:twilio:account_sid'] = 'AC1234567890abcdef1234567890abcdef';
1551
+ secureKeyStore['credential:twilio:auth_token'] = 'test_auth_token';
1552
+
1553
+ let capturedBody = '';
1554
+ globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
1555
+ const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url;
1556
+ if (urlStr.includes('Tollfree/Verifications') && init?.method === 'POST') {
1557
+ capturedBody = init?.body?.toString() ?? '';
1558
+ return new Response(JSON.stringify({
1559
+ sid: 'TF_VER_NEW',
1560
+ status: 'PENDING_REVIEW',
1561
+ }), { status: 201 });
1562
+ }
1563
+ return originalFetch(url);
1564
+ }) as typeof fetch;
1565
+
1566
+ const msg: TwilioConfigRequest = {
1567
+ type: 'twilio_config',
1568
+ action: 'sms_submit_tollfree_verification',
1569
+ verificationParams: {
1570
+ tollfreePhoneNumberSid: 'PN123',
1571
+ businessName: 'Test Biz',
1572
+ businessWebsite: 'https://test.com',
1573
+ notificationEmail: 'test@test.com',
1574
+ useCaseCategories: ['CUSTOMER_CARE'],
1575
+ useCaseSummary: 'Customer support messages',
1576
+ productionMessageSample: 'Your order has shipped!',
1577
+ optInImageUrls: ['https://test.com/optin.png'],
1578
+ optInType: 'WEB_FORM',
1579
+ messageVolume: '100',
1580
+ // businessType NOT provided — should default to SOLE_PROPRIETOR
1581
+ },
1582
+ };
1583
+
1584
+ const { ctx, sent } = createTestContext();
1585
+ await handleTwilioConfig(msg, {} as net.Socket, ctx);
1586
+
1587
+ expect(sent).toHaveLength(1);
1588
+ const res = sent[0] as { type: string; success: boolean; compliance?: Record<string, unknown> };
1589
+ expect(res.success).toBe(true);
1590
+ expect(res.compliance).toBeDefined();
1591
+ expect(res.compliance!.verificationSid).toBe('TF_VER_NEW');
1592
+ expect(res.compliance!.verificationStatus).toBe('PENDING_REVIEW');
1593
+
1594
+ // Verify the default businessType was sent to Twilio
1595
+ expect(capturedBody).toContain('BusinessType=SOLE_PROPRIETOR');
1596
+ });
1597
+
1598
+ test('sms_submit_tollfree_verification rejects invalid useCaseCategories', async () => {
1599
+ secureKeyStore['credential:twilio:account_sid'] = 'AC1234567890abcdef1234567890abcdef';
1600
+ secureKeyStore['credential:twilio:auth_token'] = 'test_auth_token';
1601
+
1602
+ const msg: TwilioConfigRequest = {
1603
+ type: 'twilio_config',
1604
+ action: 'sms_submit_tollfree_verification',
1605
+ verificationParams: {
1606
+ tollfreePhoneNumberSid: 'PN123',
1607
+ businessName: 'Test Biz',
1608
+ businessWebsite: 'https://test.com',
1609
+ notificationEmail: 'test@test.com',
1610
+ useCaseCategories: ['INVALID_CATEGORY'],
1611
+ useCaseSummary: 'Test',
1612
+ productionMessageSample: 'Test message',
1613
+ optInImageUrls: ['https://test.com/optin.png'],
1614
+ optInType: 'WEB_FORM',
1615
+ messageVolume: '100',
1616
+ },
1617
+ };
1618
+
1619
+ const { ctx, sent } = createTestContext();
1620
+ await handleTwilioConfig(msg, {} as net.Socket, ctx);
1621
+
1622
+ expect(sent).toHaveLength(1);
1623
+ const res = sent[0] as { type: string; success: boolean; error?: string };
1624
+ expect(res.success).toBe(false);
1625
+ expect(res.error).toContain('Invalid useCaseCategories');
1626
+ expect(res.error).toContain('INVALID_CATEGORY');
1627
+ });
1628
+
1629
+ test('sms_submit_tollfree_verification returns error without verificationParams', async () => {
1630
+ secureKeyStore['credential:twilio:account_sid'] = 'AC1234567890abcdef1234567890abcdef';
1631
+ secureKeyStore['credential:twilio:auth_token'] = 'test_auth_token';
1632
+
1633
+ const msg: TwilioConfigRequest = {
1634
+ type: 'twilio_config',
1635
+ action: 'sms_submit_tollfree_verification',
1636
+ };
1637
+
1638
+ const { ctx, sent } = createTestContext();
1639
+ await handleTwilioConfig(msg, {} as net.Socket, ctx);
1640
+
1641
+ expect(sent).toHaveLength(1);
1642
+ const res = sent[0] as { type: string; success: boolean; error?: string };
1643
+ expect(res.success).toBe(false);
1644
+ expect(res.error).toContain('verificationParams is required');
1645
+ });
1646
+
1647
+ // ── release_number ─────────────────────────────────────────────────
1648
+
1649
+ test('release_number clears config and secure keys', async () => {
1650
+ secureKeyStore['credential:twilio:account_sid'] = 'AC1234567890abcdef1234567890abcdef';
1651
+ secureKeyStore['credential:twilio:auth_token'] = 'test_auth_token';
1652
+ secureKeyStore['credential:twilio:phone_number'] = '+15551234567';
1653
+ rawConfigStore = {
1654
+ sms: {
1655
+ phoneNumber: '+15551234567',
1656
+ assistantPhoneNumbers: { 'ast-alpha': '+15551234567' },
1657
+ },
1658
+ };
1659
+
1660
+ globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
1661
+ const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url;
1662
+ // Phone number SID lookup
1663
+ if (urlStr.includes('IncomingPhoneNumbers.json') && urlStr.includes('PhoneNumber=')) {
1664
+ return new Response(JSON.stringify({
1665
+ incoming_phone_numbers: [{ sid: 'PN123abc', phone_number: '+15551234567' }],
1666
+ }), { status: 200 });
1667
+ }
1668
+ // Phone number deletion
1669
+ if (urlStr.includes('IncomingPhoneNumbers/PN123abc.json') && init?.method === 'DELETE') {
1670
+ return new Response('', { status: 204 });
1671
+ }
1672
+ return originalFetch(url);
1673
+ }) as typeof fetch;
1674
+
1675
+ const msg: TwilioConfigRequest = {
1676
+ type: 'twilio_config',
1677
+ action: 'release_number',
1678
+ };
1679
+
1680
+ const { ctx, sent } = createTestContext();
1681
+ await handleTwilioConfig(msg, {} as net.Socket, ctx);
1682
+
1683
+ expect(sent).toHaveLength(1);
1684
+ const res = sent[0] as { type: string; success: boolean; warning?: string };
1685
+ expect(res.success).toBe(true);
1686
+ expect(res.warning).toContain('Phone number released');
1687
+
1688
+ // Verify config was cleared
1689
+ const sms = rawConfigStore.sms as Record<string, unknown>;
1690
+ expect(sms.phoneNumber).toBeUndefined();
1691
+ expect(sms.assistantPhoneNumbers).toBeUndefined();
1692
+
1693
+ // Verify secure key was cleared
1694
+ expect(secureKeyStore['credential:twilio:phone_number']).toBeUndefined();
1695
+ });
1696
+
1697
+ test('release_number returns error when no credentials', async () => {
1698
+ rawConfigStore = { sms: { phoneNumber: '+15551234567' } };
1699
+
1700
+ const msg: TwilioConfigRequest = {
1701
+ type: 'twilio_config',
1702
+ action: 'release_number',
1703
+ };
1704
+
1705
+ const { ctx, sent } = createTestContext();
1706
+ await handleTwilioConfig(msg, {} as net.Socket, ctx);
1707
+
1708
+ expect(sent).toHaveLength(1);
1709
+ const res = sent[0] as { type: string; success: boolean; error?: string };
1710
+ expect(res.success).toBe(false);
1711
+ expect(res.error).toContain('Twilio credentials not configured');
1712
+ });
1713
+
1714
+ test('release_number returns error when no phone number assigned', async () => {
1715
+ secureKeyStore['credential:twilio:account_sid'] = 'AC1234567890abcdef1234567890abcdef';
1716
+ secureKeyStore['credential:twilio:auth_token'] = 'test_auth_token';
1717
+ rawConfigStore = {};
1718
+
1719
+ const msg: TwilioConfigRequest = {
1720
+ type: 'twilio_config',
1721
+ action: 'release_number',
1722
+ };
1723
+
1724
+ const { ctx, sent } = createTestContext();
1725
+ await handleTwilioConfig(msg, {} as net.Socket, ctx);
1726
+
1727
+ expect(sent).toHaveLength(1);
1728
+ const res = sent[0] as { type: string; success: boolean; error?: string };
1729
+ expect(res.success).toBe(false);
1730
+ expect(res.error).toContain('No phone number to release');
1731
+ });
1732
+
1733
+ // ── sms_delete_tollfree_verification ────────────────────────────────
1734
+
1735
+ test('sms_delete_tollfree_verification requires verificationSid', async () => {
1736
+ secureKeyStore['credential:twilio:account_sid'] = 'AC1234567890abcdef1234567890abcdef';
1737
+ secureKeyStore['credential:twilio:auth_token'] = 'test_auth_token';
1738
+
1739
+ const msg: TwilioConfigRequest = {
1740
+ type: 'twilio_config',
1741
+ action: 'sms_delete_tollfree_verification',
1742
+ };
1743
+
1744
+ const { ctx, sent } = createTestContext();
1745
+ await handleTwilioConfig(msg, {} as net.Socket, ctx);
1746
+
1747
+ expect(sent).toHaveLength(1);
1748
+ const res = sent[0] as { type: string; success: boolean; error?: string };
1749
+ expect(res.success).toBe(false);
1750
+ expect(res.error).toContain('verificationSid is required');
1751
+ });
1752
+
1753
+ test('sms_delete_tollfree_verification includes queue warning', async () => {
1754
+ secureKeyStore['credential:twilio:account_sid'] = 'AC1234567890abcdef1234567890abcdef';
1755
+ secureKeyStore['credential:twilio:auth_token'] = 'test_auth_token';
1756
+
1757
+ globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
1758
+ const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url;
1759
+ if (urlStr.includes('Tollfree/Verifications/TF_VER_001') && init?.method === 'DELETE') {
1760
+ return new Response('', { status: 204 });
1761
+ }
1762
+ return originalFetch(url);
1763
+ }) as typeof fetch;
1764
+
1765
+ const msg: TwilioConfigRequest = {
1766
+ type: 'twilio_config',
1767
+ action: 'sms_delete_tollfree_verification',
1768
+ verificationSid: 'TF_VER_001',
1769
+ };
1770
+
1771
+ const { ctx, sent } = createTestContext();
1772
+ await handleTwilioConfig(msg, {} as net.Socket, ctx);
1773
+
1774
+ expect(sent).toHaveLength(1);
1775
+ const res = sent[0] as { type: string; success: boolean; warning?: string };
1776
+ expect(res.success).toBe(true);
1777
+ expect(res.warning).toContain('review queue');
1778
+ });
1779
+
1780
+ // ── sms_update_tollfree_verification ────────────────────────────────
1781
+
1782
+ test('sms_update_tollfree_verification requires verificationSid', async () => {
1783
+ secureKeyStore['credential:twilio:account_sid'] = 'AC1234567890abcdef1234567890abcdef';
1784
+ secureKeyStore['credential:twilio:auth_token'] = 'test_auth_token';
1785
+
1786
+ const msg: TwilioConfigRequest = {
1787
+ type: 'twilio_config',
1788
+ action: 'sms_update_tollfree_verification',
1789
+ };
1790
+
1791
+ const { ctx, sent } = createTestContext();
1792
+ await handleTwilioConfig(msg, {} as net.Socket, ctx);
1793
+
1794
+ expect(sent).toHaveLength(1);
1795
+ const res = sent[0] as { type: string; success: boolean; error?: string };
1796
+ expect(res.success).toBe(false);
1797
+ expect(res.error).toContain('verificationSid is required');
1798
+ });
1392
1799
  });
@@ -382,6 +382,12 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
382
382
  type: 'twilio_config',
383
383
  action: 'get',
384
384
  },
385
+ channel_readiness: {
386
+ type: 'channel_readiness',
387
+ action: 'get',
388
+ channel: 'sms',
389
+ includeRemote: true,
390
+ },
385
391
  guardian_verification: {
386
392
  type: 'guardian_verification',
387
393
  action: 'create_challenge',
@@ -564,6 +570,17 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
564
570
  tool_names_list: {
565
571
  type: 'tool_names_list',
566
572
  },
573
+ dictation_request: {
574
+ type: 'dictation_request',
575
+ transcription: 'Hello world',
576
+ context: {
577
+ bundleIdentifier: 'com.example.app',
578
+ appName: 'Example App',
579
+ windowTitle: 'Main Window',
580
+ selectedText: 'some selected text',
581
+ cursorInTextField: true,
582
+ },
583
+ },
567
584
  };
568
585
 
569
586
  // ---------------------------------------------------------------------------
@@ -824,6 +841,11 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
824
841
  sessionId: 'sess-routed-001',
825
842
  interactionType: 'computer_use',
826
843
  },
844
+ ride_shotgun_progress: {
845
+ type: 'ride_shotgun_progress',
846
+ watchId: 'watch-shotgun-001',
847
+ message: 'Observing user activity...',
848
+ },
827
849
  ride_shotgun_result: {
828
850
  type: 'ride_shotgun_result',
829
851
  sessionId: 'sess-shotgun-001',
@@ -1227,6 +1249,41 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
1227
1249
  success: true,
1228
1250
  hasCredentials: true,
1229
1251
  phoneNumber: '+15551234567',
1252
+ compliance: {
1253
+ numberType: 'toll_free',
1254
+ verificationSid: 'TF_VER_001',
1255
+ verificationStatus: 'TWILIO_APPROVED',
1256
+ },
1257
+ testResult: {
1258
+ messageSid: 'SM-test-001',
1259
+ to: '+15559876543',
1260
+ initialStatus: 'queued',
1261
+ finalStatus: 'delivered',
1262
+ },
1263
+ diagnostics: {
1264
+ readiness: { ready: true, issues: [] },
1265
+ compliance: { status: 'TWILIO_APPROVED', detail: 'Toll-free verification: TWILIO_APPROVED' },
1266
+ overallStatus: 'healthy',
1267
+ actionItems: [],
1268
+ },
1269
+ },
1270
+ channel_readiness_response: {
1271
+ type: 'channel_readiness_response',
1272
+ success: true,
1273
+ snapshots: [
1274
+ {
1275
+ channel: 'sms',
1276
+ ready: false,
1277
+ checkedAt: 1700000000000,
1278
+ stale: false,
1279
+ reasons: [{ code: 'twilio_credentials', text: 'Twilio credentials are not configured' }],
1280
+ localChecks: [
1281
+ { name: 'twilio_credentials', passed: false, message: 'Twilio credentials are not configured' },
1282
+ { name: 'phone_number', passed: true, message: 'Phone number is assigned' },
1283
+ { name: 'ingress', passed: true, message: 'Public ingress URL is configured' },
1284
+ ],
1285
+ },
1286
+ ],
1230
1287
  },
1231
1288
  guardian_verification_response: {
1232
1289
  type: 'guardian_verification_response',
@@ -1608,6 +1665,12 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
1608
1665
  type: 'tool_names_list_response',
1609
1666
  names: ['bash', 'file_read', 'file_write'],
1610
1667
  },
1668
+ dictation_response: {
1669
+ type: 'dictation_response',
1670
+ text: 'Hello world',
1671
+ mode: 'dictation',
1672
+ actionPlan: undefined,
1673
+ },
1611
1674
  };
1612
1675
 
1613
1676
  // ---------------------------------------------------------------------------
@@ -0,0 +1,65 @@
1
+ import { beforeEach, describe, expect, mock, test } from 'bun:test';
2
+ import type { MessagingProvider } from '../messaging/provider.js';
3
+ import type { SendOptions } from '../messaging/provider-types.js';
4
+
5
+ const sendMessageMock = mock(async (..._args: unknown[]) => ({
6
+ id: 'msg-1',
7
+ timestamp: 123,
8
+ conversationId: 'conv-1',
9
+ }));
10
+
11
+ const provider: MessagingProvider = {
12
+ id: 'sms',
13
+ displayName: 'SMS',
14
+ credentialService: 'twilio',
15
+ capabilities: new Set(['send']),
16
+ testConnection: async () => ({ connected: true, user: 'x', platform: 'sms' }),
17
+ listConversations: async () => [],
18
+ getHistory: async () => [],
19
+ search: async () => ({ total: 0, messages: [], hasMore: false }),
20
+ sendMessage: (token: string, conversationId: string, text: string, options?: SendOptions) =>
21
+ sendMessageMock(token, conversationId, text, options),
22
+ };
23
+
24
+ mock.module('../config/bundled-skills/messaging/tools/shared.js', () => ({
25
+ resolveProvider: () => provider,
26
+ withProviderToken: async (_provider: MessagingProvider, fn: (token: string) => Promise<unknown>) => fn('provider-token'),
27
+ ok: (content: string) => ({ content, isError: false }),
28
+ err: (content: string) => ({ content, isError: true }),
29
+ }));
30
+
31
+ import { run } from '../config/bundled-skills/messaging/tools/messaging-send.js';
32
+
33
+ describe('messaging-send tool', () => {
34
+ beforeEach(() => {
35
+ sendMessageMock.mockClear();
36
+ });
37
+
38
+ test('passes assistantId from tool context to provider send options', async () => {
39
+ const result = await run(
40
+ {
41
+ platform: 'sms',
42
+ conversation_id: '+15550004444',
43
+ text: 'test message',
44
+ },
45
+ {
46
+ workingDir: '/tmp',
47
+ sessionId: 'sess-1',
48
+ conversationId: 'conv-1',
49
+ assistantId: 'ast-alpha',
50
+ },
51
+ );
52
+
53
+ expect(result.isError).toBe(false);
54
+ expect(sendMessageMock).toHaveBeenCalledWith(
55
+ 'provider-token',
56
+ '+15550004444',
57
+ 'test message',
58
+ {
59
+ subject: undefined,
60
+ inReplyTo: undefined,
61
+ assistantId: 'ast-alpha',
62
+ },
63
+ );
64
+ });
65
+ });
@@ -68,6 +68,8 @@ function makeSessionEmittingViaClient(...messages: ServerMessage[]): Session {
68
68
  persistUserMessage: () => undefined as unknown as string,
69
69
  memoryPolicy: { scopeId: 'default', includeDefaultFallback: false, strictSideEffects: false },
70
70
  setChannelCapabilities: () => {},
71
+ setAssistantId: () => {},
72
+ setGuardianContext: () => {},
71
73
  updateClient: (handler: (msg: ServerMessage) => void) => {
72
74
  clientHandler = handler;
73
75
  },
@@ -90,6 +92,8 @@ function makeSessionEmittingViaAgentLoop(...messages: ServerMessage[]): Session
90
92
  persistUserMessage: () => undefined as unknown as string,
91
93
  memoryPolicy: { scopeId: 'default', includeDefaultFallback: false, strictSideEffects: false },
92
94
  setChannelCapabilities: () => {},
95
+ setAssistantId: () => {},
96
+ setGuardianContext: () => {},
93
97
  updateClient: () => {},
94
98
  runAgentLoop: async (_content: string, _messageId: string, onEvent: (msg: ServerMessage) => void) => {
95
99
  for (const msg of messages) {
@@ -26,6 +26,12 @@ mock.module('../util/logger.js', () => ({
26
26
  }),
27
27
  }));
28
28
 
29
+ mock.module('../config/loader.js', () => ({
30
+ getConfig: () => ({
31
+ secretDetection: { enabled: false },
32
+ }),
33
+ }));
34
+
29
35
  import { initializeDb, getDb, resetDb } from '../memory/db.js';
30
36
  import { createConversation } from '../memory/conversation-store.js';
31
37
  import { createRun, getRun, setRunConfirmation } from '../memory/runs-store.js';
@@ -43,6 +49,8 @@ function makeSessionWithConfirmation(message: ServerMessage): Session {
43
49
  persistUserMessage: () => undefined as unknown as string,
44
50
  memoryPolicy: { scopeId: 'default', includeDefaultFallback: false, strictSideEffects: false },
45
51
  setChannelCapabilities: () => {},
52
+ setAssistantId: () => {},
53
+ setGuardianContext: () => {},
46
54
  updateClient: (handler: (msg: ServerMessage) => void) => {
47
55
  clientHandler = handler;
48
56
  },
@@ -64,6 +72,8 @@ function makeSessionWithEvent(message: ServerMessage): Session {
64
72
  persistUserMessage: () => undefined as unknown as string,
65
73
  memoryPolicy: { scopeId: 'default', includeDefaultFallback: false, strictSideEffects: false },
66
74
  setChannelCapabilities: () => {},
75
+ setAssistantId: () => {},
76
+ setGuardianContext: () => {},
67
77
  updateClient: () => {},
68
78
  runAgentLoop: async (_content: string, _messageId: string, onEvent: (msg: ServerMessage) => void) => {
69
79
  onEvent(message);
@@ -228,6 +238,8 @@ describe('startRun channel capability resolution', () => {
228
238
  setChannelCapabilities: (caps: ChannelCapabilities | null) => {
229
239
  if (caps) capturedCapabilities = caps;
230
240
  },
241
+ setAssistantId: () => {},
242
+ setGuardianContext: () => {},
231
243
  updateClient: () => {},
232
244
  runAgentLoop: async () => {},
233
245
  handleConfirmationResponse: () => {},
@@ -262,6 +274,8 @@ describe('startRun channel capability resolution', () => {
262
274
  setChannelCapabilities: (caps: ChannelCapabilities | null) => {
263
275
  if (caps) capturedCapabilities = caps;
264
276
  },
277
+ setAssistantId: () => {},
278
+ setGuardianContext: () => {},
265
279
  updateClient: () => {},
266
280
  runAgentLoop: async () => {},
267
281
  handleConfirmationResponse: () => {},
@@ -292,6 +306,8 @@ describe('startRun channel capability resolution', () => {
292
306
  setChannelCapabilities: (caps: ChannelCapabilities | null) => {
293
307
  if (caps) capturedCapabilities = caps;
294
308
  },
309
+ setAssistantId: () => {},
310
+ setGuardianContext: () => {},
295
311
  updateClient: () => {},
296
312
  runAgentLoop: async () => {},
297
313
  handleConfirmationResponse: () => {},
@@ -335,6 +351,8 @@ describe('strictSideEffects re-derivation across runs', () => {
335
351
  persistUserMessage: () => undefined as unknown as string,
336
352
  memoryPolicy: { scopeId: 'default', includeDefaultFallback: false, strictSideEffects: false },
337
353
  setChannelCapabilities: () => {},
354
+ setAssistantId: () => {},
355
+ setGuardianContext: () => {},
338
356
  updateClient: () => {},
339
357
  runAgentLoop: async () => {},
340
358
  handleConfirmationResponse: () => {},
@@ -369,6 +387,8 @@ describe('strictSideEffects re-derivation across runs', () => {
369
387
  persistUserMessage: () => undefined as unknown as string,
370
388
  memoryPolicy: { scopeId: 'private-scope', includeDefaultFallback: true, strictSideEffects: true },
371
389
  setChannelCapabilities: () => {},
390
+ setAssistantId: () => {},
391
+ setGuardianContext: () => {},
372
392
  updateClient: () => {},
373
393
  runAgentLoop: async () => {},
374
394
  handleConfirmationResponse: () => {},
@@ -404,6 +424,8 @@ describe('strictSideEffects re-derivation across runs', () => {
404
424
  persistUserMessage: () => undefined as unknown as string,
405
425
  memoryPolicy: { scopeId: 'default', includeDefaultFallback: false, strictSideEffects: true },
406
426
  setChannelCapabilities: () => {},
427
+ setAssistantId: () => {},
428
+ setGuardianContext: () => {},
407
429
  updateClient: () => {},
408
430
  runAgentLoop: async () => {},
409
431
  handleConfirmationResponse: () => {},