vellum 0.2.8 → 0.2.10

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 (94) hide show
  1. package/bun.lock +2 -2
  2. package/package.json +3 -2
  3. package/scripts/capture-x-graphql.ts +1 -18
  4. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +110 -0
  5. package/src/__tests__/call-bridge.test.ts +40 -0
  6. package/src/__tests__/call-state.test.ts +41 -0
  7. package/src/__tests__/config-schema.test.ts +0 -6
  8. package/src/__tests__/forbidden-legacy-symbols.test.ts +71 -0
  9. package/src/__tests__/gateway-only-enforcement.test.ts +95 -91
  10. package/src/__tests__/home-base-bootstrap.test.ts +13 -8
  11. package/src/__tests__/ingress-url-consistency.test.ts +214 -0
  12. package/src/__tests__/intent-routing.test.ts +2 -5
  13. package/src/__tests__/ipc-snapshot.test.ts +64 -14
  14. package/src/__tests__/oauth2-gateway-transport.test.ts +7 -1
  15. package/src/__tests__/onboarding-starter-tasks.test.ts +12 -2
  16. package/src/__tests__/prebuilt-home-base-seed.test.ts +9 -5
  17. package/src/__tests__/public-ingress-urls.test.ts +50 -34
  18. package/src/__tests__/relay-server.test.ts +55 -0
  19. package/src/__tests__/runtime-events-sse-parity.test.ts +343 -0
  20. package/src/__tests__/runtime-events-sse.test.ts +162 -0
  21. package/src/__tests__/skills.test.ts +83 -0
  22. package/src/__tests__/system-prompt.test.ts +2 -24
  23. package/src/__tests__/twilio-provider.test.ts +37 -1
  24. package/src/__tests__/twilio-routes.test.ts +112 -4
  25. package/src/__tests__/twitter-auth-handler.test.ts +87 -2
  26. package/src/calls/call-domain.ts +8 -6
  27. package/src/calls/call-orchestrator.ts +25 -5
  28. package/src/calls/call-state.ts +23 -0
  29. package/src/calls/relay-server.ts +56 -1
  30. package/src/calls/twilio-config.ts +11 -16
  31. package/src/calls/twilio-provider.ts +6 -1
  32. package/src/calls/twilio-routes.ts +10 -1
  33. package/src/cli/core-commands.ts +12 -4
  34. package/src/config/bundled-skills/app-builder/SKILL.md +57 -1
  35. package/src/config/bundled-skills/document/SKILL.md +11 -3
  36. package/src/config/bundled-skills/followups/icon.svg +24 -0
  37. package/src/config/bundled-skills/messaging/SKILL.md +7 -3
  38. package/src/config/bundled-skills/public-ingress/SKILL.md +183 -0
  39. package/src/config/bundled-skills/self-upgrade/SKILL.md +4 -10
  40. package/src/config/bundled-skills/tasks/TOOLS.json +25 -0
  41. package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +9 -0
  42. package/src/config/bundled-skills/transcribe/SKILL.md +25 -0
  43. package/src/config/bundled-skills/transcribe/TOOLS.json +32 -0
  44. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +370 -0
  45. package/src/config/defaults.ts +1 -2
  46. package/src/config/schema.ts +4 -11
  47. package/src/config/system-prompt.ts +64 -360
  48. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +10 -5
  49. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +9 -3
  50. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -3
  51. package/src/daemon/handlers/config.ts +44 -50
  52. package/src/daemon/handlers/home-base.ts +3 -2
  53. package/src/daemon/handlers/identity.ts +127 -0
  54. package/src/daemon/handlers/index.ts +4 -0
  55. package/src/daemon/handlers/shared.ts +1 -0
  56. package/src/daemon/handlers/subagents.ts +85 -2
  57. package/src/daemon/handlers/twitter-auth.ts +31 -2
  58. package/src/daemon/handlers/workspace-files.ts +75 -0
  59. package/src/daemon/ipc-contract-inventory.json +20 -8
  60. package/src/daemon/ipc-contract.ts +85 -21
  61. package/src/daemon/lifecycle.ts +9 -4
  62. package/src/daemon/server.ts +7 -0
  63. package/src/daemon/session-notifiers.ts +29 -0
  64. package/src/daemon/session-surfaces.ts +5 -2
  65. package/src/daemon/session-tool-setup.ts +16 -5
  66. package/src/home-base/bootstrap.ts +3 -1
  67. package/src/home-base/prebuilt/seed.ts +16 -5
  68. package/src/inbound/public-ingress-urls.ts +49 -32
  69. package/src/memory/db.ts +132 -5
  70. package/src/memory/llm-usage-store.ts +0 -1
  71. package/src/memory/runs-store.ts +51 -3
  72. package/src/memory/schema.ts +2 -2
  73. package/src/runtime/gateway-client.ts +7 -1
  74. package/src/runtime/http-server.ts +215 -27
  75. package/src/runtime/routes/channel-routes.ts +7 -2
  76. package/src/runtime/routes/events-routes.ts +79 -0
  77. package/src/runtime/routes/run-routes.ts +43 -0
  78. package/src/runtime/run-orchestrator.ts +64 -7
  79. package/src/security/oauth-callback-registry.ts +10 -0
  80. package/src/security/oauth2.ts +23 -131
  81. package/src/subagent/manager.ts +3 -1
  82. package/src/tools/browser/auto-navigate.ts +2 -2
  83. package/src/tools/browser/x-auto-navigate.ts +1 -1
  84. package/src/tools/claude-code/claude-code.ts +1 -1
  85. package/src/tools/system/version.ts +43 -0
  86. package/src/tools/tasks/work-item-run.ts +78 -0
  87. package/src/tools/terminal/parser.ts +29 -7
  88. package/src/tools/tool-manifest.ts +2 -0
  89. package/src/tools/ui-surface/definitions.ts +9 -2
  90. package/src/util/platform.ts +1 -1
  91. package/src/work-items/work-item-runner.ts +171 -0
  92. package/src/__tests__/handlers-twilio-config.test.ts +0 -221
  93. package/src/calls/__tests__/twilio-webhook-urls.test.ts +0 -162
  94. package/src/calls/twilio-webhook-urls.ts +0 -47
