@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
@@ -4,14 +4,18 @@ import { initializeProviders } from '../../providers/registry.js';
4
4
  import { addRule, removeRule, updateRule, getAllRules, acceptStarterBundle } from '../../permissions/trust-store.js';
5
5
  import { classifyRisk, check, generateAllowlistOptions, generateScopeOptions } from '../../permissions/checker.js';
6
6
  import { isSideEffectTool } from '../../tools/executor.js';
7
- import { resolveExecutionTarget } from '../../tools/execution-target.js';
8
- import { getAllTools } from '../../tools/registry.js';
7
+ import { resolveExecutionTarget, type ManifestOverride } from '../../tools/execution-target.js';
8
+ import { getAllTools, getTool } from '../../tools/registry.js';
9
+ import { loadSkillCatalog } from '../../config/skills.js';
10
+ import { parseToolManifestFile } from '../../skills/tool-manifest.js';
11
+ import { join } from 'node:path';
9
12
  import { listSchedules, updateSchedule, deleteSchedule, describeCronExpression } from '../../schedule/schedule-store.js';
10
13
  import { listReminders, cancelReminder } from '../../tools/reminder/reminder-store.js';
11
14
  import { getSecureKey, setSecureKey, deleteSecureKey } from '../../security/secure-keys.js';
12
15
  import { upsertCredentialMetadata, deleteCredentialMetadata, getCredentialMetadata } from '../../tools/credentials/metadata-store.js';
13
16
  import { postToSlackWebhook } from '../../slack/slack-webhook.js';
14
17
  import { getApp } from '../../memory/app-store.js';
18
+ import * as externalConversationStore from '../../memory/external-conversation-store.js';
15
19
  import { readHttpToken } from '../../util/platform.js';
