@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.
Files changed (109) hide show
  1. package/README.md +82 -21
  2. package/package.json +1 -1
  3. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +16 -0
  4. package/src/__tests__/app-git-history.test.ts +22 -27
  5. package/src/__tests__/app-git-service.test.ts +44 -78
  6. package/src/__tests__/call-orchestrator.test.ts +321 -0
  7. package/src/__tests__/channel-approval-routes.test.ts +1267 -93
  8. package/src/__tests__/channel-approval.test.ts +2 -0
  9. package/src/__tests__/channel-approvals.test.ts +51 -2
  10. package/src/__tests__/channel-delivery-store.test.ts +130 -1
  11. package/src/__tests__/channel-guardian.test.ts +371 -1
  12. package/src/__tests__/config-schema.test.ts +1 -1
  13. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  14. package/src/__tests__/daemon-lifecycle.test.ts +635 -0
  15. package/src/__tests__/daemon-server-session-init.test.ts +5 -0
  16. package/src/__tests__/gateway-only-enforcement.test.ts +106 -21
  17. package/src/__tests__/handlers-telegram-config.test.ts +82 -0
  18. package/src/__tests__/handlers-twilio-config.test.ts +738 -5
  19. package/src/__tests__/ingress-url-consistency.test.ts +64 -0
  20. package/src/__tests__/ipc-snapshot.test.ts +10 -0
  21. package/src/__tests__/run-orchestrator.test.ts +1 -1
  22. package/src/__tests__/secret-scanner.test.ts +223 -0
  23. package/src/__tests__/session-process-bridge.test.ts +2 -0
  24. package/src/__tests__/shell-parser-property.test.ts +357 -2
  25. package/src/__tests__/system-prompt.test.ts +25 -1
  26. package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
  27. package/src/__tests__/tool-permission-simulate-handler.test.ts +2 -2
  28. package/src/__tests__/user-reference.test.ts +68 -0
  29. package/src/calls/call-orchestrator.ts +63 -11
  30. package/src/calls/twilio-config.ts +10 -1
  31. package/src/calls/twilio-rest.ts +70 -0
  32. package/src/cli/map.ts +6 -0
  33. package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
  34. package/src/commands/cc-command-registry.ts +14 -1
  35. package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
  36. package/src/config/bundled-skills/email-setup/SKILL.md +56 -0
  37. package/src/config/bundled-skills/messaging/SKILL.md +4 -0
  38. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  39. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  40. package/src/config/defaults.ts +1 -1
  41. package/src/config/schema.ts +6 -3
  42. package/src/config/skills.ts +5 -32
  43. package/src/config/system-prompt.ts +16 -0
  44. package/src/config/user-reference.ts +29 -0
  45. package/src/config/vellum-skills/catalog.json +52 -0
  46. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
  47. package/src/config/vellum-skills/twilio-setup/SKILL.md +49 -4
  48. package/src/daemon/auth-manager.ts +103 -0
  49. package/src/daemon/computer-use-session.ts +8 -1
  50. package/src/daemon/config-watcher.ts +253 -0
  51. package/src/daemon/handlers/config.ts +193 -17
  52. package/src/daemon/handlers/sessions.ts +5 -3
  53. package/src/daemon/handlers/skills.ts +60 -17
  54. package/src/daemon/ipc-contract-inventory.json +4 -0
  55. package/src/daemon/ipc-contract.ts +16 -0
  56. package/src/daemon/ipc-handler.ts +87 -0
  57. package/src/daemon/lifecycle.ts +16 -4
  58. package/src/daemon/ride-shotgun-handler.ts +11 -1
  59. package/src/daemon/server.ts +105 -502
  60. package/src/daemon/session-agent-loop.ts +9 -14
  61. package/src/daemon/session-process.ts +20 -3
  62. package/src/daemon/session-runtime-assembly.ts +60 -44
  63. package/src/daemon/session-slash.ts +50 -2
  64. package/src/daemon/session-surfaces.ts +17 -1
  65. package/src/daemon/session.ts +8 -1
  66. package/src/inbound/public-ingress-urls.ts +20 -3
  67. package/src/index.ts +1 -23
  68. package/src/memory/app-git-service.ts +24 -0
  69. package/src/memory/app-store.ts +0 -21
  70. package/src/memory/channel-delivery-store.ts +74 -3
  71. package/src/memory/channel-guardian-store.ts +54 -26
  72. package/src/memory/conversation-key-store.ts +20 -0
  73. package/src/memory/conversation-store.ts +14 -2
  74. package/src/memory/db-connection.ts +28 -0
  75. package/src/memory/db-init.ts +1019 -0
  76. package/src/memory/db.ts +2 -1995
  77. package/src/memory/embedding-backend.ts +79 -11
  78. package/src/memory/indexer.ts +2 -0
  79. package/src/memory/job-utils.ts +64 -4
  80. package/src/memory/jobs-worker.ts +7 -1
  81. package/src/memory/recall-cache.ts +107 -0
  82. package/src/memory/retriever.ts +30 -1
  83. package/src/memory/schema-migration.ts +984 -0
  84. package/src/memory/schema.ts +6 -0
  85. package/src/memory/search/types.ts +2 -0
  86. package/src/permissions/prompter.ts +14 -3
  87. package/src/permissions/trust-store.ts +7 -0
  88. package/src/runtime/channel-approvals.ts +17 -3
  89. package/src/runtime/gateway-client.ts +2 -1
  90. package/src/runtime/http-server.ts +28 -9
  91. package/src/runtime/routes/channel-routes.ts +279 -100
  92. package/src/runtime/routes/run-routes.ts +7 -1
  93. package/src/runtime/run-orchestrator.ts +8 -1
  94. package/src/security/secret-scanner.ts +218 -0
  95. package/src/skills/clawhub.ts +6 -2
  96. package/src/skills/frontmatter.ts +63 -0
  97. package/src/skills/slash-commands.ts +23 -0
  98. package/src/skills/vellum-catalog-remote.ts +107 -0
  99. package/src/subagent/manager.ts +4 -1
  100. package/src/subagent/types.ts +2 -0
  101. package/src/tools/browser/auto-navigate.ts +132 -24
  102. package/src/tools/browser/browser-manager.ts +67 -61
  103. package/src/tools/claude-code/claude-code.ts +55 -3
  104. package/src/tools/executor.ts +10 -2
  105. package/src/tools/skills/vellum-catalog.ts +75 -127
  106. package/src/tools/subagent/spawn.ts +2 -0
  107. package/src/tools/terminal/parser.ts +21 -5
  108. package/src/util/platform.ts +8 -1
  109. package/src/util/retry.ts +4 -4
