@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.
- package/Dockerfile +2 -0
- package/README.md +37 -2
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +13 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +70 -1
- package/src/__tests__/call-routes-http.test.ts +27 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -17
- package/src/__tests__/channel-approvals.test.ts +48 -1
- package/src/__tests__/channel-guardian.test.ts +74 -22
- package/src/__tests__/channel-readiness-service.test.ts +257 -0
- package/src/__tests__/config-schema.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +13 -12
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/handlers-twilio-config.test.ts +407 -0
- package/src/__tests__/ipc-snapshot.test.ts +63 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
- package/src/__tests__/run-orchestrator.test.ts +22 -0
- package/src/__tests__/session-runtime-assembly.test.ts +85 -1
- package/src/__tests__/sms-messaging-provider.test.ts +125 -0
- package/src/__tests__/twilio-routes.test.ts +39 -3
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/calls/call-domain.ts +8 -5
- package/src/calls/call-orchestrator.ts +22 -11
- package/src/calls/twilio-config.ts +17 -11
- package/src/calls/twilio-rest.ts +276 -0
- package/src/calls/twilio-routes.ts +39 -1
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
- package/src/config/bundled-skills/messaging/SKILL.md +21 -6
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/defaults.ts +2 -1
- package/src/config/schema.ts +9 -3
- package/src/config/system-prompt.ts +24 -0
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
- package/src/daemon/handlers/config.ts +783 -9
- package/src/daemon/handlers/dictation.ts +182 -0
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +2 -0
- package/src/daemon/handlers/sessions.ts +2 -0
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/work-items.ts +15 -7
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +108 -4
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/ride-shotgun-handler.ts +1 -1
- package/src/daemon/server.ts +6 -2
- package/src/daemon/session-agent-loop.ts +5 -1
- package/src/daemon/session-runtime-assembly.ts +55 -0
- package/src/daemon/session-tool-setup.ts +2 -0
- package/src/daemon/session.ts +11 -1
- package/src/inbound/public-ingress-urls.ts +3 -3
- package/src/memory/channel-guardian-store.ts +2 -1
- package/src/memory/db-init.ts +144 -0
- package/src/memory/job-handlers/media-processing.ts +100 -0
- package/src/memory/jobs-store.ts +2 -1
- package/src/memory/jobs-worker.ts +4 -0
- package/src/memory/media-store.ts +759 -0
- package/src/memory/retriever.ts +6 -1
- package/src/memory/schema.ts +98 -0
- package/src/memory/search/entity.ts +208 -25
- package/src/memory/search/ranking.ts +6 -1
- package/src/memory/search/types.ts +24 -0
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +204 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/permissions/checker.ts +16 -2
- package/src/runtime/approval-message-composer.ts +143 -0
- package/src/runtime/channel-approvals.ts +12 -4
- package/src/runtime/channel-guardian-service.ts +44 -18
- package/src/runtime/channel-readiness-service.ts +292 -0
- package/src/runtime/channel-readiness-types.ts +29 -0
- package/src/runtime/http-server.ts +53 -27
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-routes.ts +67 -21
- package/src/runtime/run-orchestrator.ts +35 -2
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/types.ts +2 -0
- package/src/twitter/router.ts +1 -1
- 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.
|
|
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
|
-
//
|
|
1592
|
-
//
|
|
1593
|
-
const
|
|
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
|
|
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,
|