@vellumai/assistant 0.3.3 → 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.
Files changed (75) hide show
  1. package/README.md +8 -16
  2. package/package.json +1 -1
  3. package/src/__tests__/call-orchestrator.test.ts +321 -0
  4. package/src/__tests__/channel-approval-routes.test.ts +382 -124
  5. package/src/__tests__/channel-approvals.test.ts +51 -2
  6. package/src/__tests__/channel-delivery-store.test.ts +30 -4
  7. package/src/__tests__/channel-guardian.test.ts +187 -0
  8. package/src/__tests__/config-schema.test.ts +1 -1
  9. package/src/__tests__/daemon-lifecycle.test.ts +635 -0
  10. package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
  11. package/src/__tests__/handlers-twilio-config.test.ts +73 -0
  12. package/src/__tests__/secret-scanner.test.ts +223 -0
  13. package/src/__tests__/shell-parser-property.test.ts +357 -2
  14. package/src/__tests__/system-prompt.test.ts +25 -1
  15. package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
  16. package/src/__tests__/user-reference.test.ts +68 -0
  17. package/src/calls/call-orchestrator.ts +63 -11
  18. package/src/cli/map.ts +6 -0
  19. package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
  20. package/src/commands/cc-command-registry.ts +14 -1
  21. package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
  22. package/src/config/bundled-skills/messaging/SKILL.md +4 -0
  23. package/src/config/defaults.ts +1 -1
  24. package/src/config/schema.ts +3 -3
  25. package/src/config/skills.ts +5 -32
  26. package/src/config/system-prompt.ts +16 -0
  27. package/src/config/user-reference.ts +29 -0
  28. package/src/config/vellum-skills/catalog.json +52 -0
  29. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
  30. package/src/config/vellum-skills/twilio-setup/SKILL.md +38 -0
  31. package/src/daemon/auth-manager.ts +103 -0
  32. package/src/daemon/computer-use-session.ts +8 -1
  33. package/src/daemon/config-watcher.ts +253 -0
  34. package/src/daemon/handlers/config.ts +36 -13
  35. package/src/daemon/handlers/skills.ts +6 -7
  36. package/src/daemon/ipc-contract.ts +6 -0
  37. package/src/daemon/ipc-handler.ts +87 -0
  38. package/src/daemon/lifecycle.ts +16 -4
  39. package/src/daemon/ride-shotgun-handler.ts +11 -1
  40. package/src/daemon/server.ts +105 -502
  41. package/src/daemon/session-agent-loop.ts +5 -14
  42. package/src/daemon/session-runtime-assembly.ts +60 -44
  43. package/src/daemon/session.ts +8 -1
  44. package/src/memory/db-connection.ts +28 -0
  45. package/src/memory/db-init.ts +1019 -0
  46. package/src/memory/db.ts +2 -2007
  47. package/src/memory/embedding-backend.ts +79 -11
  48. package/src/memory/indexer.ts +2 -0
  49. package/src/memory/job-utils.ts +64 -4
  50. package/src/memory/jobs-worker.ts +7 -1
  51. package/src/memory/recall-cache.ts +107 -0
  52. package/src/memory/retriever.ts +30 -1
  53. package/src/memory/schema-migration.ts +984 -0
  54. package/src/memory/schema.ts +1 -0
  55. package/src/memory/search/types.ts +2 -0
  56. package/src/permissions/prompter.ts +14 -3
  57. package/src/permissions/trust-store.ts +7 -0
  58. package/src/runtime/channel-approvals.ts +17 -3
  59. package/src/runtime/gateway-client.ts +2 -1
  60. package/src/runtime/http-server.ts +15 -4
  61. package/src/runtime/routes/channel-routes.ts +172 -84
  62. package/src/runtime/routes/run-routes.ts +7 -1
  63. package/src/runtime/run-orchestrator.ts +8 -1
  64. package/src/security/secret-scanner.ts +218 -0
  65. package/src/skills/frontmatter.ts +63 -0
  66. package/src/skills/slash-commands.ts +23 -0
  67. package/src/skills/vellum-catalog-remote.ts +107 -0
  68. package/src/tools/browser/auto-navigate.ts +132 -24
  69. package/src/tools/browser/browser-manager.ts +67 -61
  70. package/src/tools/claude-code/claude-code.ts +55 -3
  71. package/src/tools/executor.ts +10 -2
  72. package/src/tools/skills/vellum-catalog.ts +61 -156
  73. package/src/tools/terminal/parser.ts +21 -5
  74. package/src/util/platform.ts +8 -1
  75. package/src/util/retry.ts +4 -4