@@ -71,6 +71,9 @@ mock.module('../config/loader.js', () => ({
71
71
  ingress: {
72
72
  publicBaseUrl: 'https://test.example.com',
73
73
  },
74
+ sms: {
75
+ phoneNumber: '+15550001111',
76
+ },
74
77
  }),
75
78
  getConfig: () => ({
76
79
  model: 'test',
@@ -82,6 +85,9 @@ mock.module('../config/loader.js', () => ({
82
85
  ingress: {
83
86
  publicBaseUrl: 'https://test.example.com',
84
87
  },
88
+ sms: {
89
+ phoneNumber: '+15550001111',
90
+ },
85
91
  }),
86
92
  invalidateConfigCache: () => {},
87
93
  }));
@@ -96,31 +102,28 @@ mock.module('../calls/twilio-provider.js', () => ({
96
102
  },
97
103
  }));
98
104
 
99
- // Mock Twilio config
100
- mock.module('../calls/twilio-config.js', () => ({
101
- getTwilioConfig: () => ({
102
- accountSid: 'AC_test',
103
- authToken: 'test_token',
104
- phoneNumber: '+15550001111',
105
- webhookBaseUrl: 'https://test.example.com',
106
- wssBaseUrl: 'wss://test.example.com',
107
- }),
108
- }));
105
+ const secureKeyStore: Record<string, string | undefined> = {
106
+ 'credential:twilio:account_sid': 'AC_test',
107
+ 'credential:twilio:auth_token': 'test_token',
108
+ 'credential:twilio:phone_number': '+15550001111',
109
+ };
109
110
 
