@vellumai/assistant 0.3.3 → 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 +45 -18
- 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 +391 -1
- package/src/__tests__/call-routes-http.test.ts +27 -2
- package/src/__tests__/channel-approval-routes.test.ts +397 -135
- package/src/__tests__/channel-approvals.test.ts +99 -3
- package/src/__tests__/channel-delivery-store.test.ts +30 -4
- package/src/__tests__/channel-guardian.test.ts +261 -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 +636 -0
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
- package/src/__tests__/handlers-twilio-config.test.ts +480 -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__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-runtime-assembly.test.ts +85 -1
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/sms-messaging-provider.test.ts +125 -0
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/twilio-routes.test.ts +39 -3
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- 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 +85 -22
- 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/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- 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 +24 -5
- 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/skills.ts +5 -32
- package/src/config/system-prompt.ts +40 -0
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +58 -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/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +819 -22
- 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/skills.ts +6 -7
- package/src/daemon/handlers/work-items.ts +15 -7
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +114 -4
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +111 -504
- package/src/daemon/session-agent-loop.ts +10 -15
- package/src/daemon/session-runtime-assembly.ts +115 -44
- package/src/daemon/session-tool-setup.ts +2 -0
- package/src/daemon/session.ts +19 -2
- package/src/inbound/public-ingress-urls.ts +3 -3
- package/src/memory/channel-guardian-store.ts +2 -1
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1163 -0
- package/src/memory/db.ts +2 -2007
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-handlers/media-processing.ts +100 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-store.ts +2 -1
- package/src/memory/jobs-worker.ts +11 -1
- package/src/memory/media-store.ts +759 -0
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +36 -2
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +99 -0
- package/src/memory/search/entity.ts +208 -25
- package/src/memory/search/ranking.ts +6 -1
- package/src/memory/search/types.ts +26 -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/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/approval-message-composer.ts +143 -0
- package/src/runtime/channel-approvals.ts +29 -7
- 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/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +65 -28
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-routes.ts +237 -103
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +43 -3
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/executor.ts +10 -2
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/skills/vellum-catalog.ts +61 -156
- package/src/tools/terminal/parser.ts +21 -5
- package/src/tools/types.ts +2 -0
- package/src/twitter/router.ts +1 -1
- package/src/util/platform.ts +43 -1
- package/src/util/retry.ts +4 -4
|
@@ -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
|
}
|
|
@@ -628,20 +642,36 @@ export async function handleIngressConfig(
|
|
|
628
642
|
triggerGatewayReconcile(effectiveUrl);
|
|
629
643
|
|
|
630
644
|
// Best-effort Twilio webhook reconciliation: when ingress is being
|
|
631
|
-
// enabled/updated and
|
|
645
|
+
// enabled/updated and Twilio numbers are assigned with valid credentials,
|
|
632
646
|
// push the new webhook URLs to Twilio so calls and SMS route correctly.
|
|
633
647
|
if (isEnabled && hasTwilioCredentials()) {
|
|
634
648
|
const currentConfig = loadRawConfig();
|
|
635
649
|
const smsConfig = (currentConfig?.sms ?? {}) as Record<string, unknown>;
|
|
636
|
-
const
|
|
637
|
-
|
|
650
|
+
const assignedNumbers = new Set<string>();
|
|
651
|
+
const legacyNumber = (smsConfig.phoneNumber as string) ?? '';
|
|
652
|
+
if (legacyNumber) assignedNumbers.add(legacyNumber);
|
|
653
|
+
|
|
654
|
+
const assistantPhoneNumbers = smsConfig.assistantPhoneNumbers;
|
|
655
|
+
if (assistantPhoneNumbers && typeof assistantPhoneNumbers === 'object' && !Array.isArray(assistantPhoneNumbers)) {
|
|
656
|
+
for (const number of Object.values(assistantPhoneNumbers as Record<string, unknown>)) {
|
|
657
|
+
if (typeof number === 'string' && number) {
|
|
658
|
+
assignedNumbers.add(number);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (assignedNumbers.size > 0) {
|
|
638
664
|
const acctSid = getSecureKey('credential:twilio:account_sid')!;
|
|
639
665
|
const acctToken = getSecureKey('credential:twilio:auth_token')!;
|
|
640
|
-
// Fire-and-forget: webhook sync failure must not block the ingress save
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
666
|
+
// Fire-and-forget: webhook sync failure must not block the ingress save.
|
|
667
|
+
// Reconcile every assigned number so assistant-scoped mappings do not
|
|
668
|
+
// retain stale Twilio webhook URLs after ingress URL changes.
|
|
669
|
+
for (const assignedNumber of assignedNumbers) {
|
|
670
|
+
syncTwilioWebhooks(assignedNumber, acctSid, acctToken, currentConfig as IngressConfig)
|
|
671
|
+
.catch(() => {
|
|
672
|
+
// Already logged inside syncTwilioWebhooks
|
|
673
|
+
});
|
|
674
|
+
}
|
|
645
675
|
}
|
|
646
676
|
}
|
|
647
677
|
} else {
|
|
@@ -1159,6 +1189,32 @@ export async function handleTelegramConfig(
|
|
|
1159
1189
|
}
|
|
1160
1190
|
}
|
|
1161
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
|
+
|
|
1162
1218
|
export async function handleTwilioConfig(
|
|
1163
1219
|
msg: TwilioConfigRequest,
|
|
1164
1220
|
socket: net.Socket,
|
|
@@ -1459,6 +1515,608 @@ export async function handleTwilioConfig(
|
|
|
1459
1515
|
hasCredentials: true,
|
|
1460
1516
|
numbers,
|
|
1461
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
|
+
|
|
1462
2120
|
} else {
|
|
1463
2121
|
ctx.send(socket, {
|
|
1464
2122
|
type: 'twilio_config_response',
|
|
@@ -1484,12 +2142,12 @@ export function handleGuardianVerification(
|
|
|
1484
2142
|
socket: net.Socket,
|
|
1485
2143
|
ctx: HandlerContext,
|
|
1486
2144
|
): void {
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
const channel = msg.channel ?? 'telegram';
|
|
2145
|
+
// Use the assistant ID from the request when available; fall back to
|
|
2146
|
+
// 'self' for backward compatibility with single-assistant mode.
|
|
2147
|
+
const assistantId = msg.assistantId ?? 'self';
|
|
2148
|
+
const channel = msg.channel ?? 'telegram';
|
|
1492
2149
|
|
|
2150
|
+
try {
|
|
1493
2151
|
if (msg.action === 'create_challenge') {
|
|
1494
2152
|
const result = createVerificationChallenge(assistantId, channel, msg.sessionId);
|
|
1495
2153
|
|
|
@@ -1498,14 +2156,47 @@ export function handleGuardianVerification(
|
|
|
1498
2156
|
success: true,
|
|
1499
2157
|
secret: result.secret,
|
|
1500
2158
|
instruction: result.instruction,
|
|
2159
|
+
channel,
|
|
1501
2160
|
});
|
|
1502
2161
|
} else if (msg.action === 'status') {
|
|
1503
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
|
+
}
|
|
1504
2190
|
ctx.send(socket, {
|
|
1505
2191
|
type: 'guardian_verification_response',
|
|
1506
2192
|
success: true,
|
|
1507
2193
|
bound: binding !== null,
|
|
1508
2194
|
guardianExternalUserId: binding?.guardianExternalUserId,
|
|
2195
|
+
guardianUsername,
|
|
2196
|
+
guardianDisplayName,
|
|
2197
|
+
channel,
|
|
2198
|
+
assistantId,
|
|
2199
|
+
guardianDeliveryChatId: binding?.guardianDeliveryChatId,
|
|
1509
2200
|
});
|
|
1510
2201
|
} else if (msg.action === 'revoke') {
|
|
1511
2202
|
revokeGuardianBinding(assistantId, channel);
|
|
@@ -1513,12 +2204,14 @@ export function handleGuardianVerification(
|
|
|
1513
2204
|
type: 'guardian_verification_response',
|
|
1514
2205
|
success: true,
|
|
1515
2206
|
bound: false,
|
|
2207
|
+
channel,
|
|
1516
2208
|
});
|
|
1517
2209
|
} else {
|
|
1518
2210
|
ctx.send(socket, {
|
|
1519
2211
|
type: 'guardian_verification_response',
|
|
1520
2212
|
success: false,
|
|
1521
2213
|
error: `Unknown action: ${String(msg.action)}`,
|
|
2214
|
+
channel,
|
|
1522
2215
|
});
|
|
1523
2216
|
}
|
|
1524
2217
|
} catch (err) {
|
|
@@ -1528,6 +2221,58 @@ export function handleGuardianVerification(
|
|
|
1528
2221
|
type: 'guardian_verification_response',
|
|
1529
2222
|
success: false,
|
|
1530
2223
|
error: message,
|
|
2224
|
+
channel,
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
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,
|
|
1531
2276
|
});
|
|
1532
2277
|
}
|
|
1533
2278
|
}
|
|
@@ -1540,6 +2285,32 @@ export function handleEnvVarsRequest(socket: net.Socket, ctx: HandlerContext): v
|
|
|
1540
2285
|
ctx.send(socket, { type: 'env_vars_response', vars });
|
|
1541
2286
|
}
|
|
1542
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
|
+
|
|
1543
2314
|
export async function handleToolPermissionSimulate(
|
|
1544
2315
|
msg: ToolPermissionSimulateRequest,
|
|
1545
2316
|
socket: net.Socket,
|
|
@@ -1565,13 +2336,15 @@ export async function handleToolPermissionSimulate(
|
|
|
1565
2336
|
|
|
1566
2337
|
const workingDir = msg.workingDir ?? process.cwd();
|
|
1567
2338
|
|
|
1568
|
-
//
|
|
1569
|
-
//
|
|
1570
|
-
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);
|
|
1571
2344
|
const policyContext = { executionTarget };
|
|
1572
2345
|
|
|
1573
|
-
const riskLevel = await classifyRisk(msg.toolName, msg.input, workingDir);
|
|
1574
|
-
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);
|
|
1575
2348
|
|
|
1576
2349
|
// Private-thread override: promote allow → prompt for side-effect tools
|
|
1577
2350
|
if (
|
|
@@ -1629,7 +2402,7 @@ export async function handleToolPermissionSimulate(
|
|
|
1629
2402
|
|
|
1630
2403
|
export function handleToolNamesList(socket: net.Socket, ctx: HandlerContext): void {
|
|
1631
2404
|
const tools = getAllTools();
|
|
1632
|
-
const
|
|
2405
|
+
const nameSet = new Set(tools.map((t) => t.name));
|
|
1633
2406
|
const schemas: Record<string, import('../ipc-contract.js').ToolInputSchema> = {};
|
|
1634
2407
|
for (const tool of tools) {
|
|
1635
2408
|
try {
|
|
@@ -1639,6 +2412,29 @@ export function handleToolNamesList(socket: net.Socket, ctx: HandlerContext): vo
|
|
|
1639
2412
|
// Skip tools whose definitions can't be resolved
|
|
1640
2413
|
}
|
|
1641
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));
|
|
1642
2438
|
ctx.send(socket, { type: 'tool_names_list_response', names, schemas });
|
|
1643
2439
|
}
|
|
1644
2440
|
|
|
@@ -1663,6 +2459,7 @@ export const configHandlers = defineHandlers({
|
|
|
1663
2459
|
twitter_integration_config: handleTwitterIntegrationConfig,
|
|
1664
2460
|
telegram_config: handleTelegramConfig,
|
|
1665
2461
|
twilio_config: handleTwilioConfig,
|
|
2462
|
+
channel_readiness: handleChannelReadiness,
|
|
1666
2463
|
guardian_verification: handleGuardianVerification,
|
|
1667
2464
|
env_vars_request: (_msg, socket, ctx) => handleEnvVarsRequest(socket, ctx),
|
|
1668
2465
|
tool_permission_simulate: handleToolPermissionSimulate,
|