package/bun.lock CHANGED
@@ -11,7 +11,7 @@
11
11
  "@huggingface/transformers": "^3.8.1",
12
12
  "@qdrant/js-client-rest": "^1.16.2",
13
13
  "@sentry/node": "^10.38.0",
14
- "@vellumai/cli": "0.1.8",
14
+ "@vellumai/cli": "0.1.10",
15
15
  "@vellumai/vellum-gateway": "0.1.10",
16
16
  "agentmail": "^0.1.0",
17
17
  "archiver": "^7.0.1",
@@ -542,7 +542,7 @@
542
542
 
543
543
  "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg=="],
544
544
 
545
- "@vellumai/cli": ["@vellumai/cli@0.1.8", "", { "dependencies": { "ink": "^6.7.0", "react": "^19.2.4" }, "bin": { "vellum-cli": "src/index.ts" } }, "sha512-36P6W1rV1dK9Qg0N3oFjffz1CSarkGBgqc2ZGEI1de9RPSkVWBI0QmKCZrPeg1/qDzEh3InUIsV1qjNIe7z1pg=="],
545
+ "@vellumai/cli": ["@vellumai/cli@0.1.10", "", { "dependencies": { "ink": "^6.7.0", "react": "^19.2.4" }, "bin": { "vellum-cli": "src/index.ts" } }, "sha512-KBnfQRlt5VAJX6JMMYTWXq3Poi3YfVXE/O+C2249W1j58fxzxoRRENLK9WXDIKCIHlQ3+HRDug7ZAcYaEXURRA=="],
546
546
 