110
111
  mock.module('../security/secure-keys.js', () => ({
111
- getSecureKey: () => null,
112
- setSecureKey: () => true,
113
- deleteSecureKey: () => {},
112
+ getSecureKey: (key: string) => secureKeyStore[key] ?? null,
113
+ setSecureKey: (key: string, value: string) => {
114
+ secureKeyStore[key] = value;
115
+ return true;
116
+ },
117
+ deleteSecureKey: (key: string) => {
118
+ delete secureKeyStore[key];
119
+ },
114
120
  }));
115
121
 
116
- mock.module('../inbound/public-ingress-urls.js', () => ({
117
- getPublicBaseUrl: () => 'https://test.example.com',
118
- getTwilioRelayUrl: () => 'wss://test.example.com/webhooks/twilio/relay',
119
- getTwilioVoiceWebhookUrl: (_cfg: unknown, id: string) => `https://test.example.com/webhooks/twilio/voice?callSessionId=${id}`,
120
- getTwilioStatusCallbackUrl: () => 'https://test.example.com/webhooks/twilio/status',
121
- getTwilioConnectActionUrl: () => 'https://test.example.com/webhooks/twilio/connect-action',
122
- getOAuthCallbackUrl: () => 'https://test.example.com/webhooks/oauth/callback',
123
- }));
122
+ // NOTE: Do NOT mock '../inbound/public-ingress-urls.js' here.
123
+ // Those are pure functions that derive URLs from the config object returned by
124
+ // loadConfig() (which is already mocked above). Mocking them at the module level
125
+ // leaks into other test files (e.g. ingress-url-consistency.test.ts) that need
126
+ // the real implementations, causing cross-test contamination.
124
127
 
125
128
  // Mock the oauth callback registry
