@vellumai/assistant 0.3.2 → 0.3.4
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/README.md +82 -21
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +16 -0
- package/src/__tests__/app-git-history.test.ts +22 -27
- package/src/__tests__/app-git-service.test.ts +44 -78
- package/src/__tests__/call-orchestrator.test.ts +321 -0
- package/src/__tests__/channel-approval-routes.test.ts +1267 -93
- package/src/__tests__/channel-approval.test.ts +2 -0
- package/src/__tests__/channel-approvals.test.ts +51 -2
- package/src/__tests__/channel-delivery-store.test.ts +130 -1
- package/src/__tests__/channel-guardian.test.ts +371 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +635 -0
- package/src/__tests__/daemon-server-session-init.test.ts +5 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +106 -21
- package/src/__tests__/handlers-telegram-config.test.ts +82 -0
- package/src/__tests__/handlers-twilio-config.test.ts +738 -5
- package/src/__tests__/ingress-url-consistency.test.ts +64 -0
- package/src/__tests__/ipc-snapshot.test.ts +10 -0
- package/src/__tests__/run-orchestrator.test.ts +1 -1
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-process-bridge.test.ts +2 -0
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/calls/call-orchestrator.ts +63 -11
- package/src/calls/twilio-config.ts +10 -1
- package/src/calls/twilio-rest.ts +70 -0
- 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/email-setup/SKILL.md +56 -0
- package/src/config/bundled-skills/messaging/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +6 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +16 -0
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +52 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +49 -4
- 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 +193 -17
- package/src/daemon/handlers/sessions.ts +5 -3
- package/src/daemon/handlers/skills.ts +60 -17
- package/src/daemon/ipc-contract-inventory.json +4 -0
- package/src/daemon/ipc-contract.ts +16 -0
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +105 -502
- package/src/daemon/session-agent-loop.ts +9 -14
- package/src/daemon/session-process.ts +20 -3
- package/src/daemon/session-runtime-assembly.ts +60 -44
- package/src/daemon/session-slash.ts +50 -2
- package/src/daemon/session-surfaces.ts +17 -1
- package/src/daemon/session.ts +8 -1
- package/src/inbound/public-ingress-urls.ts +20 -3
- package/src/index.ts +1 -23
- package/src/memory/app-git-service.ts +24 -0
- package/src/memory/app-store.ts +0 -21
- package/src/memory/channel-delivery-store.ts +74 -3
- package/src/memory/channel-guardian-store.ts +54 -26
- package/src/memory/conversation-key-store.ts +20 -0
- package/src/memory/conversation-store.ts +14 -2
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1019 -0
- package/src/memory/db.ts +2 -1995
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-worker.ts +7 -1
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +30 -1
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +6 -0
- package/src/memory/search/types.ts +2 -0
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/channel-approvals.ts +17 -3
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +28 -9
- package/src/runtime/routes/channel-routes.ts +279 -100
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +8 -1
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/clawhub.ts +6 -2
- 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/subagent/manager.ts +4 -1
- package/src/subagent/types.ts +2 -0
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/executor.ts +10 -2
- package/src/tools/skills/vellum-catalog.ts +75 -127
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/terminal/parser.ts +21 -5
- package/src/util/platform.ts +8 -1
- package/src/util/retry.ts +4 -4
|
@@ -5,7 +5,7 @@ import { addRule, removeRule, updateRule, getAllRules, acceptStarterBundle } fro
|
|
|
5
5
|
import { classifyRisk, check, generateAllowlistOptions, generateScopeOptions } from '../../permissions/checker.js';
|
|
6
6
|
import { isSideEffectTool } from '../../tools/executor.js';
|
|
7
7
|
import { resolveExecutionTarget } from '../../tools/execution-target.js';
|
|
8
|
-
import { getAllTools
|
|
8
|
+
import { getAllTools } from '../../tools/registry.js';
|
|
9
9
|
import { listSchedules, updateSchedule, deleteSchedule, describeCronExpression } from '../../schedule/schedule-store.js';
|
|
10
10
|
import { listReminders, cancelReminder } from '../../tools/reminder/reminder-store.js';
|
|
11
11
|
import { getSecureKey, setSecureKey, deleteSecureKey } from '../../security/secure-keys.js';
|
|
@@ -37,7 +37,14 @@ import {
|
|
|
37
37
|
listIncomingPhoneNumbers,
|
|
38
38
|
searchAvailableNumbers,
|
|
39
39
|
provisionPhoneNumber,
|
|
40
|
+
updatePhoneNumberWebhooks,
|
|
40
41
|
} from '../../calls/twilio-rest.js';
|
|
42
|
+
import {
|
|
43
|
+
getTwilioVoiceWebhookUrl,
|
|
44
|
+
getTwilioStatusCallbackUrl,
|
|
45
|
+
getTwilioSmsWebhookUrl,
|
|
46
|
+
type IngressConfig,
|
|
47
|
+
} from '../../inbound/public-ingress-urls.js';
|
|
41
48
|
import { createVerificationChallenge, getGuardianBinding, revokeBinding as revokeGuardianBinding } from '../../runtime/channel-guardian-service.js';
|
|
42
49
|
import { log, CONFIG_RELOAD_DEBOUNCE_MS, defineHandlers, type HandlerContext } from './shared.js';
|
|
43
50
|
import { MODEL_TO_PROVIDER } from '../session-slash.js';
|
|
@@ -509,11 +516,46 @@ function triggerGatewayReconcile(ingressPublicBaseUrl: string | undefined): void
|
|
|
509
516
|
});
|
|
510
517
|
}
|
|
511
518
|
|
|
512
|
-
|
|
519
|
+
/**
|
|
520
|
+
* Best-effort Twilio webhook sync helper.
|
|
521
|
+
*
|
|
522
|
+
* Computes the voice, status-callback, and SMS webhook URLs from the current
|
|
523
|
+
* ingress config and pushes them to the Twilio IncomingPhoneNumber API.
|
|
524
|
+
*
|
|
525
|
+
* Returns `{ success, warning }`. When the update fails, `success` is false
|
|
526
|
+
* and `warning` contains a human-readable message. Callers should treat
|
|
527
|
+
* failure as non-fatal so that the primary operation (provision, assign,
|
|
528
|
+
* ingress save) still succeeds.
|
|
529
|
+
*/
|
|
530
|
+
async function syncTwilioWebhooks(
|
|
531
|
+
phoneNumber: string,
|
|
532
|
+
accountSid: string,
|
|
533
|
+
authToken: string,
|
|
534
|
+
ingressConfig: IngressConfig,
|
|
535
|
+
): Promise<{ success: boolean; warning?: string }> {
|
|
536
|
+
try {
|
|
537
|
+
const voiceUrl = getTwilioVoiceWebhookUrl(ingressConfig);
|
|
538
|
+
const statusCallbackUrl = getTwilioStatusCallbackUrl(ingressConfig);
|
|
539
|
+
const smsUrl = getTwilioSmsWebhookUrl(ingressConfig);
|
|
540
|
+
await updatePhoneNumberWebhooks(accountSid, authToken, phoneNumber, {
|
|
541
|
+
voiceUrl,
|
|
542
|
+
statusCallbackUrl,
|
|
543
|
+
smsUrl,
|
|
544
|
+
});
|
|
545
|
+
log.info({ phoneNumber }, 'Twilio webhooks configured successfully');
|
|
546
|
+
return { success: true };
|
|
547
|
+
} catch (err) {
|
|
548
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
549
|
+
log.warn({ err, phoneNumber }, `Webhook configuration skipped: ${message}`);
|
|
550
|
+
return { success: false, warning: `Webhook configuration skipped: ${message}` };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export async function handleIngressConfig(
|
|
513
555
|
msg: IngressConfigRequest,
|
|
514
556
|
socket: net.Socket,
|
|
515
557
|
ctx: HandlerContext,
|
|
516
|
-
): void {
|
|
558
|
+
): Promise<void> {
|
|
517
559
|
const localGatewayTarget = computeGatewayTarget();
|
|
518
560
|
try {
|
|
519
561
|
if (msg.action === 'get') {
|
|
@@ -584,6 +626,40 @@ export function handleIngressConfig(
|
|
|
584
626
|
// fallback branch above) rather than the raw `value` from the UI.
|
|
585
627
|
const effectiveUrl = isEnabled ? process.env.INGRESS_PUBLIC_BASE_URL : undefined;
|
|
586
628
|
triggerGatewayReconcile(effectiveUrl);
|
|
629
|
+
|
|
630
|
+
// Best-effort Twilio webhook reconciliation: when ingress is being
|
|
631
|
+
// enabled/updated and Twilio numbers are assigned with valid credentials,
|
|
632
|
+
// push the new webhook URLs to Twilio so calls and SMS route correctly.
|
|
633
|
+
if (isEnabled && hasTwilioCredentials()) {
|
|
634
|
+
const currentConfig = loadRawConfig();
|
|
635
|
+
const smsConfig = (currentConfig?.sms ?? {}) as Record<string, unknown>;
|
|
636
|
+
const assignedNumbers = new Set<string>();
|
|
637
|
+
const legacyNumber = (smsConfig.phoneNumber as string) ?? '';
|
|
638
|
+
if (legacyNumber) assignedNumbers.add(legacyNumber);
|
|
639
|
+
|
|
640
|
+
const assistantPhoneNumbers = smsConfig.assistantPhoneNumbers;
|
|
641
|
+
if (assistantPhoneNumbers && typeof assistantPhoneNumbers === 'object' && !Array.isArray(assistantPhoneNumbers)) {
|
|
642
|
+
for (const number of Object.values(assistantPhoneNumbers as Record<string, unknown>)) {
|
|
643
|
+
if (typeof number === 'string' && number) {
|
|
644
|
+
assignedNumbers.add(number);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (assignedNumbers.size > 0) {
|
|
650
|
+
const acctSid = getSecureKey('credential:twilio:account_sid')!;
|
|
651
|
+
const acctToken = getSecureKey('credential:twilio:auth_token')!;
|
|
652
|
+
// Fire-and-forget: webhook sync failure must not block the ingress save.
|
|
653
|
+
// Reconcile every assigned number so assistant-scoped mappings do not
|
|
654
|
+
// retain stale Twilio webhook URLs after ingress URL changes.
|
|
655
|
+
for (const assignedNumber of assignedNumbers) {
|
|
656
|
+
syncTwilioWebhooks(assignedNumber, acctSid, acctToken, currentConfig as IngressConfig)
|
|
657
|
+
.catch(() => {
|
|
658
|
+
// Already logged inside syncTwilioWebhooks
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
587
663
|
} else {
|
|
588
664
|
ctx.send(socket, { type: 'ingress_config_response', enabled: false, publicBaseUrl: '', localGatewayTarget, success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
|
|
589
665
|
}
|
|
@@ -1109,7 +1185,15 @@ export async function handleTwilioConfig(
|
|
|
1109
1185
|
const hasCredentials = hasTwilioCredentials();
|
|
1110
1186
|
const raw = loadRawConfig();
|
|
1111
1187
|
const sms = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
1112
|
-
|
|
1188
|
+
// When assistantId is provided, look up in assistantPhoneNumbers first,
|
|
1189
|
+
// fall back to the legacy phoneNumber field
|
|
1190
|
+
let phoneNumber: string;
|
|
1191
|
+
if (msg.assistantId) {
|
|
1192
|
+
const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
|
|
1193
|
+
phoneNumber = mapping[msg.assistantId] ?? (sms.phoneNumber as string) ?? '';
|
|
1194
|
+
} else {
|
|
1195
|
+
phoneNumber = (sms.phoneNumber as string) ?? '';
|
|
1196
|
+
}
|
|
1113
1197
|
ctx.send(socket, {
|
|
1114
1198
|
type: 'twilio_config_response',
|
|
1115
1199
|
success: true,
|
|
@@ -1192,9 +1276,12 @@ export async function handleTwilioConfig(
|
|
|
1192
1276
|
hasCredentials: true,
|
|
1193
1277
|
});
|
|
1194
1278
|
} else if (msg.action === 'clear_credentials') {
|
|
1279
|
+
// Only clear authentication credentials (Account SID and Auth Token).
|
|
1280
|
+
// Preserve the phone number in both config (sms.phoneNumber) and secure
|
|
1281
|
+
// key (credential:twilio:phone_number) so that re-entering credentials
|
|
1282
|
+
// resumes working without needing to reassign the number.
|
|
1195
1283
|
deleteSecureKey('credential:twilio:account_sid');
|
|
1196
1284
|
deleteSecureKey('credential:twilio:auth_token');
|
|
1197
|
-
deleteSecureKey('credential:twilio:phone_number');
|
|
1198
1285
|
deleteCredentialMetadata('twilio', 'account_sid');
|
|
1199
1286
|
deleteCredentialMetadata('twilio', 'auth_token');
|
|
1200
1287
|
|
|
@@ -1233,11 +1320,64 @@ export async function handleTwilioConfig(
|
|
|
1233
1320
|
// Purchase the first available number
|
|
1234
1321
|
const purchased = await provisionPhoneNumber(accountSid, authToken, available[0].phoneNumber);
|
|
1235
1322
|
|
|
1323
|
+
// Auto-assign: persist the purchased number in secure storage and config
|
|
1324
|
+
// (same persistence as assign_number for consistency)
|
|
1325
|
+
const phoneStored = setSecureKey('credential:twilio:phone_number', purchased.phoneNumber);
|
|
1326
|
+
if (!phoneStored) {
|
|
1327
|
+
ctx.send(socket, {
|
|
1328
|
+
type: 'twilio_config_response',
|
|
1329
|
+
success: false,
|
|
1330
|
+
hasCredentials: hasTwilioCredentials(),
|
|
1331
|
+
phoneNumber: purchased.phoneNumber,
|
|
1332
|
+
error: `Phone number ${purchased.phoneNumber} was purchased but could not be saved. Use assign_number to assign it manually.`,
|
|
1333
|
+
});
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
const raw = loadRawConfig();
|
|
1338
|
+
const sms = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
1339
|
+
// When assistantId is provided, only set the legacy global phoneNumber
|
|
1340
|
+
// if it's not already set — this prevents multi-assistant assignments
|
|
1341
|
+
// from clobbering each other's outbound SMS number.
|
|
1342
|
+
if (msg.assistantId) {
|
|
1343
|
+
if (!sms.phoneNumber) {
|
|
1344
|
+
sms.phoneNumber = purchased.phoneNumber;
|
|
1345
|
+
}
|
|
1346
|
+
} else {
|
|
1347
|
+
sms.phoneNumber = purchased.phoneNumber;
|
|
1348
|
+
}
|
|
1349
|
+
// When assistantId is provided, also persist into the per-assistant mapping
|
|
1350
|
+
if (msg.assistantId) {
|
|
1351
|
+
const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
|
|
1352
|
+
mapping[msg.assistantId] = purchased.phoneNumber;
|
|
1353
|
+
sms.assistantPhoneNumbers = mapping;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
const wasSuppressed = ctx.suppressConfigReload;
|
|
1357
|
+
ctx.setSuppressConfigReload(true);
|
|
1358
|
+
try {
|
|
1359
|
+
saveRawConfig({ ...raw, sms });
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
ctx.setSuppressConfigReload(wasSuppressed);
|
|
1362
|
+
throw err;
|
|
1363
|
+
}
|
|
1364
|
+
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
1365
|
+
|
|
1366
|
+
// Best-effort webhook configuration — non-fatal so the number is
|
|
1367
|
+
// still usable even if ingress isn't configured yet.
|
|
1368
|
+
const webhookResult = await syncTwilioWebhooks(
|
|
1369
|
+
purchased.phoneNumber,
|
|
1370
|
+
accountSid,
|
|
1371
|
+
authToken,
|
|
1372
|
+
loadRawConfig() as IngressConfig,
|
|
1373
|
+
);
|
|
1374
|
+
|
|
1236
1375
|
ctx.send(socket, {
|
|
1237
1376
|
type: 'twilio_config_response',
|
|
1238
1377
|
success: true,
|
|
1239
1378
|
hasCredentials: true,
|
|
1240
1379
|
phoneNumber: purchased.phoneNumber,
|
|
1380
|
+
warning: webhookResult.warning,
|
|
1241
1381
|
});
|
|
1242
1382
|
} else if (msg.action === 'assign_number') {
|
|
1243
1383
|
if (!msg.phoneNumber) {
|
|
@@ -1266,7 +1406,22 @@ export async function handleTwilioConfig(
|
|
|
1266
1406
|
// Also persist in assistant config (non-secret) for the UI
|
|
1267
1407
|
const raw = loadRawConfig();
|
|
1268
1408
|
const sms = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
1269
|
-
|
|
1409
|
+
// When assistantId is provided, only set the legacy global phoneNumber
|
|
1410
|
+
// if it's not already set — this prevents multi-assistant assignments
|
|
1411
|
+
// from clobbering each other's outbound SMS number.
|
|
1412
|
+
if (msg.assistantId) {
|
|
1413
|
+
if (!sms.phoneNumber) {
|
|
1414
|
+
sms.phoneNumber = msg.phoneNumber;
|
|
1415
|
+
}
|
|
1416
|
+
} else {
|
|
1417
|
+
sms.phoneNumber = msg.phoneNumber;
|
|
1418
|
+
}
|
|
1419
|
+
// When assistantId is provided, also persist into the per-assistant mapping
|
|
1420
|
+
if (msg.assistantId) {
|
|
1421
|
+
const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
|
|
1422
|
+
mapping[msg.assistantId] = msg.phoneNumber;
|
|
1423
|
+
sms.assistantPhoneNumbers = mapping;
|
|
1424
|
+
}
|
|
1270
1425
|
|
|
1271
1426
|
const wasSuppressed = ctx.suppressConfigReload;
|
|
1272
1427
|
ctx.setSuppressConfigReload(true);
|
|
@@ -1278,11 +1433,26 @@ export async function handleTwilioConfig(
|
|
|
1278
1433
|
}
|
|
1279
1434
|
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
1280
1435
|
|
|
1436
|
+
// Best-effort webhook configuration when credentials are available
|
|
1437
|
+
let webhookWarning: string | undefined;
|
|
1438
|
+
if (hasTwilioCredentials()) {
|
|
1439
|
+
const acctSid = getSecureKey('credential:twilio:account_sid')!;
|
|
1440
|
+
const acctToken = getSecureKey('credential:twilio:auth_token')!;
|
|
1441
|
+
const webhookResult = await syncTwilioWebhooks(
|
|
1442
|
+
msg.phoneNumber,
|
|
1443
|
+
acctSid,
|
|
1444
|
+
acctToken,
|
|
1445
|
+
loadRawConfig() as IngressConfig,
|
|
1446
|
+
);
|
|
1447
|
+
webhookWarning = webhookResult.warning;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1281
1450
|
ctx.send(socket, {
|
|
1282
1451
|
type: 'twilio_config_response',
|
|
1283
1452
|
success: true,
|
|
1284
1453
|
hasCredentials: hasTwilioCredentials(),
|
|
1285
1454
|
phoneNumber: msg.phoneNumber,
|
|
1455
|
+
warning: webhookWarning,
|
|
1286
1456
|
});
|
|
1287
1457
|
} else if (msg.action === 'list_numbers') {
|
|
1288
1458
|
if (!hasTwilioCredentials()) {
|
|
@@ -1330,12 +1500,12 @@ export function handleGuardianVerification(
|
|
|
1330
1500
|
socket: net.Socket,
|
|
1331
1501
|
ctx: HandlerContext,
|
|
1332
1502
|
): void {
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
const channel = msg.channel ?? 'telegram';
|
|
1503
|
+
// Use the assistant ID from the request when available; fall back to
|
|
1504
|
+
// 'self' for backward compatibility with single-assistant mode.
|
|
1505
|
+
const assistantId = msg.assistantId ?? 'self';
|
|
1506
|
+
const channel = msg.channel ?? 'telegram';
|
|
1338
1507
|
|
|
1508
|
+
try {
|
|
1339
1509
|
if (msg.action === 'create_challenge') {
|
|
1340
1510
|
const result = createVerificationChallenge(assistantId, channel, msg.sessionId);
|
|
1341
1511
|
|
|
@@ -1344,6 +1514,7 @@ export function handleGuardianVerification(
|
|
|
1344
1514
|
success: true,
|
|
1345
1515
|
secret: result.secret,
|
|
1346
1516
|
instruction: result.instruction,
|
|
1517
|
+
channel,
|
|
1347
1518
|
});
|
|
1348
1519
|
} else if (msg.action === 'status') {
|
|
1349
1520
|
const binding = getGuardianBinding(assistantId, channel);
|
|
@@ -1352,19 +1523,24 @@ export function handleGuardianVerification(
|
|
|
1352
1523
|
success: true,
|
|
1353
1524
|
bound: binding !== null,
|
|
1354
1525
|
guardianExternalUserId: binding?.guardianExternalUserId,
|
|
1526
|
+
channel,
|
|
1527
|
+
assistantId,
|
|
1528
|
+
guardianDeliveryChatId: binding?.guardianDeliveryChatId,
|
|
1355
1529
|
});
|
|
1356
1530
|
} else if (msg.action === 'revoke') {
|
|
1357
|
-
|
|
1531
|
+
revokeGuardianBinding(assistantId, channel);
|
|
1358
1532
|
ctx.send(socket, {
|
|
1359
1533
|
type: 'guardian_verification_response',
|
|
1360
1534
|
success: true,
|
|
1361
1535
|
bound: false,
|
|
1536
|
+
channel,
|
|
1362
1537
|
});
|
|
1363
1538
|
} else {
|
|
1364
1539
|
ctx.send(socket, {
|
|
1365
1540
|
type: 'guardian_verification_response',
|
|
1366
1541
|
success: false,
|
|
1367
1542
|
error: `Unknown action: ${String(msg.action)}`,
|
|
1543
|
+
channel,
|
|
1368
1544
|
});
|
|
1369
1545
|
}
|
|
1370
1546
|
} catch (err) {
|
|
@@ -1374,6 +1550,7 @@ export function handleGuardianVerification(
|
|
|
1374
1550
|
type: 'guardian_verification_response',
|
|
1375
1551
|
success: false,
|
|
1376
1552
|
error: message,
|
|
1553
|
+
channel,
|
|
1377
1554
|
});
|
|
1378
1555
|
}
|
|
1379
1556
|
}
|
|
@@ -1411,11 +1588,10 @@ export async function handleToolPermissionSimulate(
|
|
|
1411
1588
|
|
|
1412
1589
|
const workingDir = msg.workingDir ?? process.cwd();
|
|
1413
1590
|
|
|
1414
|
-
//
|
|
1415
|
-
//
|
|
1416
|
-
const
|
|
1417
|
-
const
|
|
1418
|
-
const policyContext = executionTarget ? { executionTarget } : undefined;
|
|
1591
|
+
// Resolve execution target using manifest metadata or prefix heuristics.
|
|
1592
|
+
// resolveExecutionTarget handles unregistered tools via prefix fallback.
|
|
1593
|
+
const executionTarget = resolveExecutionTarget(msg.toolName);
|
|
1594
|
+
const policyContext = { executionTarget };
|
|
1419
1595
|
|
|
1420
1596
|
const riskLevel = await classifyRisk(msg.toolName, msg.input, workingDir);
|
|
1421
1597
|
const result = await check(msg.toolName, msg.input, workingDir, policyContext);
|
|
@@ -198,8 +198,9 @@ export function handleSecretResponse(
|
|
|
198
198
|
log.warn({ requestId: msg.requestId }, 'No session found with pending secret prompt for requestId');
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
export function handleSessionList(socket: net.Socket, ctx: HandlerContext): void {
|
|
202
|
-
const conversations = conversationStore.listConversations(
|
|
201
|
+
export function handleSessionList(socket: net.Socket, ctx: HandlerContext, offset = 0, limit = 50): void {
|
|
202
|
+
const conversations = conversationStore.listConversations(limit, false, offset);
|
|
203
|
+
const totalCount = conversationStore.countConversations();
|
|
203
204
|
const bindings = externalConversationStore.getBindingsForConversations(
|
|
204
205
|
conversations.map((c) => c.id),
|
|
205
206
|
);
|
|
@@ -223,6 +224,7 @@ export function handleSessionList(socket: net.Socket, ctx: HandlerContext): void
|
|
|
223
224
|
} : {}),
|
|
224
225
|
};
|
|
225
226
|
}),
|
|
227
|
+
hasMore: offset + conversations.length < totalCount,
|
|
226
228
|
});
|
|
227
229
|
}
|
|
228
230
|
|
|
@@ -541,7 +543,7 @@ export const sessionHandlers = defineHandlers({
|
|
|
541
543
|
user_message: handleUserMessage,
|
|
542
544
|
confirmation_response: handleConfirmationResponse,
|
|
543
545
|
secret_response: handleSecretResponse,
|
|
544
|
-
session_list: (
|
|
546
|
+
session_list: (msg, socket, ctx) => handleSessionList(socket, ctx, msg.offset ?? 0, msg.limit ?? 50),
|
|
545
547
|
session_create: handleSessionCreate,
|
|
546
548
|
sessions_clear: (_msg, socket, ctx) => handleSessionsClear(socket, ctx),
|
|
547
549
|
session_switch: handleSessionSwitch,
|
|
@@ -5,8 +5,9 @@ import { getConfig, loadRawConfig, saveRawConfig, invalidateConfigCache } from '
|
|
|
5
5
|
import { loadSkillCatalog, loadSkillBySelector, ensureSkillIcon } from '../../config/skills.js';
|
|
6
6
|
import { resolveSkillStates } from '../../config/skill-state.js';
|
|
7
7
|
import { getWorkspaceSkillsDir } from '../../util/platform.js';
|
|
8
|
-
import { clawhubInstall, clawhubUpdate, clawhubSearch, clawhubCheckUpdates, clawhubInspect } from '../../skills/clawhub.js';
|
|
8
|
+
import { clawhubInstall, clawhubUpdate, clawhubSearch, clawhubCheckUpdates, clawhubInspect, type ClawhubSearchResultItem } from '../../skills/clawhub.js';
|
|
9
9
|
import { removeSkillsIndexEntry, deleteManagedSkill, validateManagedSkillId } from '../../skills/managed-store.js';
|
|
10
|
+
import { listCatalogEntries, installFromVellumCatalog, checkVellumSkill } from '../../tools/skills/vellum-catalog.js';
|
|
10
11
|
import type {
|
|
11
12
|
SkillDetailRequest,
|
|
12
13
|
SkillsEnableRequest,
|
|
@@ -186,26 +187,44 @@ export async function handleSkillsInstall(
|
|
|
186
187
|
ctx: HandlerContext,
|
|
187
188
|
): Promise<void> {
|
|
188
189
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
190
|
+
// Check if the slug matches a vellum-skills catalog entry first
|
|
191
|
+
const isVellumSkill = await checkVellumSkill(msg.slug);
|
|
192
|
+
|
|
193
|
+
let skillId: string;
|
|
194
|
+
|
|
195
|
+
if (isVellumSkill) {
|
|
196
|
+
// Install from vellum-skills catalog (remote with bundled fallback)
|
|
197
|
+
const result = await installFromVellumCatalog(msg.slug);
|
|
198
|
+
if (!result.success) {
|
|
199
|
+
ctx.send(socket, {
|
|
200
|
+
type: 'skills_operation_response',
|
|
201
|
+
operation: 'install',
|
|
202
|
+
success: false,
|
|
203
|
+
error: result.error ?? 'Unknown error',
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
skillId = result.skillName ?? msg.slug;
|
|
208
|
+
} else {
|
|
209
|
+
// Install from clawhub (community)
|
|
210
|
+
const result = await clawhubInstall(msg.slug, { version: msg.version });
|
|
211
|
+
if (!result.success) {
|
|
212
|
+
ctx.send(socket, {
|
|
213
|
+
type: 'skills_operation_response',
|
|
214
|
+
operation: 'install',
|
|
215
|
+
success: false,
|
|
216
|
+
error: result.error ?? 'Unknown error',
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const rawId = result.skillName ?? msg.slug;
|
|
221
|
+
skillId = rawId.includes('/') ? rawId.split('/').pop()! : rawId;
|
|
198
222
|
}
|
|
199
223
|
|
|
200
224
|
// Reload skill catalog so the newly installed skill is picked up
|
|
201
225
|
loadSkillCatalog();
|
|
202
226
|
|
|
203
227
|
// Auto-enable the newly installed skill so it's immediately usable.
|
|
204
|
-
// Use basename of slug to match the catalog ID (directory basename), since
|
|
205
|
-
// install slugs can be namespaced (e.g. "org/name") but skill state keys use
|
|
206
|
-
// the bare directory name.
|
|
207
|
-
const rawId = result.skillName ?? msg.slug;
|
|
208
|
-
const skillId = rawId.includes('/') ? rawId.split('/').pop()! : rawId;
|
|
209
228
|
try {
|
|
210
229
|
const raw = loadRawConfig();
|
|
211
230
|
ensureSkillEntry(raw, skillId).enabled = true;
|
|
@@ -404,12 +423,36 @@ export async function handleSkillsSearch(
|
|
|
404
423
|
ctx: HandlerContext,
|
|
405
424
|
): Promise<void> {
|
|
406
425
|
try {
|
|
407
|
-
|
|
426
|
+
// Search vellum-skills catalog (remote with bundled fallback)
|
|
427
|
+
const catalogEntries = await listCatalogEntries();
|
|
428
|
+
const query = (msg.query ?? '').toLowerCase();
|
|
429
|
+
const matchingCatalog = catalogEntries.filter((e) => {
|
|
430
|
+
if (!query) return true;
|
|
431
|
+
return e.name.toLowerCase().includes(query) || e.description.toLowerCase().includes(query) || e.id.toLowerCase().includes(query);
|
|
432
|
+
});
|
|
433
|
+
const vellumSkills: ClawhubSearchResultItem[] = matchingCatalog.map((e) => ({
|
|
434
|
+
name: e.name,
|
|
435
|
+
slug: e.id,
|
|
436
|
+
description: e.description,
|
|
437
|
+
author: 'Vellum',
|
|
438
|
+
stars: 0,
|
|
439
|
+
installs: 0,
|
|
440
|
+
version: '',
|
|
441
|
+
createdAt: 0,
|
|
442
|
+
source: 'vellum' as const,
|
|
443
|
+
}));
|
|
444
|
+
|
|
445
|
+
// Search clawhub concurrently
|
|
446
|
+
const clawhubResult = await clawhubSearch(msg.query);
|
|
447
|
+
|
|
448
|
+
// Merge: vellum first, then clawhub
|
|
449
|
+
const merged = { skills: [...vellumSkills, ...clawhubResult.skills] };
|
|
450
|
+
|
|
408
451
|
ctx.send(socket, {
|
|
409
452
|
type: 'skills_operation_response',
|
|
410
453
|
operation: 'search',
|
|
411
454
|
success: true,
|
|
412
|
-
data:
|
|
455
|
+
data: merged,
|
|
413
456
|
});
|
|
414
457
|
} catch (err) {
|
|
415
458
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"ToolNamesListRequest",
|
|
92
92
|
"ToolPermissionSimulateRequest",
|
|
93
93
|
"TrustRulesList",
|
|
94
|
+
"TwilioConfigRequest",
|
|
94
95
|
"TwitterAuthStartRequest",
|
|
95
96
|
"TwitterAuthStatusRequest",
|
|
96
97
|
"TwitterIntegrationConfigRequest",
|
|
@@ -215,6 +216,7 @@
|
|
|
215
216
|
"ToolUseStart",
|
|
216
217
|
"TraceEvent",
|
|
217
218
|
"TrustRulesListResponse",
|
|
219
|
+
"TwilioConfigResponse",
|
|
218
220
|
"TwitterAuthResult",
|
|
219
221
|
"TwitterAuthStatusResponse",
|
|
220
222
|
"TwitterIntegrationConfigResponse",
|
|
@@ -338,6 +340,7 @@
|
|
|
338
340
|
"tool_names_list",
|
|
339
341
|
"tool_permission_simulate",
|
|
340
342
|
"trust_rules_list",
|
|
343
|
+
"twilio_config",
|
|
341
344
|
"twitter_auth_start",
|
|
342
345
|
"twitter_auth_status",
|
|
343
346
|
"twitter_integration_config",
|
|
@@ -462,6 +465,7 @@
|
|
|
462
465
|
"tool_use_start",
|
|
463
466
|
"trace_event",
|
|
464
467
|
"trust_rules_list_response",
|
|
468
|
+
"twilio_config_response",
|
|
465
469
|
"twitter_auth_result",
|
|
466
470
|
"twitter_auth_status_response",
|
|
467
471
|
"twitter_integration_config_response",
|
|
@@ -67,6 +67,10 @@ export interface SecretResponse {
|
|
|
67
67
|
|
|
68
68
|
export interface SessionListRequest {
|
|
69
69
|
type: 'session_list';
|
|
70
|
+
/** Number of sessions to skip (for pagination). Defaults to 0. */
|
|
71
|
+
offset?: number;
|
|
72
|
+
/** Maximum number of sessions to return. Defaults to 50. */
|
|
73
|
+
limit?: number;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
/** Lightweight session transport metadata for channel identity and natural-language guidance. */
|
|
@@ -557,6 +561,7 @@ export interface TwilioConfigRequest {
|
|
|
557
561
|
phoneNumber?: string; // Only for action: 'assign_number'
|
|
558
562
|
areaCode?: string; // Only for action: 'provision_number'
|
|
559
563
|
country?: string; // Only for action: 'provision_number' (ISO 3166-1 alpha-2, default 'US')
|
|
564
|
+
assistantId?: string; // Scope number assignment/lookup to a specific assistant
|
|
560
565
|
}
|
|
561
566
|
|
|
562
567
|
export interface TwilioConfigResponse {
|
|
@@ -566,6 +571,8 @@ export interface TwilioConfigResponse {
|
|
|
566
571
|
phoneNumber?: string;
|
|
567
572
|
numbers?: Array<{ phoneNumber: string; friendlyName: string; capabilities: { voice: boolean; sms: boolean } }>;
|
|
568
573
|
error?: string;
|
|
574
|
+
/** Non-fatal warning message (e.g. webhook sync failure that did not prevent the primary operation). */
|
|
575
|
+
warning?: string;
|
|
569
576
|
}
|
|
570
577
|
|
|
571
578
|
export interface GuardianVerificationRequest {
|
|
@@ -573,6 +580,7 @@ export interface GuardianVerificationRequest {
|
|
|
573
580
|
action: 'create_challenge' | 'status' | 'revoke';
|
|
574
581
|
channel?: string; // Defaults to 'telegram'
|
|
575
582
|
sessionId?: string;
|
|
583
|
+
assistantId?: string; // Defaults to 'self'
|
|
576
584
|
}
|
|
577
585
|
|
|
578
586
|
export interface GuardianVerificationResponse {
|
|
@@ -583,6 +591,12 @@ export interface GuardianVerificationResponse {
|
|
|
583
591
|
/** Present when action is 'status'. */
|
|
584
592
|
bound?: boolean;
|
|
585
593
|
guardianExternalUserId?: string;
|
|
594
|
+
/** The channel this status pertains to (e.g. "telegram", "sms"). Present when action is 'status'. */
|
|
595
|
+
channel?: string;
|
|
596
|
+
/** The assistant ID scoped to this status. Present when action is 'status'. */
|
|
597
|
+
assistantId?: string;
|
|
598
|
+
/** The delivery chat ID for the guardian (e.g. Telegram chat ID). Present when action is 'status' and bound is true. */
|
|
599
|
+
guardianDeliveryChatId?: string;
|
|
586
600
|
error?: string;
|
|
587
601
|
}
|
|
588
602
|
|
|
@@ -1271,6 +1285,8 @@ export interface ChannelBinding {
|
|
|
1271
1285
|
export interface SessionListResponse {
|
|
1272
1286
|
type: 'session_list_response';
|
|
1273
1287
|
sessions: Array<{ id: string; title: string; updatedAt: number; threadType?: ThreadType; channelBinding?: ChannelBinding }>;
|
|
1288
|
+
/** Whether more sessions exist beyond the returned page. */
|
|
1289
|
+
hasMore?: boolean;
|
|
1274
1290
|
}
|
|
1275
1291
|
|
|
1276
1292
|
export interface SessionsClearResponse {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC wire-level helpers: socket writing, broadcast, and assistant-event
|
|
3
|
+
* hub publishing. Extracted from DaemonServer to separate transport
|
|
4
|
+
* concerns from session management and business logic.
|
|
5
|
+
*/
|
|
6
|
+
import * as net from 'node:net';
|
|
7
|
+
import { serialize, type ServerMessage } from './ipc-protocol.js';
|
|
8
|
+
import { assistantEventHub } from '../runtime/assistant-event-hub.js';
|
|
9
|
+
import { buildAssistantEvent } from '../runtime/assistant-event.js';
|
|
10
|
+
import { getLogger } from '../util/logger.js';
|
|
11
|
+
|
|
12
|
+
const log = getLogger('ipc-handler');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Manages IPC message delivery: writing to individual sockets,
|
|
16
|
+
* broadcasting to all authenticated sockets, and publishing events
|
|
17
|
+
* to the assistant-events hub in order.
|
|
18
|
+
*/
|
|
19
|
+
export class IpcSender {
|
|
20
|
+
private _hubChain: Promise<void> = Promise.resolve();
|
|
21
|
+
|
|
22
|
+
/** Write to a single socket without publishing to the event hub. */
|
|
23
|
+
writeToSocket(socket: net.Socket, msg: ServerMessage): void {
|
|
24
|
+
if (!socket.destroyed && socket.writable) {
|
|
25
|
+
socket.write(serialize(msg));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Send a message to a single socket and publish to the event hub.
|
|
31
|
+
* `sessionId` is resolved from the message itself or the socket binding.
|
|
32
|
+
*/
|
|
33
|
+
send(
|
|
34
|
+
socket: net.Socket,
|
|
35
|
+
msg: ServerMessage,
|
|
36
|
+
socketToSession: Map<net.Socket, string>,
|
|
37
|
+
assistantId: string,
|
|
38
|
+
): void {
|
|
39
|
+
this.writeToSocket(socket, msg);
|
|
40
|
+
const sessionId = extractSessionId(msg) ?? socketToSession.get(socket);
|
|
41
|
+
this.publishAssistantEvent(msg, sessionId, assistantId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Broadcast a message to all authenticated sockets, then publish
|
|
46
|
+
* a single event to the hub.
|
|
47
|
+
*/
|
|
48
|
+
broadcast(
|
|
49
|
+
authenticatedSockets: Set<net.Socket>,
|
|
50
|
+
msg: ServerMessage,
|
|
51
|
+
socketToSession: Map<net.Socket, string>,
|
|
52
|
+
assistantId: string,
|
|
53
|
+
excludeSocket?: net.Socket,
|
|
54
|
+
): void {
|
|
55
|
+
for (const socket of authenticatedSockets) {
|
|
56
|
+
if (socket === excludeSocket) continue;
|
|
57
|
+
this.writeToSocket(socket, msg);
|
|
58
|
+
}
|
|
59
|
+
const sessionId = extractSessionId(msg)
|
|
60
|
+
?? (excludeSocket ? socketToSession.get(excludeSocket) : undefined);
|
|
61
|
+
this.publishAssistantEvent(msg, sessionId, assistantId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Publish `msg` as an `AssistantEvent` to the process-level hub.
|
|
66
|
+
* Publications are serialized via a promise chain so subscribers
|
|
67
|
+
* always observe events in send order.
|
|
68
|
+
*/
|
|
69
|
+
private publishAssistantEvent(msg: ServerMessage, sessionId?: string, assistantId?: string): void {
|
|
70
|
+
const id = assistantId ?? 'default';
|
|
71
|
+
const event = buildAssistantEvent(id, msg, sessionId);
|
|
72
|
+
this._hubChain = this._hubChain
|
|
73
|
+
.then(() => assistantEventHub.publish(event))
|
|
74
|
+
.catch((err: unknown) => {
|
|
75
|
+
log.warn({ err }, 'assistant-events hub subscriber threw during IPC send');
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Extract sessionId from a ServerMessage if present. */
|
|
81
|
+
function extractSessionId(msg: ServerMessage): string | undefined {
|
|
82
|
+
const record = msg as unknown as Record<string, unknown>;
|
|
83
|
+
if ('sessionId' in msg && typeof record.sessionId === 'string') {
|
|
84
|
+
return record.sessionId as string;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -429,11 +429,23 @@ export async function runDaemon(): Promise<void> {
|
|
|
429
429
|
if (httpPortEnv) {
|
|
430
430
|
const port = parseInt(httpPortEnv, 10);
|
|
431
431
|
if (!isNaN(port) && port > 0) {
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
432
|
+
// Resolve the bearer token in priority order:
|
|
433
|
+
// 1. Explicit env var (e.g. cloud deploys)
|
|
434
|
+
// 2. Existing token file on disk (preserves QR-paired iOS devices across restarts)
|
|
435
|
+
// 3. Fresh random token (first-time startup)
|
|
436
436
|
const httpTokenPath = getHttpTokenPath();
|
|
437
|
+
let bearerToken = process.env.RUNTIME_PROXY_BEARER_TOKEN;
|
|
438
|
+
if (!bearerToken) {
|
|
439
|
+
try {
|
|
440
|
+
const existing = readFileSync(httpTokenPath, 'utf-8').trim();
|
|
441
|
+
if (existing) bearerToken = existing;
|
|
442
|
+
} catch {
|
|
443
|
+
// File doesn't exist or can't be read — will generate below
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (!bearerToken) {
|
|
447
|
+
bearerToken = randomBytes(32).toString('hex');
|
|
448
|
+
}
|
|
437
449
|
writeFileSync(httpTokenPath, bearerToken, { mode: 0o600 });
|
|
438
450
|
chmodSync(httpTokenPath, 0o600);
|
|
439
451
|
|