16
20
  import type {
17
21
  ModelSetRequest,
@@ -29,6 +33,7 @@ import type {
29
33
  TwitterIntegrationConfigRequest,
30
34
  TelegramConfigRequest,
31
35
  TwilioConfigRequest,
36
+ ChannelReadinessRequest,
32
37
  GuardianVerificationRequest,
33
38
  ToolPermissionSimulateRequest,
34
39
  } from '../ipc-protocol.js';
@@ -38,6 +43,14 @@ import {
38
43
  searchAvailableNumbers,
39
44
  provisionPhoneNumber,
40
45
  updatePhoneNumberWebhooks,
46
+ getTollFreeVerificationStatus,
47
+ submitTollFreeVerification,
48
+ updateTollFreeVerification,
49
+ deleteTollFreeVerification,
50
+ getPhoneNumberSid,
51
+ releasePhoneNumber,
52
+ fetchMessageStatus,
53
+ type TollFreeVerificationSubmitParams,
41
54
  } from '../../calls/twilio-rest.js';
42
55
  import {
43
56
  getTwilioVoiceWebhookUrl,
@@ -46,6 +59,7 @@ import {
46
59
  type IngressConfig,
47
60
  } from '../../inbound/public-ingress-urls.js';
48
61
  import { createVerificationChallenge, getGuardianBinding, revokeBinding as revokeGuardianBinding } from '../../runtime/channel-guardian-service.js';
62
+ import { createReadinessService, type ChannelReadinessService } from '../../runtime/channel-readiness-service.js';
49
63
  import { log, CONFIG_RELOAD_DEBOUNCE_MS, defineHandlers, type HandlerContext } from './shared.js';
50
64
  import { MODEL_TO_PROVIDER } from '../session-slash.js';
51
65
 
@@ -406,7 +420,7 @@ export async function handleShareToSlack(
406
420
  ctx.send(socket, {
407
421
  type: 'share_to_slack_response',
408
422
  success: false,
409
- error: 'No Slack webhook URL configured. Set one in Settings.',
423
+ error: 'No Slack webhook URL configured. Provide one here in the chat, or set it from the Settings page.',
410
424
  });
411
425
  return;
412
426
  }
@@ -1175,6 +1189,32 @@ export async function handleTelegramConfig(
1175
1189
  }
1176
1190
  }
1177
1191
 
1192
+ /** In-memory store for the last SMS send test result. Shared between sms_send_test and sms_doctor. */
1193
+ let _lastTestResult: {
1194
+ messageSid: string;
1195
+ to: string;
1196
+ initialStatus: string;
1197
+ finalStatus: string;
1198
+ errorCode?: string;
1199
+ errorMessage?: string;
1200
+ timestamp: number;
1201
+ } | undefined;
1202
+
1203
+ /** Map a Twilio error code to a human-readable remediation suggestion. */
1204
+ function mapTwilioErrorRemediation(errorCode: string | undefined): string | undefined {
1205
+ if (!errorCode) return undefined;
1206
+ const map: Record<string, string> = {
1207
+ '30003': 'Unreachable destination. The handset may be off or out of service.',
1208
+ '30004': 'Message blocked by carrier or recipient.',
1209
+ '30005': 'Unknown destination phone number. Verify the number is valid.',
1210
+ '30006': 'Landline or unreachable carrier. SMS cannot be delivered to this number.',
1211
+ '30007': 'Message flagged as spam by carrier. Adjust content or register for A2P.',
1212
+ '30008': 'Unknown error from the carrier network.',
1213
+ '21610': 'Recipient has opted out (STOP). Cannot send until they opt back in.',
1214
+ };
1215
+ return map[errorCode];
1216
+ }
1217
+
1178
1218
  export async function handleTwilioConfig(
1179
1219
  msg: TwilioConfigRequest,
1180
1220
  socket: net.Socket,
@@ -1475,6 +1515,608 @@ export async function handleTwilioConfig(
1475
1515
  hasCredentials: true,
1476
1516
  numbers,
1477
1517
  });
1518
+ } else if (msg.action === 'sms_compliance_status') {
1519
+ if (!hasTwilioCredentials()) {
1520
+ ctx.send(socket, {
1521
+ type: 'twilio_config_response',
1522
+ success: false,
1523
+ hasCredentials: false,
1524
+ error: 'Twilio credentials not configured. Set credentials first.',
1525
+ });
1526
+ return;
1527
+ }
1528
+
1529
+ const raw = loadRawConfig();
1530
+ const sms = (raw?.sms ?? {}) as Record<string, unknown>;
1531
+ let phoneNumber: string;
1532
+ if (msg.assistantId) {
1533
+ const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
1534
+ phoneNumber = mapping[msg.assistantId] ?? (sms.phoneNumber as string) ?? '';
1535
+ } else {
1536
+ phoneNumber = (sms.phoneNumber as string) ?? '';
1537
+ }
1538
+
1539
+ if (!phoneNumber) {
1540
+ ctx.send(socket, {
1541
+ type: 'twilio_config_response',
1542
+ success: false,
1543
+ hasCredentials: true,
1544
+ error: 'No phone number assigned. Assign a number first.',
1545
+ });
1546
+ return;
1547
+ }
1548
+
1549
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
1550
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
1551
+
1552
+ // Determine number type from prefix
1553
+ const tollFreePrefixes = ['+1800', '+1833', '+1844', '+1855', '+1866', '+1877', '+1888'];
1554
+ const isTollFree = tollFreePrefixes.some((prefix) => phoneNumber.startsWith(prefix));
1555
+ const numberType = isTollFree ? 'toll_free' : 'local_10dlc';
1556
+
1557
+ if (!isTollFree) {
1558
+ // Non-toll-free numbers don't need toll-free verification
1559
+ ctx.send(socket, {
1560
+ type: 'twilio_config_response',
1561
+ success: true,
1562
+ hasCredentials: true,
1563
+ phoneNumber,
1564
+ compliance: { numberType },
1565
+ });
1566
+ return;
1567
+ }
1568
+
1569
+ // Look up the phone number SID and check verification status
1570
+ const phoneSid = await getPhoneNumberSid(accountSid, authToken, phoneNumber);
1571
+ if (!phoneSid) {
1572
+ ctx.send(socket, {
1573
+ type: 'twilio_config_response',
1574
+ success: false,
1575
+ hasCredentials: true,
1576
+ phoneNumber,
1577
+ error: `Phone number ${phoneNumber} not found on Twilio account`,
1578
+ });
1579
+ return;
1580
+ }
1581
+
1582
+ const verification = await getTollFreeVerificationStatus(accountSid, authToken, phoneSid);
1583
+
1584
+ ctx.send(socket, {
1585
+ type: 'twilio_config_response',
1586
+ success: true,
1587
+ hasCredentials: true,
1588
+ phoneNumber,
1589
+ compliance: {
1590
+ numberType,
1591
+ verificationSid: verification?.sid,
1592
+ verificationStatus: verification?.status,
1593
+ rejectionReason: verification?.rejectionReason,
1594
+ rejectionReasons: verification?.rejectionReasons,
1595
+ errorCode: verification?.errorCode,
1596
+ editAllowed: verification?.editAllowed,
1597
+ editExpiration: verification?.editExpiration,
1598
+ },
1599
+ });
1600
+ } else if (msg.action === 'sms_submit_tollfree_verification') {
1601
+ if (!hasTwilioCredentials()) {
1602
+ ctx.send(socket, {
1603
+ type: 'twilio_config_response',
1604
+ success: false,
1605
+ hasCredentials: false,
1606
+ error: 'Twilio credentials not configured. Set credentials first.',
1607
+ });
1608
+ return;
1609
+ }
1610
+
1611
+ const vp = msg.verificationParams;
1612
+ if (!vp) {
1613
+ ctx.send(socket, {
1614
+ type: 'twilio_config_response',
1615
+ success: false,
1616
+ hasCredentials: true,
1617
+ error: 'verificationParams is required for sms_submit_tollfree_verification action',
1618
+ });
1619
+ return;
1620
+ }
1621
+
1622
+ // Validate required fields
1623
+ const requiredFields: Array<[string, unknown]> = [
1624
+ ['tollfreePhoneNumberSid', vp.tollfreePhoneNumberSid],
1625
+ ['businessName', vp.businessName],
1626
+ ['businessWebsite', vp.businessWebsite],
1627
+ ['notificationEmail', vp.notificationEmail],
1628
+ ['useCaseCategories', vp.useCaseCategories],
1629
+ ['useCaseSummary', vp.useCaseSummary],
1630
+ ['productionMessageSample', vp.productionMessageSample],
1631
+ ['optInImageUrls', vp.optInImageUrls],
1632
+ ['optInType', vp.optInType],
1633
+ ['messageVolume', vp.messageVolume],
1634
+ ];
1635
+
1636
+ const missing = requiredFields
1637
+ .filter(([, v]) => v === undefined || v === null || v === '' || (Array.isArray(v) && v.length === 0))
1638
+ .map(([name]) => name);
1639
+
1640
+ if (missing.length > 0) {
1641
+ ctx.send(socket, {
1642
+ type: 'twilio_config_response',
1643
+ success: false,
1644
+ hasCredentials: true,
1645
+ error: `Missing required verification fields: ${missing.join(', ')}`,
1646
+ });
1647
+ return;
1648
+ }
1649
+
1650
+ // Validate enum values
1651
+ const validUseCaseCategories = [
1652
+ 'TWO_FACTOR_AUTHENTICATION', 'ACCOUNT_NOTIFICATION', 'CUSTOMER_CARE',
1653
+ 'DELIVERY_NOTIFICATION', 'FRAUD_ALERT', 'HIGHER_EDUCATION', 'MARKETING',
1654
+ 'POLLING_AND_VOTING', 'PUBLIC_SERVICE_ANNOUNCEMENT', 'SECURITY_ALERT',
1655
+ ];
1656
+ const invalidCategories = (vp.useCaseCategories ?? []).filter((c) => !validUseCaseCategories.includes(c));
1657
+ if (invalidCategories.length > 0) {
1658
+ ctx.send(socket, {
1659
+ type: 'twilio_config_response',
1660
+ success: false,
1661
+ hasCredentials: true,
1662
+ error: `Invalid useCaseCategories: ${invalidCategories.join(', ')}. Valid values: ${validUseCaseCategories.join(', ')}`,
1663
+ });
1664
+ return;
1665
+ }
1666
+
1667
+ const validOptInTypes = ['VERBAL', 'WEB_FORM', 'PAPER_FORM', 'VIA_TEXT', 'MOBILE_QR_CODE'];
1668
+ if (!validOptInTypes.includes(vp.optInType!)) {
1669
+ ctx.send(socket, {
1670
+ type: 'twilio_config_response',
1671
+ success: false,
1672
+ hasCredentials: true,
1673
+ error: `Invalid optInType: ${vp.optInType}. Valid values: ${validOptInTypes.join(', ')}`,
1674
+ });
1675
+ return;
1676
+ }
1677
+
1678
+ const validMessageVolumes = [
1679
+ '10', '100', '1,000', '10,000', '100,000', '250,000',
1680
+ '500,000', '750,000', '1,000,000', '5,000,000', '10,000,000+',
1681
+ ];
1682
+ if (!validMessageVolumes.includes(vp.messageVolume!)) {
1683
+ ctx.send(socket, {
1684
+ type: 'twilio_config_response',
1685
+ success: false,
1686
+ hasCredentials: true,
1687
+ error: `Invalid messageVolume: ${vp.messageVolume}. Valid values: ${validMessageVolumes.join(', ')}`,
1688
+ });
1689
+ return;
1690
+ }
1691
+
1692
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
1693
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
1694
+
1695
+ const submitParams: TollFreeVerificationSubmitParams = {
1696
+ tollfreePhoneNumberSid: vp.tollfreePhoneNumberSid!,
1697
+ businessName: vp.businessName!,
1698
+ businessWebsite: vp.businessWebsite!,
1699
+ notificationEmail: vp.notificationEmail!,
1700
+ useCaseCategories: vp.useCaseCategories!,
1701
+ useCaseSummary: vp.useCaseSummary!,
1702
+ productionMessageSample: vp.productionMessageSample!,
1703
+ optInImageUrls: vp.optInImageUrls!,
1704
+ optInType: vp.optInType!,
1705
+ messageVolume: vp.messageVolume!,
1706
+ businessType: vp.businessType ?? 'SOLE_PROPRIETOR',
1707
+ customerProfileSid: vp.customerProfileSid,
1708
+ };
1709
+
1710
+ const verification = await submitTollFreeVerification(accountSid, authToken, submitParams);
1711
+
1712
+ ctx.send(socket, {
1713
+ type: 'twilio_config_response',
1714
+ success: true,
1715
+ hasCredentials: true,
1716
+ compliance: {
1717
+ numberType: 'toll_free',
1718
+ verificationSid: verification.sid,
1719
+ verificationStatus: verification.status,
1720
+ },
1721
+ });
1722
+ } else if (msg.action === 'sms_update_tollfree_verification') {
1723
+ if (!hasTwilioCredentials()) {
1724
+ ctx.send(socket, {
1725
+ type: 'twilio_config_response',
1726
+ success: false,
1727
+ hasCredentials: false,
1728
+ error: 'Twilio credentials not configured. Set credentials first.',
1729
+ });
1730
+ return;
1731
+ }
1732
+
1733
+ if (!msg.verificationSid) {
1734
+ ctx.send(socket, {
1735
+ type: 'twilio_config_response',
1736
+ success: false,
1737
+ hasCredentials: true,
1738
+ error: 'verificationSid is required for sms_update_tollfree_verification action',
1739
+ });
1740
+ return;
1741
+ }
1742
+
1743
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
1744
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
1745
+
1746
+ const verification = await updateTollFreeVerification(
1747
+ accountSid,
1748
+ authToken,
1749
+ msg.verificationSid,
1750
+ msg.verificationParams ?? {},
1751
+ );
1752
+
1753
+ ctx.send(socket, {
1754
+ type: 'twilio_config_response',
1755
+ success: true,
1756
+ hasCredentials: true,
1757
+ compliance: {
1758
+ numberType: 'toll_free',
1759
+ verificationSid: verification.sid,
1760
+ verificationStatus: verification.status,
1761
+ editAllowed: verification.editAllowed,
1762
+ editExpiration: verification.editExpiration,
1763
+ },
1764
+ });
1765
+ } else if (msg.action === 'sms_delete_tollfree_verification') {
1766
+ if (!hasTwilioCredentials()) {
1767
+ ctx.send(socket, {
1768
+ type: 'twilio_config_response',
1769
+ success: false,
1770
+ hasCredentials: false,
1771
+ error: 'Twilio credentials not configured. Set credentials first.',
1772
+ });
1773
+ return;
1774
+ }
1775
+
1776
+ if (!msg.verificationSid) {
1777
+ ctx.send(socket, {
1778
+ type: 'twilio_config_response',
1779
+ success: false,
1780
+ hasCredentials: true,
1781
+ error: 'verificationSid is required for sms_delete_tollfree_verification action',
1782
+ });
1783
+ return;
1784
+ }
1785
+
1786
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
1787
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
1788
+
1789
+ await deleteTollFreeVerification(accountSid, authToken, msg.verificationSid);
1790
+
1791
+ ctx.send(socket, {
1792
+ type: 'twilio_config_response',
1793
+ success: true,
1794
+ hasCredentials: true,
1795
+ warning: 'Toll-free verification deleted. Re-submitting may reset your position in the review queue.',
1796
+ });
1797
+ } else if (msg.action === 'release_number') {
1798
+ if (!hasTwilioCredentials()) {
1799
+ ctx.send(socket, {
1800
+ type: 'twilio_config_response',
1801
+ success: false,
1802
+ hasCredentials: false,
1803
+ error: 'Twilio credentials not configured. Set credentials first.',
1804
+ });
1805
+ return;
1806
+ }
1807
+
1808
+ const raw = loadRawConfig();
1809
+ const sms = (raw?.sms ?? {}) as Record<string, unknown>;
1810
+ let phoneNumber: string;
1811
+ if (msg.phoneNumber) {
1812
+ phoneNumber = msg.phoneNumber;
1813
+ } else if (msg.assistantId) {
1814
+ const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
1815
+ phoneNumber = mapping[msg.assistantId] ?? (sms.phoneNumber as string) ?? '';
1816
+ } else {
1817
+ phoneNumber = (sms.phoneNumber as string) ?? '';
1818
+ }
1819
+
1820
+ if (!phoneNumber) {
1821
+ ctx.send(socket, {
1822
+ type: 'twilio_config_response',
1823
+ success: false,
1824
+ hasCredentials: true,
1825
+ error: 'No phone number to release. Specify phoneNumber or ensure one is assigned.',
1826
+ });
1827
+ return;
1828
+ }
1829
+
1830
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
1831
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
1832
+
1833
+ await releasePhoneNumber(accountSid, authToken, phoneNumber);
1834
+
1835
+ // Clear the number from config and secure key store
1836
+ if (sms.phoneNumber === phoneNumber) {
1837
+ delete sms.phoneNumber;
1838
+ }
1839
+ const assistantPhoneNumbers = sms.assistantPhoneNumbers as Record<string, string> | undefined;
1840
+ if (assistantPhoneNumbers) {
1841
+ for (const [id, num] of Object.entries(assistantPhoneNumbers)) {
1842
+ if (num === phoneNumber) {
1843
+ delete assistantPhoneNumbers[id];
1844
+ }
1845
+ }
1846
+ if (Object.keys(assistantPhoneNumbers).length === 0) {
1847
+ delete sms.assistantPhoneNumbers;
1848
+ }
1849
+ }
1850
+
1851
+ const wasSuppressed = ctx.suppressConfigReload;
1852
+ ctx.setSuppressConfigReload(true);
1853
+ try {
1854
+ saveRawConfig({ ...raw, sms });
1855
+ } catch (err) {
1856
+ ctx.setSuppressConfigReload(wasSuppressed);
1857
+ throw err;
1858
+ }
1859
+ ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
1860
+
1861
+ // Clear the phone number from secure key store if it matches
1862
+ const storedPhone = getSecureKey('credential:twilio:phone_number');
1863
+ if (storedPhone === phoneNumber) {
1864
+ deleteSecureKey('credential:twilio:phone_number');
1865
+ }
1866
+
1867
+ ctx.send(socket, {
1868
+ type: 'twilio_config_response',
1869
+ success: true,
1870
+ hasCredentials: true,
1871
+ warning: 'Phone number released from Twilio. Any associated toll-free verification context is lost.',
1872
+ });
1873
+ } else if (msg.action === 'sms_send_test') {
1874
+ // ── SMS send test ────────────────────────────────────────────────
1875
+ if (!hasTwilioCredentials()) {
1876
+ ctx.send(socket, {
1877
+ type: 'twilio_config_response',
1878
+ success: false,
1879
+ hasCredentials: false,
1880
+ error: 'Twilio credentials not configured. Set credentials first.',
1881
+ });
1882
+ return;
1883
+ }
1884
+
1885
+ const to = msg.phoneNumber;
1886
+ if (!to) {
1887
+ ctx.send(socket, {
1888
+ type: 'twilio_config_response',
1889
+ success: false,
1890
+ hasCredentials: true,
1891
+ error: 'phoneNumber is required for sms_send_test action.',
1892
+ });
1893
+ return;
1894
+ }
1895
+
1896
+ const raw = loadRawConfig();
1897
+ const smsSection = (raw?.sms ?? {}) as Record<string, unknown>;
1898
+ const from = (smsSection.phoneNumber as string | undefined)
1899
+ || getSecureKey('credential:twilio:phone_number')
1900
+ || '';
1901
+ if (!from) {
1902
+ ctx.send(socket, {
1903
+ type: 'twilio_config_response',
1904
+ success: false,
1905
+ hasCredentials: true,
1906
+ error: 'No phone number assigned. Run the twilio-setup skill to assign a number.',
1907
+ });
1908
+ return;
1909
+ }
1910
+
1911
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
1912
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
1913
+ const text = msg.text || 'Test SMS from your Vellum assistant';
1914
+
1915
+ // Send via gateway's /deliver/sms endpoint
1916
+ const bearerToken = readHttpToken();
1917
+ const gatewayPort = Number(process.env.GATEWAY_PORT) || 7830;
1918
+ const gatewayUrl = process.env.GATEWAY_INTERNAL_BASE_URL?.replace(/\/+$/, '') || `http://127.0.0.1:${gatewayPort}`;
1919
+
1920
+ const sendResp = await fetch(`${gatewayUrl}/deliver/sms`, {
1921
+ method: 'POST',
1922
+ headers: {
1923
+ 'Content-Type': 'application/json',
1924
+ ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
1925
+ },
1926
+ body: JSON.stringify({ to, text }),
1927
+ signal: AbortSignal.timeout(30_000),
1928
+ });
1929
+
1930
+ if (!sendResp.ok) {
1931
+ const errBody = await sendResp.text().catch(() => '<unreadable>');
1932
+ ctx.send(socket, {
1933
+ type: 'twilio_config_response',
1934
+ success: false,
1935
+ hasCredentials: true,
1936
+ error: `SMS send failed (${sendResp.status}): ${errBody}`,
1937
+ });
1938
+ return;
1939
+ }
1940
+
1941
+ const sendData = await sendResp.json().catch(() => ({})) as {
1942
+ messageSid?: string;
1943
+ status?: string;
1944
+ };
1945
+ const messageSid = sendData.messageSid || '';
1946
+ const initialStatus = sendData.status || 'unknown';
1947
+
1948
+ // Poll Twilio for final status (up to 3 times, 2s apart)
1949
+ let finalStatus = initialStatus;
1950
+ let errorCode: string | undefined;
1951
+ let errorMessage: string | undefined;
1952
+
1953
+ if (messageSid) {
1954
+ for (let i = 0; i < 3; i++) {
1955
+ await new Promise((r) => setTimeout(r, 2000));
1956
+ try {
1957
+ const pollResult = await fetchMessageStatus(accountSid, authToken, messageSid);
1958
+ finalStatus = pollResult.status;
1959
+ errorCode = pollResult.errorCode;
1960
+ errorMessage = pollResult.errorMessage;
1961
+ // Stop polling if we've reached a terminal status
1962
+ if (['delivered', 'undelivered', 'failed'].includes(finalStatus)) break;
1963
+ } catch {
1964
+ // Polling failure is non-fatal; we'll use the last known status
1965
+ break;
1966
+ }
1967
+ }
1968
+ }
1969
+
1970
+ const testResult = {
1971
+ messageSid,
1972
+ to,
1973
+ initialStatus,
1974
+ finalStatus,
1975
+ ...(errorCode ? { errorCode } : {}),
1976
+ ...(errorMessage ? { errorMessage } : {}),
1977
+ };
1978
+
1979
+ // Store for sms_doctor
1980
+ _lastTestResult = { ...testResult, timestamp: Date.now() };
1981
+
1982
+ ctx.send(socket, {
1983
+ type: 'twilio_config_response',
1984
+ success: true,
1985
+ hasCredentials: true,
1986
+ testResult,
1987
+ });
1988
+
1989
+ } else if (msg.action === 'sms_doctor') {
1990
+ // ── SMS doctor diagnostic ────────────────────────────────────────
1991
+ const hasCredentials = hasTwilioCredentials();
1992
+
1993
+ // 1. Channel readiness check
1994
+ let readinessReady = false;
1995
+ const readinessIssues: string[] = [];
1996
+ try {
1997
+ const readinessService = getReadinessService();
1998
+ const snapshot = await readinessService.getReadiness('sms', { includeRemote: false });
1999
+ readinessReady = snapshot.ready;
2000
+ for (const r of snapshot.reasons) {
2001
+ readinessIssues.push(r.text);
2002
+ }
2003
+ } catch (err) {
2004
+ readinessIssues.push(`Readiness check failed: ${err instanceof Error ? err.message : String(err)}`);
2005
+ }
2006
+
2007
+ // 2. Compliance status
2008
+ let complianceStatus = 'unknown';
2009
+ let complianceDetail: string | undefined;
2010
+ let complianceRemediation: string | undefined;
2011
+ if (hasCredentials) {
2012
+ try {
2013
+ const raw = loadRawConfig();
2014
+ const smsSection = (raw?.sms ?? {}) as Record<string, unknown>;
2015
+ const phoneNumber = (smsSection.phoneNumber as string | undefined) || getSecureKey('credential:twilio:phone_number') || '';
2016
+ if (phoneNumber) {
2017
+ const accountSid = getSecureKey('credential:twilio:account_sid')!;
2018
+ const authToken = getSecureKey('credential:twilio:auth_token')!;
2019
+ // Determine number type and verification status
2020
+ const isTollFree = phoneNumber.startsWith('+1') && ['800','888','877','866','855','844','833'].some(
2021
+ (p) => phoneNumber.startsWith(`+1${p}`),
2022
+ );
2023
+ if (isTollFree) {
2024
+ try {
2025
+ const verification = await getTollFreeVerificationStatus(accountSid, authToken, phoneNumber);
2026
+ if (verification) {
2027
+ const status = verification.status;
2028
+ complianceStatus = status;
2029
+ complianceDetail = `Toll-free verification: ${status}`;
2030
+ if (status === 'TWILIO_APPROVED') {
2031
+ complianceRemediation = undefined;
2032
+ } else if (status === 'PENDING_REVIEW' || status === 'IN_REVIEW') {
2033
+ complianceRemediation = 'Toll-free verification is pending. Messaging may have limited throughput until approved.';
2034
+ } else if (status === 'TWILIO_REJECTED') {
2035
+ complianceRemediation = 'Toll-free verification was rejected. Check rejection reasons and resubmit.';
2036
+ } else {
2037
+ complianceRemediation = 'Submit a toll-free verification to enable full messaging throughput.';
2038
+ }
2039
+ } else {
2040
+ complianceStatus = 'unverified';
2041
+ complianceDetail = 'Toll-free number without verification';
2042
+ complianceRemediation = 'Submit a toll-free verification request to avoid filtering.';
2043
+ }
2044
+ } catch {
2045
+ complianceStatus = 'check_failed';
2046
+ complianceDetail = 'Could not retrieve toll-free verification status';
2047
+ }
2048
+ } else {
2049
+ complianceStatus = 'local_10dlc';
2050
+ complianceDetail = 'Local/10DLC number — carrier registration handled externally';
2051
+ }
2052
+ } else {
2053
+ complianceStatus = 'no_number';
2054
+ complianceDetail = 'No phone number assigned';
2055
+ complianceRemediation = 'Assign a phone number via the twilio-setup skill.';
2056
+ }
2057
+ } catch {
2058
+ complianceStatus = 'check_failed';
2059
+ complianceDetail = 'Could not determine compliance status';
2060
+ }
2061
+ } else {
2062
+ complianceStatus = 'no_credentials';
2063
+ complianceDetail = 'Twilio credentials are not configured';
2064
+ complianceRemediation = 'Set Twilio credentials via the twilio-setup skill.';
2065
+ }
2066
+
2067
+ // 3. Last send test result
2068
+ let lastSend: { status: string; errorCode?: string; remediation?: string } | undefined;
2069
+ if (_lastTestResult) {
2070
+ lastSend = {
2071
+ status: _lastTestResult.finalStatus,
2072
+ ...((_lastTestResult.errorCode) ? { errorCode: _lastTestResult.errorCode } : {}),
2073
+ ...((_lastTestResult.errorCode) ? { remediation: mapTwilioErrorRemediation(_lastTestResult.errorCode) } : {}),
2074
+ };
2075
+ }
2076
+
2077
+ // 4. Determine overall status
2078
+ const actionItems: string[] = [];
2079
+ let overallStatus: 'healthy' | 'degraded' | 'broken' = 'healthy';
2080
+
2081
+ if (!hasCredentials) {
2082
+ overallStatus = 'broken';
2083
+ actionItems.push('Configure Twilio credentials.');
2084
+ }
2085
+ if (!readinessReady) {
2086
+ overallStatus = 'broken';
2087
+ for (const issue of readinessIssues) actionItems.push(issue);
2088
+ }
2089
+ if (complianceStatus === 'unverified' || complianceStatus === 'PENDING_REVIEW' || complianceStatus === 'IN_REVIEW') {
2090
+ if (overallStatus === 'healthy') overallStatus = 'degraded';
2091
+ if (complianceRemediation) actionItems.push(complianceRemediation);
2092
+ }
2093
+ if (complianceStatus === 'TWILIO_REJECTED' || complianceStatus === 'no_number') {
2094
+ overallStatus = 'broken';
2095
+ if (complianceRemediation) actionItems.push(complianceRemediation);
2096
+ }
2097
+ if (_lastTestResult && ['failed', 'undelivered'].includes(_lastTestResult.finalStatus)) {
2098
+ if (overallStatus === 'healthy') overallStatus = 'degraded';
2099
+ const remediation = mapTwilioErrorRemediation(_lastTestResult.errorCode);
2100
+ actionItems.push(remediation || `Last test SMS ${_lastTestResult.finalStatus}. Check Twilio logs for details.`);
2101
+ }
2102
+
2103
+ ctx.send(socket, {
2104
+ type: 'twilio_config_response',
2105
+ success: true,
2106
+ hasCredentials,
2107
+ diagnostics: {
2108
+ readiness: { ready: readinessReady, issues: readinessIssues },
2109
+ compliance: {
2110
+ status: complianceStatus,
2111
+ ...(complianceDetail ? { detail: complianceDetail } : {}),
2112
+ ...(complianceRemediation ? { remediation: complianceRemediation } : {}),
2113
+ },
2114
+ ...(lastSend ? { lastSend } : {}),
2115
+ overallStatus,
2116
+ actionItems,
2117
+ },
2118
+ });
2119
+
1478
2120
  } else {
1479
2121
  ctx.send(socket, {
1480
2122
  type: 'twilio_config_response',
@@ -1518,11 +2160,40 @@ export function handleGuardianVerification(
1518
2160
  });
1519
2161
  } else if (msg.action === 'status') {
1520
2162
  const binding = getGuardianBinding(assistantId, channel);
2163
+ let guardianUsername: string | undefined;
2164
+ let guardianDisplayName: string | undefined;
2165
+ if (binding?.metadataJson) {
2166
+ try {
2167
+ const parsed = JSON.parse(binding.metadataJson) as Record<string, unknown>;
2168
+ if (typeof parsed.username === 'string' && parsed.username.trim().length > 0) {
2169
+ guardianUsername = parsed.username.trim();
2170
+ }
2171
+ if (typeof parsed.displayName === 'string' && parsed.displayName.trim().length > 0) {
2172
+ guardianDisplayName = parsed.displayName.trim();
2173
+ }
2174
+ } catch {
2175
+ // ignore malformed metadata
2176
+ }
2177
+ }
2178
+ if (binding?.guardianDeliveryChatId && (!guardianUsername || !guardianDisplayName)) {
2179
+ const ext = externalConversationStore.getBindingByChannelChat(
2180
+ channel,
2181
+ binding.guardianDeliveryChatId,
2182
+ );
2183
+ if (!guardianUsername && ext?.username) {
2184
+ guardianUsername = ext.username;
2185
+ }
2186
+ if (!guardianDisplayName && ext?.displayName) {
2187
+ guardianDisplayName = ext.displayName;
2188
+ }
2189
+ }
1521
2190
  ctx.send(socket, {
1522
2191
  type: 'guardian_verification_response',
1523
2192
  success: true,
1524
2193
  bound: binding !== null,
1525
2194
  guardianExternalUserId: binding?.guardianExternalUserId,
2195
+ guardianUsername,
2196
+ guardianDisplayName,
1526
2197
  channel,
1527
2198
  assistantId,
1528
2199
  guardianDeliveryChatId: binding?.guardianDeliveryChatId,
@@ -1555,6 +2226,57 @@ export function handleGuardianVerification(
1555
2226
  }
1556
2227
  }
1557
2228
 
2229
+ // Lazy singleton — created on first use so module-load stays lightweight.
2230
+ let _readinessService: ChannelReadinessService | undefined;
2231
+ function getReadinessService(): ChannelReadinessService {
2232
+ if (!_readinessService) {
2233
+ _readinessService = createReadinessService();
2234
+ }
2235
+ return _readinessService;
2236
+ }
2237
+
2238
+ export async function handleChannelReadiness(
2239
+ msg: ChannelReadinessRequest,
2240
+ socket: net.Socket,
2241
+ ctx: HandlerContext,
2242
+ ): Promise<void> {
2243
+ try {
2244
+ const service = getReadinessService();
2245
+
2246
+ if (msg.action === 'refresh') {
2247
+ if (msg.channel) {
2248
+ service.invalidateChannel(msg.channel);
2249
+ } else {
2250
+ service.invalidateAll();
2251
+ }
2252
+ }
2253
+
2254
+ const snapshots = await service.getReadiness(msg.channel, msg.includeRemote);
2255
+
2256
+ ctx.send(socket, {
2257
+ type: 'channel_readiness_response',
2258
+ success: true,
2259
+ snapshots: snapshots.map((s) => ({
2260
+ channel: s.channel,
2261
+ ready: s.ready,
2262
+ checkedAt: s.checkedAt,
2263
+ stale: s.stale,
2264
+ reasons: s.reasons,
2265
+ localChecks: s.localChecks,
2266
+ remoteChecks: s.remoteChecks,
2267
+ })),
2268
+ });
2269
+ } catch (err) {
2270
+ const message = err instanceof Error ? err.message : String(err);
2271
+ log.error({ err }, 'Failed to handle channel readiness');
2272
+ ctx.send(socket, {
2273
+ type: 'channel_readiness_response',
2274
+ success: false,
2275
+ error: message,
2276
+ });
2277
+ }
2278
+ }
2279
+
1558
2280
  export function handleEnvVarsRequest(socket: net.Socket, ctx: HandlerContext): void {
1559
2281
  const vars: Record<string, string> = {};
1560
2282
  for (const [key, value] of Object.entries(process.env)) {
@@ -1563,6 +2285,32 @@ export function handleEnvVarsRequest(socket: net.Socket, ctx: HandlerContext): v
1563
2285
  ctx.send(socket, { type: 'env_vars_response', vars });
1564
2286
  }
1565
2287
 
2288
+ /**
2289
+ * Look up manifest metadata for a tool that isn't in the live registry.
2290
+ * Searches all installed skills' TOOLS.json manifests for a matching tool name.
2291
+ */
2292
+ function resolveManifestOverride(toolName: string): ManifestOverride | undefined {
2293
+ if (getTool(toolName)) return undefined;
2294
+ try {
2295
+ const catalog = loadSkillCatalog();
2296
+ for (const skill of catalog) {
2297
+ if (!skill.toolManifest?.present || !skill.toolManifest.valid) continue;
2298
+ try {
2299
+ const manifest = parseToolManifestFile(join(skill.directoryPath, 'TOOLS.json'));
2300
+ const entry = manifest.tools.find((t) => t.name === toolName);
2301
+ if (entry) {
2302
+ return { risk: entry.risk, execution_target: entry.execution_target };
2303
+ }
2304
+ } catch {
2305
+ // Skip unparseable manifests
2306
+ }
2307
+ }
2308
+ } catch {
2309
+ // Non-fatal
2310
+ }
2311
+ return undefined;
2312
+ }
2313
+
1566
2314
  export async function handleToolPermissionSimulate(
1567
2315
  msg: ToolPermissionSimulateRequest,
1568
2316
  socket: net.Socket,
@@ -1588,13 +2336,15 @@ export async function handleToolPermissionSimulate(
1588
2336
 
1589
2337
  const workingDir = msg.workingDir ?? process.cwd();
1590
2338
 
1591
- // Resolve execution target using manifest metadata or prefix heuristics.
1592
- // resolveExecutionTarget handles unregistered tools via prefix fallback.
1593
- const executionTarget = resolveExecutionTarget(msg.toolName);
2339
+ // For unregistered skill tools, resolve manifest metadata so the simulation
2340
+ // uses accurate risk/execution_target values instead of falling back to defaults.
2341
+ const manifestOverride = resolveManifestOverride(msg.toolName);
2342
+
2343
+ const executionTarget = resolveExecutionTarget(msg.toolName, manifestOverride);
1594
2344
  const policyContext = { executionTarget };
1595
2345
 
1596
- const riskLevel = await classifyRisk(msg.toolName, msg.input, workingDir);
1597
- const result = await check(msg.toolName, msg.input, workingDir, policyContext);
2346
+ const riskLevel = await classifyRisk(msg.toolName, msg.input, workingDir, undefined, manifestOverride);
2347
+ const result = await check(msg.toolName, msg.input, workingDir, policyContext, manifestOverride);
1598
2348
 
1599
2349
  // Private-thread override: promote allow → prompt for side-effect tools
1600
2350
  if (
@@ -1652,7 +2402,7 @@ export async function handleToolPermissionSimulate(
1652
2402
 
1653
2403
  export function handleToolNamesList(socket: net.Socket, ctx: HandlerContext): void {
1654
2404
  const tools = getAllTools();
1655
- const names = tools.map((t) => t.name).sort((a, b) => a.localeCompare(b));
2405
+ const nameSet = new Set(tools.map((t) => t.name));
1656
2406
  const schemas: Record<string, import('../ipc-contract.js').ToolInputSchema> = {};
1657
2407
  for (const tool of tools) {
1658
2408
  try {
@@ -1662,6 +2412,29 @@ export function handleToolNamesList(socket: net.Socket, ctx: HandlerContext): vo
1662
2412
  // Skip tools whose definitions can't be resolved
1663
2413
  }
1664
2414
  }
2415
+
2416
+ // Include tools from all installed skills, even those not currently
2417
+ // activated in any session.
2418
+ try {
2419
+ const catalog = loadSkillCatalog();
2420
+ for (const skill of catalog) {
2421
+ if (!skill.toolManifest?.present || !skill.toolManifest.valid) continue;
2422
+ try {
2423
+ const manifest = parseToolManifestFile(join(skill.directoryPath, 'TOOLS.json'));
2424
+ for (const entry of manifest.tools) {
2425
+ if (nameSet.has(entry.name)) continue;
2426
+ nameSet.add(entry.name);
2427
+ schemas[entry.name] = entry.input_schema as unknown as import('../ipc-contract.js').ToolInputSchema;
2428
+ }
2429
+ } catch {
2430
+ // Skip skills whose manifests can't be parsed
2431
+ }
2432
+ }
2433
+ } catch {
2434
+ // Non-fatal — fall back to registered tools only
2435
+ }
2436
+
2437
+ const names = Array.from(nameSet).sort((a, b) => a.localeCompare(b));
1665
2438
  ctx.send(socket, { type: 'tool_names_list_response', names, schemas });
1666
2439
  }
1667
2440
 
@@ -1686,6 +2459,7 @@ export const configHandlers = defineHandlers({
1686
2459
  twitter_integration_config: handleTwitterIntegrationConfig,
1687
2460
  telegram_config: handleTelegramConfig,
1688
2461
  twilio_config: handleTwilioConfig,
2462
+ channel_readiness: handleChannelReadiness,
1689
2463
  guardian_verification: handleGuardianVerification,
1690
2464
  env_vars_request: (_msg, socket, ctx) => handleEnvVarsRequest(socket, ctx),
1691
2465
  tool_permission_simulate: handleToolPermissionSimulate,