126
129
  mock.module('../security/oauth-callback-registry.js', () => ({
@@ -289,6 +292,49 @@ describe('gateway-only ingress enforcement', () => {
289
292
  });
290
293
  });
291
294
 
295
+ // ── SMS-specific direct webhook routes blocked ──────────────────────
296
+
297
+ describe('SMS webhook routes are blocked at the runtime (gateway-only)', () => {
298
+
299
+ test('POST /webhooks/twilio/sms returns 410 (cannot bypass gateway)', async () => {
300
+ const res = await fetch(`http://127.0.0.1:${port}/webhooks/twilio/sms`, {
301
+ method: 'POST',
302
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
303
+ body: makeFormBody({ Body: 'hello', From: '+15551234567', To: '+15559876543', MessageSid: 'SM123' }),
304
+ });
305
+ expect(res.status).toBe(410);
306
+ const body = await res.json() as { error: string; code: string };
307
+ expect(body.code).toBe('GATEWAY_ONLY');
308
+ expect(body.error).toContain('Direct webhook access disabled');
309
+ });
310
+
311
+ test('POST /v1/calls/twilio/sms returns 410 (legacy path also blocked)', async () => {
312
+ const res = await fetch(`http://127.0.0.1:${port}/v1/calls/twilio/sms`, {
313
+ method: 'POST',
314
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
315
+ body: makeFormBody({ Body: 'hello', From: '+15551234567', MessageSid: 'SM456' }),
316
+ });
317
+ expect(res.status).toBe(410);
318
+ const body = await res.json() as { error: string; code: string };
319
+ expect(body.code).toBe('GATEWAY_ONLY');
320
+ });
321
+
322
+ test('POST /webhooks/twilio/sms with valid auth still returns 410 (auth does not bypass gateway-only)', async () => {
323
+ const res = await fetch(`http://127.0.0.1:${port}/webhooks/twilio/sms`, {
324
+ method: 'POST',
325
+ headers: {
326
+ ...AUTH_HEADERS,
327
+ 'Content-Type': 'application/x-www-form-urlencoded',
328
+ },
329
+ body: makeFormBody({ Body: 'sneaky', From: '+15551234567', MessageSid: 'SM789' }),
330
+ });
331
+ // The gateway-only guard runs before auth for Twilio webhook paths
332
+ expect(res.status).toBe(410);
333
+ const body = await res.json() as { error: string; code: string };
334
+ expect(body.code).toBe('GATEWAY_ONLY');
335
+ });
336
+ });
337
+
292
338
  // ── Internal forwarding routes still work ─────
293
339
 
294
340
  describe('internal forwarding routes are not blocked', () => {
@@ -601,6 +647,45 @@ describe('gateway-only ingress enforcement', () => {
601
647
  // header, the request is rejected if bearer auth is missing.
602
648
  expect(res.status).toBe(401);
603
649
  });
650
+
651
+ test('POST /v1/channels/inbound with SMS sourceChannel requires X-Gateway-Origin', async () => {
652
+ const res = await fetch(`http://127.0.0.1:${port}/v1/channels/inbound`, {
653
+ method: 'POST',
654
+ headers: {
655
+ ...AUTH_HEADERS,
656
+ 'Content-Type': 'application/json',
657
+ },
658
+ body: JSON.stringify({
659
+ sourceChannel: 'sms',
660
+ externalChatId: '+15551234567',
661
+ externalMessageId: 'SM-test-gw-1',
662
+ content: 'hello via SMS',
663
+ }),
664
+ });
665
+ // SMS messages must also go through the gateway — missing X-Gateway-Origin is rejected.
666
+ expect(res.status).toBe(403);
667
+ const body = await res.json() as { error: string; code: string };
668
+ expect(body.code).toBe('GATEWAY_ORIGIN_REQUIRED');
669
+ });
670
+
671
+ test('POST /v1/channels/inbound with SMS sourceChannel and valid X-Gateway-Origin passes', async () => {
672
+ const res = await fetch(`http://127.0.0.1:${port}/v1/channels/inbound`, {
673
+ method: 'POST',
674
+ headers: {
675
+ ...AUTH_HEADERS,
676
+ 'Content-Type': 'application/json',
677
+ 'X-Gateway-Origin': TEST_TOKEN,
678
+ },
679
+ body: JSON.stringify({
680
+ sourceChannel: 'sms',
681
+ externalChatId: '+15551234567',
682
+ externalMessageId: 'SM-test-gw-2',
683
+ content: 'hello via SMS',
684
+ }),
685
+ });
686
+ // Should NOT be 403 — the gateway-origin check passes.
687
+ expect(res.status).not.toBe(403);
688
+ });
604
689
  });
605
690
 
606
691
  // ── Startup warning for non-loopback host ──────────────────────────
@@ -905,6 +905,7 @@ describe('Telegram config handler', () => {
905
905
 
906
906
  import { handleGuardianVerification } from '../daemon/handlers/config.js';
907
907
  import type { GuardianVerificationRequest } from '../daemon/ipc-contract.js';
908
+ import { createBinding } from '../memory/channel-guardian-store.js';
908
909
 
909
910
  describe('Guardian verification IPC actions', () => {
910
911
  beforeEach(() => {
@@ -965,4 +966,85 @@ describe('Guardian verification IPC actions', () => {
965
966
  expect(res.success).toBe(false);
966
967
  expect(res.error).toContain('Unknown action');
967
968
  });
969
+
970
+ test('create_challenge with explicit assistantId scopes challenge to that assistant', () => {
971
+ const msg: GuardianVerificationRequest = {
972
+ type: 'guardian_verification',
973
+ action: 'create_challenge',
974
+ channel: 'telegram',
975
+ assistantId: 'asst-ipc-X',
976
+ };
977
+
978
+ const { ctx, sent } = createTestContext();
979
+ handleGuardianVerification(msg, {} as net.Socket, ctx);
980
+
981
+ expect(sent).toHaveLength(1);
982
+ const res = sent[0] as { type: string; success: boolean; secret?: string; instruction?: string };
983
+ expect(res.success).toBe(true);
984
+ expect(res.secret).toBeDefined();
985
+ expect(res.instruction).toContain('/guardian_verify');
986
+ });
987
+
988
+ test('status action with explicit assistantId checks binding for that assistant', () => {
989
+ // Create a control binding for a known assistant so we can verify
990
+ // that querying a *different* assistantId actually returns bound=false
991
+ // (not just because no bindings exist at all).
992
+ createBinding({
993
+ assistantId: 'asst-ipc-bound',
994
+ channel: 'telegram',
995
+ guardianExternalUserId: 'guardian-user-1',
996
+ guardianDeliveryChatId: 'guardian-chat-1',
997
+ });
998
+
999
+ // Querying a different assistant should return bound=false
1000
+ const unboundMsg: GuardianVerificationRequest = {
1001
+ type: 'guardian_verification',
1002
+ action: 'status',
1003
+ channel: 'telegram',
1004
+ assistantId: 'asst-ipc-Y',
1005
+ };
1006
+
1007
+ const { ctx: ctx1, sent: sent1 } = createTestContext();
1008
+ handleGuardianVerification(unboundMsg, {} as net.Socket, ctx1);
1009
+
1010
+ expect(sent1).toHaveLength(1);
1011
+ const unboundRes = sent1[0] as { type: string; success: boolean; bound: boolean };
1012
+ expect(unboundRes.success).toBe(true);
1013
+ expect(unboundRes.bound).toBe(false);
1014
+
1015
+ // Querying the bound assistant should return bound=true
1016
+ const boundMsg: GuardianVerificationRequest = {
1017
+ type: 'guardian_verification',
1018
+ action: 'status',
1019
+ channel: 'telegram',
1020
+ assistantId: 'asst-ipc-bound',
1021
+ };
1022
+
1023
+ const { ctx: ctx2, sent: sent2 } = createTestContext();
1024
+ handleGuardianVerification(boundMsg, {} as net.Socket, ctx2);
1025
+
1026
+ expect(sent2).toHaveLength(1);
1027
+ const boundRes = sent2[0] as { type: string; success: boolean; bound: boolean; guardianExternalUserId?: string };
1028
+ expect(boundRes.success).toBe(true);
1029
+ expect(boundRes.bound).toBe(true);
1030
+ expect(boundRes.guardianExternalUserId).toBe('guardian-user-1');
1031
+ });
1032
+
1033
+ test('assistantId defaults to "self" when not provided', () => {
1034
+ // create_challenge without assistantId should scope to 'self'
1035
+ const createMsg: GuardianVerificationRequest = {
1036
+ type: 'guardian_verification',
1037
+ action: 'create_challenge',
1038
+ channel: 'telegram',
1039
+ // assistantId intentionally omitted
1040
+ };
1041
+
1042
+ const { ctx: ctx1, sent: sent1 } = createTestContext();
1043
+ handleGuardianVerification(createMsg, {} as net.Socket, ctx1);
1044
+
1045
+ expect(sent1).toHaveLength(1);
1046
+ const createRes = sent1[0] as { type: string; success: boolean; secret?: string };
1047
+ expect(createRes.success).toBe(true);
1048
+ expect(createRes.secret).toBeDefined();
1049
+ });
968
1050
  });