547
547
  "@vellumai/vellum-gateway": ["@vellumai/vellum-gateway@0.1.10", "", { "dependencies": { "file-type": "^21.3.0", "pino": "^9.6.0", "pino-pretty": "^13.1.3", "zod": "^4.3.6" } }, "sha512-a41fGexW8RpWL4RTfZ3EM+XJMvz7t26D1axu2xAtZioXW3ZWMLGuogHnIJsgglzESl49E6VmmUsUGeD+dseV2w=="],
548
548
 
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "vellum",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vellum": "./src/index.ts"
7
7
  },
8
8
  "scripts": {
9
9
  "dev": "bun run src/index.ts",
10
+ "daemon:restart:http": "RUNTIME_HTTP_PORT=7821 bun run src/index.ts daemon restart",
10
11
  "db:generate": "drizzle-kit generate",
11
12
  "db:push": "drizzle-kit push",
12
13
  "ipc:inventory": "bun run scripts/ipc/check-contract-inventory.ts",
@@ -28,7 +29,7 @@
28
29
  "@huggingface/transformers": "^3.8.1",
29
30
  "@qdrant/js-client-rest": "^1.16.2",
30
31
  "@sentry/node": "^10.38.0",
31
- "@vellumai/cli": "0.1.8",
32
+ "@vellumai/cli": "0.1.10",
32
33
  "@vellumai/vellum-gateway": "0.1.10",
33
34
  "agentmail": "^0.1.0",
34
35
  "archiver": "^7.0.1",
@@ -63,7 +63,7 @@ class CDPClient {
63
63
  const msg = JSON.parse(String(event.data));
64
64
  if (msg.id != null) {
65
65
  const cb = this.callbacks.get(msg.id);
66
- if (cb) { this.callbacks.delete(msg.id); msg.error ? cb.reject(new Error(msg.error.message)) : cb.resolve(msg.result); }
66
+ if (cb) { this.callbacks.delete(msg.id); if (msg.error) { cb.reject(new Error(msg.error.message)); } else { cb.resolve(msg.result); } }
67
67
  } else if (msg.method) {
68
68
  for (const h of this.eventHandlers.get(msg.method) ?? []) h(msg.params ?? {});
69
69
  }
@@ -216,20 +216,6 @@ function notifyQuerySeen(queryName: string) {
216
216
  }
217
217
  }
218
218
 
219
- function waitForQuery(queryName: string, timeoutMs = 15000): Promise<boolean> {
220
- if (seenQueries.has(queryName)) return Promise.resolve(true);
221
- return new Promise(resolve => {
222
- const timer = setTimeout(() => {
223
- queryWaiters.delete(queryName);
224
- resolve(false);
225
- }, timeoutMs);
226
- queryWaiters.set(queryName, () => {
227
- clearTimeout(timer);
228
- resolve(true);
229
- });
230
- });
231
- }
232
-
233
219
  function waitForAnyQuery(queryNames: string[], timeoutMs = 15000): Promise<boolean> {
234
220
  if (queryNames.some(q => seenQueries.has(q))) return Promise.resolve(true);
235
221
  return new Promise(resolve => {
@@ -251,7 +237,6 @@ function waitForAnyQuery(queryNames: string[], timeoutMs = 15000): Promise<boole
251
237
 
252
238
  // We'll keep a reference to one client that's on an x.com tab for navigation
253
239
  let navigationClient: CDPClient | null = null;
254
- let navigationWsUrl: string | null = null;
255
240
 
256
241
  for (const page of pages) {
257
242
  const client = new CDPClient();
@@ -261,7 +246,6 @@ for (const page of pages) {
261
246
  // Track which client is on an x.com tab for navigation
262
247
  if (page.url.includes('x.com') || page.url.includes('twitter.com')) {
263
248
  navigationClient = client;
264
- navigationWsUrl = page.webSocketDebuggerUrl;
265
249
  }
266
250
 
267
251
  client.on('Network.requestWillBeSent', (params) => {
@@ -347,7 +331,6 @@ for (const page of pages) {
347
331
  if (!navigationClient && pages.length > 0) {
348
332
  navigationClient = new CDPClient();
349
333
  await navigationClient.connect(pages[0].webSocketDebuggerUrl);
350
- navigationWsUrl = pages[0].webSocketDebuggerUrl;
351
334
  }
352
335
 
353
336
  // ─── CDP navigation helpers ──────────────────────────────────────────────────
@@ -530,6 +530,13 @@ exports[`IPC message snapshots ClientMessage types slack_webhook_config serializ
530
530
  }
531
531
  `;
532
532
 
533
+ exports[`IPC message snapshots ClientMessage types ingress_config serializes to expected JSON 1`] = `
534
+ {
535
+ "action": "get",
536
+ "type": "ingress_config",
537
+ }
538
+ `;
539
+
533
540
  exports[`IPC message snapshots ClientMessage types vercel_api_config serializes to expected JSON 1`] = `
534
541
  {
535
542
  "action": "get",
@@ -796,6 +803,33 @@ exports[`IPC message snapshots ClientMessage types subagent_message serializes t
796
803
  }
797
804
  `;
798
805
 
806
+ exports[`IPC message snapshots ClientMessage types subagent_detail_request serializes to expected JSON 1`] = `
807
+ {
808
+ "conversationId": "conv-001",
809
+ "subagentId": "sub-001",
810
+ "type": "subagent_detail_request",
811
+ }
812
+ `;
813
+
814
+ exports[`IPC message snapshots ClientMessage types workspace_files_list serializes to expected JSON 1`] = `
815
+ {
816
+ "type": "workspace_files_list",
817
+ }
818
+ `;
819
+
820
+ exports[`IPC message snapshots ClientMessage types workspace_file_read serializes to expected JSON 1`] = `
821
+ {
822
+ "path": "IDENTITY.md",
823
+ "type": "workspace_file_read",
824
+ }
825
+ `;
826
+
827
+ exports[`IPC message snapshots ClientMessage types identity_get serializes to expected JSON 1`] = `
828
+ {
829
+ "type": "identity_get",
830
+ }
831
+ `;
832
+
799
833
  exports[`IPC message snapshots ServerMessage types auth_result serializes to expected JSON 1`] = `
800
834
  {
801
835
  "success": true,
@@ -1448,6 +1482,14 @@ exports[`IPC message snapshots ServerMessage types watcher_escalation serializes
1448
1482
  }
1449
1483
  `;
1450
1484
 
1485
+ exports[`IPC message snapshots ServerMessage types agent_heartbeat_alert serializes to expected JSON 1`] = `
1486
+ {
1487
+ "body": "No activity detected in the last 60 minutes.",
1488
+ "title": "Agent heartbeat stalled",
1489
+ "type": "agent_heartbeat_alert",
1490
+ }
1491
+ `;
1492
+
1451
1493
  exports[`IPC message snapshots ServerMessage types watch_started serializes to expected JSON 1`] = `
1452
1494
  {
1453
1495
  "durationSeconds": 300,
@@ -1770,6 +1812,16 @@ exports[`IPC message snapshots ServerMessage types slack_webhook_config_response
1770
1812
  }
1771
1813
  `;
1772
1814
 
1815
+ exports[`IPC message snapshots ServerMessage types ingress_config_response serializes to expected JSON 1`] = `
1816
+ {
1817
+ "enabled": true,
1818
+ "localGatewayTarget": "http://127.0.0.1:7830",
1819
+ "publicBaseUrl": "https://example.com",
1820
+ "success": true,
1821
+ "type": "ingress_config_response",
1822
+ }
1823
+ `;
1824
+
1773
1825
  exports[`IPC message snapshots ServerMessage types vercel_api_config_response serializes to expected JSON 1`] = `
1774
1826
  {
1775
1827
  "hasToken": true,
@@ -2175,6 +2227,15 @@ exports[`IPC message snapshots ServerMessage types open_tasks_window serializes
2175
2227
  }
2176
2228
  `;
2177
2229
 
2230
+ exports[`IPC message snapshots ServerMessage types task_run_thread_created serializes to expected JSON 1`] = `
2231
+ {
2232
+ "conversationId": "conv-task-run-001",
2233
+ "title": "Process report",
2234
+ "type": "task_run_thread_created",
2235
+ "workItemId": "wi-001",
2236
+ }
2237
+ `;
2238
+
2178
2239
  exports[`IPC message snapshots ServerMessage types subagent_spawned serializes to expected JSON 1`] = `
2179
2240
  {
2180
2241
  "label": "Research Agent",
@@ -2204,3 +2265,52 @@ exports[`IPC message snapshots ServerMessage types subagent_event serializes to
2204
2265
  "type": "subagent_event",
2205
2266
  }
2206
2267
  `;
2268
+
2269
+ exports[`IPC message snapshots ServerMessage types subagent_detail_response serializes to expected JSON 1`] = `
2270
+ {
2271
+ "events": [
2272
+ {
2273
+ "content": "Reading file...",
2274
+ "isError": false,
2275
+ "toolName": "read_file",
2276
+ "type": "tool_use",
2277
+ },
2278
+ ],
2279
+ "objective": "Search for documentation",
2280
+ "subagentId": "sub-001",
2281
+ "type": "subagent_detail_response",
2282
+ }
2283
+ `;
2284
+
2285
+ exports[`IPC message snapshots ServerMessage types workspace_files_list_response serializes to expected JSON 1`] = `
2286
+ {
2287
+ "files": [
2288
+ {
2289
+ "exists": true,
2290
+ "name": "IDENTITY.md",
2291
+ "path": "IDENTITY.md",
2292
+ },
2293
+ ],
2294
+ "type": "workspace_files_list_response",
2295
+ }
2296
+ `;
2297
+
2298
+ exports[`IPC message snapshots ServerMessage types workspace_file_read_response serializes to expected JSON 1`] = `
2299
+ {
2300
+ "content": "# My Identity",
2301
+ "path": "IDENTITY.md",
2302
+ "type": "workspace_file_read_response",
2303
+ }
2304
+ `;
2305
+
2306
+ exports[`IPC message snapshots ServerMessage types identity_get_response serializes to expected JSON 1`] = `
2307
+ {
2308
+ "emoji": "✨",
2309
+ "found": true,
2310
+ "home": "~/workspace",
2311
+ "name": "Vex",
2312
+ "personality": "Friendly",
2313
+ "role": "AI assistant",
2314
+ "type": "identity_get_response",
2315
+ }
2316
+ `;
@@ -92,6 +92,9 @@ import {
92
92
  import {
93
93
  registerCallQuestionNotifier,
94
94
  unregisterCallQuestionNotifier,
95
+ registerCallTranscriptNotifier,
96
+ unregisterCallTranscriptNotifier,
97
+ fireCallTranscriptNotifier,
95
98
  registerCallCompletionNotifier,
96
99
  unregisterCallCompletionNotifier,
97
100
  fireCallQuestionNotifier,
@@ -335,6 +338,43 @@ describe('call-bridge', () => {
335
338
  unregisterCallQuestionNotifier('conv-notifier-q');
336
339
  });
337
340
 
341
+ // ── Call transcript notifier ─────────────────────────────────────
342
+
343
+ test('call transcript notifier persists transcript line and emits events', () => {
344
+ ensureConversation('conv-notifier-t');
345
+
346
+ const emittedEvents: Array<{ type: string; text?: string }> = [];
347
+ const sendToClient = (msg: { type: string; text?: string }) => {
348
+ emittedEvents.push(msg);
349
+ };
350
+
351
+ registerCallTranscriptNotifier('conv-notifier-t', (_callSessionId: string, speaker: 'caller' | 'assistant', text: string) => {
352
+ const speakerLabel = speaker === 'caller' ? 'Caller' : 'Assistant';
353
+ const transcriptText = `**Live call transcript**\n${speakerLabel}: ${text}`;
354
+ conversationStore.addMessage(
355
+ 'conv-notifier-t',
356
+ 'assistant',
357
+ JSON.stringify([{ type: 'text', text: transcriptText }]),
358
+ );
359
+ sendToClient({ type: 'assistant_text_delta', text: transcriptText });
360
+ sendToClient({ type: 'message_complete' });
361
+ });
362
+
363
+ fireCallTranscriptNotifier('conv-notifier-t', 'call-session-1', 'caller', 'Can you confirm the appointment?');
364
+
365
+ const msgs = getMessagesForConversation('conv-notifier-t');
366
+ expect(msgs.length).toBe(1);
367
+ expect(msgs[0].role).toBe('assistant');
368
+ expect(msgs[0].content).toContain('Caller: Can you confirm the appointment?');
369
+
370
+ expect(emittedEvents.length).toBe(2);
371
+ expect(emittedEvents[0].type).toBe('assistant_text_delta');
372
+ expect(emittedEvents[0].text).toContain('Live call transcript');
373
+ expect(emittedEvents[1].type).toBe('message_complete');
374
+
375
+ unregisterCallTranscriptNotifier('conv-notifier-t');
376
+ });
377
+
338
378
  // ── Call completion notifier ────────────────────────────────────
339
379
 
340
380
  test('call completion notifier persists summary and emits events', () => {
@@ -10,6 +10,9 @@ import {
10
10
  registerCallQuestionNotifier,
11
11
  unregisterCallQuestionNotifier,
12
12
  fireCallQuestionNotifier,
13
+ registerCallTranscriptNotifier,
14
+ unregisterCallTranscriptNotifier,
15
+ fireCallTranscriptNotifier,
13
16
  registerCallCompletionNotifier,
14
17
  unregisterCallCompletionNotifier,
15
18
  fireCallCompletionNotifier,
@@ -23,6 +26,7 @@ describe('call-state', () => {
23
26
  // Clean up notifiers between tests
24
27
  beforeEach(() => {
25
28
  unregisterCallQuestionNotifier('test-conv');
29
+ unregisterCallTranscriptNotifier('test-conv');
26
30
  unregisterCallCompletionNotifier('test-conv');
27
31
  unregisterCallOrchestrator('test-session');
28
32
  });
@@ -62,6 +66,43 @@ describe('call-state', () => {
62
66
  fireCallQuestionNotifier('unregistered-conv', 'session-1', 'question');
63
67
  });
64
68
 
69
+ // ── Transcript notifiers ──────────────────────────────────────────
70
+
71
+ test('registerCallTranscriptNotifier + fireCallTranscriptNotifier: callback receives args', () => {
72
+ let receivedSessionId = '';
73
+ let receivedSpeaker = '';
74
+ let receivedText = '';
75
+
76
+ registerCallTranscriptNotifier('test-conv', (callSessionId, speaker, text) => {
77
+ receivedSessionId = callSessionId;
78
+ receivedSpeaker = speaker;
79
+ receivedText = text;
80
+ });
81
+
82
+ fireCallTranscriptNotifier('test-conv', 'session-321', 'caller', 'Hello from caller');
83
+
84
+ expect(receivedSessionId).toBe('session-321');
85
+ expect(receivedSpeaker).toBe('caller');
86
+ expect(receivedText).toBe('Hello from caller');
87
+ });
88
+
89
+ test('unregisterCallTranscriptNotifier: fire after unregister does nothing', () => {
90
+ let called = false;
91
+
92
+ registerCallTranscriptNotifier('test-conv', () => {
93
+ called = true;
94
+ });
95
+
96
+ unregisterCallTranscriptNotifier('test-conv');
97
+ fireCallTranscriptNotifier('test-conv', 'session-321', 'assistant', 'Test');
98
+
99
+ expect(called).toBe(false);
100
+ });
101
+
102
+ test('fireCallTranscriptNotifier does nothing when no notifier is registered', () => {
103
+ fireCallTranscriptNotifier('unregistered-conv', 'session-1', 'caller', 'text');
104
+ });
105
+
65
106
  // ── Completion notifiers ──────────────────────────────────────────
66
107
 
67
108
  test('registerCallCompletionNotifier + fireCallCompletionNotifier: callback receives callSessionId', () => {
@@ -646,7 +646,6 @@ describe('AssistantConfigSchema', () => {
646
646
  expect(result.calls).toEqual({
647
647
  enabled: true,
648
648
  provider: 'twilio',
649
- webhookBaseUrl: '',
650
649
  maxDurationSeconds: 3600,
651
650
  userConsultTimeoutSeconds: 120,
652
651
  disclosure: {
@@ -659,11 +658,6 @@ describe('AssistantConfigSchema', () => {
659
658
  });
660
659
  });
661
660
 
662
- test('calls.webhookBaseUrl defaults to empty string', () => {
663
- const result = AssistantConfigSchema.parse({});
664
- expect(result.calls.webhookBaseUrl).toBe('');
665
- });
666
-
667
661
  test('accepts valid calls config overrides', () => {
668
662
  const result = AssistantConfigSchema.parse({
669
663
  calls: {
@@ -0,0 +1,71 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { execSync } from 'node:child_process';
3
+ import { resolve } from 'node:path';
4
+
5
+ /**
6
+ * Guard test: fail if any legacy Twilio ingress symbols reappear in
7
+ * production source code, docs, configs, or scripts.
8
+ *
9
+ * Context: As part of the gateway-only ingress migration (#5948, #6000),
10
+ * all Twilio webhook configuration was consolidated into the gateway service.
11
+ * The assistant no longer manages its own Twilio webhook URLs — the gateway
12
+ * is the single ingress point for all telephony webhooks. Re-introducing
13
+ * these symbols in the assistant would bypass that architecture and create
14
+ * a split-brain ingress problem.
15
+ *
16
+ * Forbidden symbols:
17
+ * - legacy uppercase Twilio webhook base env var
18
+ * - twilioWebhookBaseUrl
19
+ * - twilio_webhook_config
20
+ * - calls.webhookBaseUrl
21
+ *
22
+ * Excluded directories:
23
+ * - node_modules — third-party code, not under our control
24
+ * - __tests__ — test files (including this guard test) reference the
25
+ * symbols in grep patterns and assertions
26
+ * - .private — local-only developer notes and scratch files
27
+ */
28
+ describe('forbidden legacy symbols', () => {
29
+ test('no production code references removed Twilio ingress symbols', () => {
30
+ const legacyEnvVar = ['TWILIO', 'WEBHOOK', 'BASE', 'URL'].join('_');
31
+ const forbiddenSymbols = [
32
+ legacyEnvVar,
33
+ 'twilioWebhookBaseUrl',
34
+ 'twilio_webhook_config',
35
+ 'calls.webhookBaseUrl',
36
+ ];
37
+ const escapedPattern = forbiddenSymbols
38
+ .map((symbol) => symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
39
+ .join('|');
40
+
41
+ const repoRoot = resolve(__dirname, '..', '..', '..');
42
+ let matches = '';
43
+ try {
44
+ // Use git grep so only tracked files are searched. This automatically
45
+ // excludes untracked local .env files while still scanning committed
46
+ // environment templates like .env.example.
47
+ matches = execSync(
48
+ `git grep -rn -E "${escapedPattern}" --` +
49
+ ' "*.ts" "*.tsx" "*.js" "*.mjs" "*.swift"' +
50
+ ' "*.json" "*.md" "*.yml" "*.yaml"' +
51
+ ' "*.sh" "*.env" "*.env.*"' +
52
+ ' ":!node_modules" ":!*/__tests__/*" ":!.private"',
53
+ { cwd: repoRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
54
+ );
55
+ } catch (err: unknown) {
56
+ // grep exits with code 1 when no matches are found — that is the expected (passing) case
57
+ const exitCode = (err as { status?: number }).status;
58
+ if (exitCode === 1) {
59
+ // No matches found — test passes
60
+ return;
61
+ }
62
+ // Any other error is unexpected
63
+ throw err;
64
+ }
65
+
66
+ // If we reach here, grep found matches (exit code 0) — fail the test
67
+ expect(matches.trim()).toBe(
68
+ '', // should be empty — if not, the matched lines appear in the failure message
69
+ );
70
+ });
71
+ });