@@ -298,6 +298,53 @@ describe('handleChannelDecision', () => {
298
298
  expect(orchestrator.submitDecision).toHaveBeenCalledWith(run.id, 'deny');
299
299
  });
300
300
 
301
+ test('uses decision.runId to target the matching pending run', () => {
302
+ ensureConversation('conv-1');
303
+ const olderRun = createRun('conv-1');
304
+ setRunConfirmation(olderRun.id, {
305
+ ...sampleConfirmation,
306
+ toolName: 'shell',
307
+ toolUseId: 'req-older',
308
+ });
309
+ const newerRun = createRun('conv-1');
310
+ setRunConfirmation(newerRun.id, {
311
+ ...sampleConfirmation,
312
+ toolName: 'browser',
313
+ toolUseId: 'req-newer',
314
+ });
315
+
316
+ const orchestrator = makeMockOrchestrator();
317
+ const decision: ApprovalDecisionResult = {
318
+ action: 'approve_once',
319
+ source: 'telegram_button',
320
+ runId: newerRun.id,
321
+ };
322
+
323
+ const result = handleChannelDecision('conv-1', decision, orchestrator);
324
+ expect(result.applied).toBe(true);
325
+ expect(result.runId).toBe(newerRun.id);
326
+ expect(orchestrator.submitDecision).toHaveBeenCalledWith(newerRun.id, 'allow');
327
+ expect(orchestrator.submitDecision).not.toHaveBeenCalledWith(olderRun.id, 'allow');
328
+ });
329
+
330
+ test('returns applied: false when decision.runId does not match a pending run', () => {
331
+ ensureConversation('conv-1');
332
+ const run = createRun('conv-1');
333
+ setRunConfirmation(run.id, sampleConfirmation);
334
+
335
+ const orchestrator = makeMockOrchestrator();
336
+ const decision: ApprovalDecisionResult = {
337
+ action: 'approve_once',
338
+ source: 'telegram_button',
339
+ runId: 'run-missing',
340
+ };
341
+
342
+ const result = handleChannelDecision('conv-1', decision, orchestrator);
343
+ expect(result.applied).toBe(false);
344
+ expect(result.runId).toBeUndefined();
345
+ expect(orchestrator.submitDecision).not.toHaveBeenCalled();
346
+ });
347
+
301
348
  test('approve_always adds a trust rule and submits "allow"', () => {
302
349
  ensureConversation('conv-1');
303
350
  const run = createRun('conv-1');
@@ -319,14 +366,16 @@ describe('handleChannelDecision', () => {
319
366
  expect(result.applied).toBe(true);
320
367
  expect(result.runId).toBe(run.id);
321
368
 
322
- // Trust rule added with first allowlist and scope option
369
+ // Trust rule added with first allowlist and scope option.
370
+ // executionTarget is undefined for core tools like 'shell' — only
371
+ // skill-origin tools persist it (see channel-approvals.ts).
323
372
  expect(addRuleSpy).toHaveBeenCalledWith(
324
373
  'shell',
325
374
  'rm -rf *',
326
375
  '/tmp/project',
327
376
  'allow',
328
377
  100,
329
- { executionTarget: 'sandbox' },
378
+ { executionTarget: undefined },
330
379
  );
331
380
 
332
381
  // The run is still approved with a simple "allow"
@@ -24,7 +24,7 @@ mock.module('../util/logger.js', () => ({
24
24
  }));
25
25
 
26
26
  import { initializeDb, getDb, resetDb } from '../memory/db.js';
27
- import { channelInboundEvents, conversations, messages } from '../memory/schema.js';
27
+ import { channelInboundEvents, conversations, externalConversationBindings, messages } from '../memory/schema.js';
28
28
  import {
29
29
  recordInbound,
30
30
  linkMessage,
@@ -470,7 +470,7 @@ describe('channel-delivery-store', () => {
470
470
 
471
471
  // ── handleDeleteConversation assistantId parameter ───────────────
472
472
 
473
- test('handleDeleteConversation uses route assistantId param to delete scoped key', async () => {
473
+ test('handleDeleteConversation with non-self assistant deletes only scoped key', async () => {
474
474
  // Set up a scoped conversation key like the one created by recordInbound
475
475
  // with a specific assistantId.
476
476
  const convId = 'conv-delete-test';
@@ -488,6 +488,13 @@ describe('channel-delivery-store', () => {
488
488
  }).run();
489
489
  setConversationKey(scopedKey, convId);
490
490
  setConversationKey(legacyKey, convId);
491
+ db.insert(externalConversationBindings).values({
492
+ conversationId: convId,
493
+ sourceChannel: 'telegram',
494
+ externalChatId: 'chat-del',
495
+ createdAt: now,
496
+ updatedAt: now,
497
+ }).run();
491
498
 
492
499
  // Verify both keys exist
493
500
  expect(getConversationByKey(scopedKey)).not.toBeNull();
@@ -510,9 +517,15 @@ describe('channel-delivery-store', () => {
510
517
  const json = await res.json() as { ok: boolean };
511
518
  expect(json.ok).toBe(true);
512
519
 
513
- // Both the legacy key and the scoped key should be deleted
520
+ // Non-self delete should only remove the scoped key and preserve legacy.
514
521
  expect(getConversationByKey(scopedKey)).toBeNull();
515
- expect(getConversationByKey(legacyKey)).toBeNull();
522
+ expect(getConversationByKey(legacyKey)).not.toBeNull();
523
+ // Non-self delete should not mutate assistant-agnostic external bindings.
524
+ const remainingBinding = db.select()
525
+ .from(externalConversationBindings)
526
+ .where(eq(externalConversationBindings.conversationId, convId))
527
+ .get();
528
+ expect(remainingBinding).not.toBeNull();
516
529
  });
517
530
 
518
531
  test('handleDeleteConversation defaults to "self" when no assistantId provided', async () => {
@@ -530,6 +543,13 @@ describe('channel-delivery-store', () => {
530
543
  }).run();
531
544
  setConversationKey(scopedKey, convId);
532
545
  setConversationKey(legacyKey, convId);
546
+ db.insert(externalConversationBindings).values({
547
+ conversationId: convId,
548
+ sourceChannel: 'telegram',
549
+ externalChatId: 'chat-def',
550
+ createdAt: now,
551
+ updatedAt: now,
552
+ }).run();
533
553
 
534
554
  const req = new Request('http://localhost/channels/conversation', {
535
555
  method: 'DELETE',
@@ -546,5 +566,11 @@ describe('channel-delivery-store', () => {
546
566
 
547
567
  expect(getConversationByKey(scopedKey)).toBeNull();
548
568
  expect(getConversationByKey(legacyKey)).toBeNull();
569
+ // Self delete should keep external bindings in sync for the canonical route.
570
+ const remainingBinding = db.select()
571
+ .from(externalConversationBindings)
572
+ .where(eq(externalConversationBindings.conversationId, convId))
573
+ .get();
574
+ expect(remainingBinding).toBeUndefined();
549
575
  });
550
576
  });
@@ -52,6 +52,10 @@ import {
52
52
  isGuardian,
53
53
  revokeBinding as serviceRevokeBinding,
54
54
  } from '../runtime/channel-guardian-service.js';
55
+ import { handleGuardianVerification } from '../daemon/handlers/config.js';
56
+ import type { GuardianVerificationRequest, GuardianVerificationResponse } from '../daemon/ipc-contract.js';
57
+ import type { HandlerContext } from '../daemon/handlers/shared.js';
58
+ import type * as net from 'node:net';
55
59
 
56
60
  initializeDb();
57
61
 
@@ -1186,3 +1190,186 @@ describe('assistant-scoped approval request lookups', () => {
1186
1190
  expect(foundB!.toolName).toBe('browser');
1187
1191
  });
1188
1192
  });
1193
+
1194
+ // ═══════════════════════════════════════════════════════════════════════════
1195
+ // 10. IPC handler — channel-aware guardian status response
1196
+ // ═══════════════════════════════════════════════════════════════════════════
1197
+
1198
+ /**
1199
+ * Creates a minimal mock HandlerContext that captures the response sent via ctx.send().
1200
+ */
1201
+ function createMockCtx(): { ctx: HandlerContext; lastResponse: () => GuardianVerificationResponse | null } {
1202
+ let captured: GuardianVerificationResponse | null = null;
1203
+ const ctx = {
1204
+ sessions: new Map(),
1205
+ socketToSession: new Map(),
1206
+ cuSessions: new Map(),
1207
+ socketToCuSession: new Map(),
1208
+ cuObservationParseSequence: new Map(),
1209
+ socketSandboxOverride: new Map(),
1210
+ sharedRequestTimestamps: [],
1211
+ debounceTimers: { schedule: () => {}, cancel: () => {} } as unknown as HandlerContext['debounceTimers'],
1212
+ suppressConfigReload: false,
1213
+ setSuppressConfigReload: () => {},
1214
+ updateConfigFingerprint: () => {},
1215
+ send: (_socket: net.Socket, msg: unknown) => { captured = msg as GuardianVerificationResponse; },
1216
+ broadcast: () => {},
1217
+ clearAllSessions: () => 0,
1218
+ getOrCreateSession: () => Promise.resolve({} as never),
1219
+ touchSession: () => {},
1220
+ } as unknown as HandlerContext;
1221
+ return { ctx, lastResponse: () => captured };
1222
+ }
1223
+
1224
+ const mockSocket = {} as net.Socket;
1225
+
1226
+ describe('IPC handler channel-aware guardian status', () => {
1227
+ beforeEach(() => {
1228
+ resetTables();
1229
+ });
1230
+
1231
+ test('status action for telegram returns channel and assistantId fields', () => {
1232
+ const { ctx, lastResponse } = createMockCtx();
1233
+ const msg: GuardianVerificationRequest = {
1234
+ type: 'guardian_verification',
1235
+ action: 'status',
1236
+ channel: 'telegram',
1237
+ assistantId: 'self',
1238
+ };
1239
+
1240
+ handleGuardianVerification(msg, mockSocket, ctx);
1241
+
1242
+ const resp = lastResponse();
1243
+ expect(resp).not.toBeNull();
1244
+ expect(resp!.success).toBe(true);
1245
+ expect(resp!.channel).toBe('telegram');
1246
+ expect(resp!.assistantId).toBe('self');
1247
+ expect(resp!.bound).toBe(false);
1248
+ expect(resp!.guardianDeliveryChatId).toBeUndefined();
1249
+ });
1250
+
1251
+ test('status action for sms returns channel: sms and assistantId: self', () => {
1252
+ const { ctx, lastResponse } = createMockCtx();
1253
+ const msg: GuardianVerificationRequest = {
1254
+ type: 'guardian_verification',
1255
+ action: 'status',
1256
+ channel: 'sms',
1257
+ assistantId: 'self',
1258
+ };
1259
+
1260
+ handleGuardianVerification(msg, mockSocket, ctx);
1261
+
1262
+ const resp = lastResponse();
1263
+ expect(resp).not.toBeNull();
1264
+ expect(resp!.success).toBe(true);
1265
+ expect(resp!.channel).toBe('sms');
1266
+ expect(resp!.assistantId).toBe('self');
1267
+ expect(resp!.bound).toBe(false);
1268
+ });
1269
+
1270
+ test('status action returns guardianDeliveryChatId when bound', () => {
1271
+ createBinding({
1272
+ assistantId: 'self',
1273
+ channel: 'telegram',
1274
+ guardianExternalUserId: 'user-42',
1275
+ guardianDeliveryChatId: 'chat-42',
1276
+ });
1277
+
1278
+ const { ctx, lastResponse } = createMockCtx();
1279
+ const msg: GuardianVerificationRequest = {
1280
+ type: 'guardian_verification',
1281
+ action: 'status',
1282
+ channel: 'telegram',
1283
+ assistantId: 'self',
1284
+ };
1285
+
1286
+ handleGuardianVerification(msg, mockSocket, ctx);
1287
+
1288
+ const resp = lastResponse();
1289
+ expect(resp).not.toBeNull();
1290
+ expect(resp!.success).toBe(true);
1291
+ expect(resp!.bound).toBe(true);
1292
+ expect(resp!.guardianExternalUserId).toBe('user-42');
1293
+ expect(resp!.guardianDeliveryChatId).toBe('chat-42');
1294
+ expect(resp!.channel).toBe('telegram');
1295
+ expect(resp!.assistantId).toBe('self');
1296
+ });
1297
+
1298
+ test('status action defaults channel to telegram when omitted (backward compat)', () => {
1299
+ const { ctx, lastResponse } = createMockCtx();
1300
+ const msg: GuardianVerificationRequest = {
1301
+ type: 'guardian_verification',
1302
+ action: 'status',
1303
+ // channel omitted — should default to 'telegram'
1304
+ };
1305
+
1306
+ handleGuardianVerification(msg, mockSocket, ctx);
1307
+
1308
+ const resp = lastResponse();
1309
+ expect(resp).not.toBeNull();
1310
+ expect(resp!.channel).toBe('telegram');
1311
+ expect(resp!.assistantId).toBe('self');
1312
+ });
1313
+
1314
+ test('status action defaults assistantId to self when omitted (backward compat)', () => {
1315
+ const { ctx, lastResponse } = createMockCtx();
1316
+ const msg: GuardianVerificationRequest = {
1317
+ type: 'guardian_verification',
1318
+ action: 'status',
1319
+ channel: 'sms',
1320
+ // assistantId omitted — should default to 'self'
1321
+ };
1322
+
1323
+ handleGuardianVerification(msg, mockSocket, ctx);
1324
+
1325
+ const resp = lastResponse();
1326
+ expect(resp).not.toBeNull();
1327
+ expect(resp!.assistantId).toBe('self');
1328
+ expect(resp!.channel).toBe('sms');
1329
+ });
1330
+
1331
+ test('status action with custom assistantId returns correct value', () => {
1332
+ createBinding({
1333
+ assistantId: 'asst-custom',
1334
+ channel: 'telegram',
1335
+ guardianExternalUserId: 'user-77',
1336
+ guardianDeliveryChatId: 'chat-77',
1337
+ });
1338
+
1339
+ const { ctx, lastResponse } = createMockCtx();
1340
+ const msg: GuardianVerificationRequest = {
1341
+ type: 'guardian_verification',
1342
+ action: 'status',
1343
+ channel: 'telegram',
1344
+ assistantId: 'asst-custom',
1345
+ };
1346
+
1347
+ handleGuardianVerification(msg, mockSocket, ctx);
1348
+
1349
+ const resp = lastResponse();
1350
+ expect(resp).not.toBeNull();
1351
+ expect(resp!.success).toBe(true);
1352
+ expect(resp!.bound).toBe(true);
1353
+ expect(resp!.assistantId).toBe('asst-custom');
1354
+ expect(resp!.channel).toBe('telegram');
1355
+ expect(resp!.guardianExternalUserId).toBe('user-77');
1356
+ expect(resp!.guardianDeliveryChatId).toBe('chat-77');
1357
+ });
1358
+
1359
+ test('status action for unbound sms does not return guardianDeliveryChatId', () => {
1360
+ const { ctx, lastResponse } = createMockCtx();
1361
+ const msg: GuardianVerificationRequest = {
1362
+ type: 'guardian_verification',
1363
+ action: 'status',
1364
+ channel: 'sms',
1365
+ };
1366
+
1367
+ handleGuardianVerification(msg, mockSocket, ctx);
1368
+
1369
+ const resp = lastResponse();
1370
+ expect(resp).not.toBeNull();
1371
+ expect(resp!.bound).toBe(false);
1372
+ expect(resp!.guardianDeliveryChatId).toBeUndefined();
1373
+ expect(resp!.guardianExternalUserId).toBeUndefined();
1374
+ });
1375
+ });
@@ -681,7 +681,7 @@ describe('AssistantConfigSchema', () => {
681
681
  userConsultTimeoutSeconds: 120,
682
682
  disclosure: {
683
683
  enabled: true,
684
- text: 'At the very beginning of the call, disclose that you are an AI assistant calling on behalf of the user.',
684
+ text: 'At the very beginning of the call, introduce yourself as an assistant calling on behalf of the user.',
685
685
  },
686
686
  safety: {
687
687
  denyCategories: [],