@vellumai/assistant 0.4.9 → 0.4.11

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 (116) hide show
  1. package/ARCHITECTURE.md +24 -0
  2. package/Dockerfile +1 -1
  3. package/README.md +16 -9
  4. package/package.json +1 -1
  5. package/src/__tests__/account-registry.test.ts +1 -0
  6. package/src/__tests__/actor-token-service.test.ts +1 -0
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  8. package/src/__tests__/asset-materialize-tool.test.ts +7 -0
  9. package/src/__tests__/asset-search-tool.test.ts +7 -0
  10. package/src/__tests__/browser-fill-credential.test.ts +1 -0
  11. package/src/__tests__/call-start-guardian-guard.test.ts +1 -0
  12. package/src/__tests__/channel-approval-routes.test.ts +29 -0
  13. package/src/__tests__/channel-guardian.test.ts +2143 -1546
  14. package/src/__tests__/channel-retry-sweep.test.ts +169 -14
  15. package/src/__tests__/claude-code-tool-profiles.test.ts +1 -0
  16. package/src/__tests__/computer-use-tools.test.ts +1 -0
  17. package/src/__tests__/contacts-tools.test.ts +1 -0
  18. package/src/__tests__/conversation-attention-telegram.test.ts +1 -0
  19. package/src/__tests__/credential-policy-validate.test.ts +97 -0
  20. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  21. package/src/__tests__/credential-vault-unit.test.ts +1 -0
  22. package/src/__tests__/credential-vault.test.ts +1 -0
  23. package/src/__tests__/delete-managed-skill-tool.test.ts +1 -0
  24. package/src/__tests__/file-edit-tool.test.ts +1 -0
  25. package/src/__tests__/file-read-tool.test.ts +1 -0
  26. package/src/__tests__/file-write-tool.test.ts +1 -0
  27. package/src/__tests__/followup-tools.test.ts +1 -0
  28. package/src/__tests__/gateway-only-guard.test.ts +1 -1
  29. package/src/__tests__/guardian-control-plane-policy.test.ts +5 -4
  30. package/src/__tests__/guardian-grant-minting.test.ts +3 -0
  31. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +4 -3
  32. package/src/__tests__/guardian-routing-state.test.ts +8 -0
  33. package/src/__tests__/headless-browser-interactions.test.ts +1 -0
  34. package/src/__tests__/headless-browser-navigate.test.ts +1 -0
  35. package/src/__tests__/headless-browser-read-tools.test.ts +1 -0
  36. package/src/__tests__/headless-browser-snapshot.test.ts +1 -0
  37. package/src/__tests__/host-file-edit-tool.test.ts +1 -0
  38. package/src/__tests__/host-file-read-tool.test.ts +1 -0
  39. package/src/__tests__/host-file-write-tool.test.ts +1 -0
  40. package/src/__tests__/host-shell-tool.test.ts +1 -0
  41. package/src/__tests__/lifecycle-docs-guard.test.ts +207 -0
  42. package/src/__tests__/managed-skill-lifecycle.test.ts +1 -0
  43. package/src/__tests__/media-reuse-story.e2e.test.ts +8 -0
  44. package/src/__tests__/messaging-send-tool.test.ts +1 -0
  45. package/src/__tests__/playbook-execution.test.ts +1 -0
  46. package/src/__tests__/playbook-tools.test.ts +1 -0
  47. package/src/__tests__/relay-server.test.ts +4 -0
  48. package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -0
  49. package/src/__tests__/schedule-tools.test.ts +1 -0
  50. package/src/__tests__/secret-onetime-send.test.ts +4 -0
  51. package/src/__tests__/secret-scanner-executor.test.ts +2 -0
  52. package/src/__tests__/send-notification-tool.test.ts +2 -0
  53. package/src/__tests__/shell-credential-ref.test.ts +1 -0
  54. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -0
  55. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  56. package/src/__tests__/skill-load-tool.test.ts +1 -0
  57. package/src/__tests__/skill-script-runner-host.test.ts +1 -0
  58. package/src/__tests__/skill-script-runner-sandbox.test.ts +1 -0
  59. package/src/__tests__/skill-script-runner.test.ts +1 -0
  60. package/src/__tests__/skill-tool-factory.test.ts +1 -0
  61. package/src/__tests__/subagent-tools.test.ts +1 -1
  62. package/src/__tests__/swarm-recursion.test.ts +1 -0
  63. package/src/__tests__/swarm-session-integration.test.ts +1 -0
  64. package/src/__tests__/swarm-tool.test.ts +1 -0
  65. package/src/__tests__/task-management-tools.test.ts +1 -0
  66. package/src/__tests__/task-tools.test.ts +1 -0
  67. package/src/__tests__/terminal-tools.test.ts +1 -0
  68. package/src/__tests__/tool-approval-handler.test.ts +2 -2
  69. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  70. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  71. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  72. package/src/__tests__/tool-executor-shell-integration.test.ts +1 -0
  73. package/src/__tests__/tool-executor.test.ts +1 -0
  74. package/src/__tests__/trust-context-guards.test.ts +218 -0
  75. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -0
  76. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +6 -0
  77. package/src/__tests__/trusted-contact-multichannel.test.ts +1 -0
  78. package/src/__tests__/trusted-contact-verification.test.ts +1 -0
  79. package/src/__tests__/view-image-tool.test.ts +1 -0
  80. package/src/calls/guardian-dispatch.ts +4 -4
  81. package/src/cli/mcp.ts +183 -3
  82. package/src/config/bundled-skills/agentmail/SKILL.md +4 -4
  83. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +1 -0
  84. package/src/config/bundled-skills/phone-calls/SKILL.md +17 -119
  85. package/src/config/system-prompt.ts +4 -2
  86. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  87. package/src/daemon/computer-use-session.ts +1 -0
  88. package/src/daemon/session-agent-loop.ts +1 -1
  89. package/src/daemon/session-memory.ts +2 -2
  90. package/src/daemon/session-runtime-assembly.ts +2 -2
  91. package/src/daemon/session-tool-setup.ts +1 -1
  92. package/src/mcp/client.ts +55 -6
  93. package/src/mcp/manager.ts +9 -0
  94. package/src/mcp/mcp-oauth-provider.ts +347 -0
  95. package/src/memory/channel-delivery-store.ts +1 -0
  96. package/src/memory/db-init.ts +4 -0
  97. package/src/memory/delivery-status.ts +43 -0
  98. package/src/memory/guardian-bindings.ts +3 -3
  99. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +108 -0
  100. package/src/memory/migrations/index.ts +1 -0
  101. package/src/memory/migrations/registry.ts +6 -0
  102. package/src/memory/schema.ts +1 -1
  103. package/src/runtime/actor-trust-resolver.ts +13 -4
  104. package/src/runtime/channel-retry-sweep.ts +31 -14
  105. package/src/runtime/guardian-context-resolver.ts +25 -64
  106. package/src/runtime/guardian-outbound-actions.ts +399 -108
  107. package/src/runtime/guardian-vellum-migration.ts +1 -23
  108. package/src/runtime/guardian-verification-templates.ts +66 -30
  109. package/src/runtime/local-actor-identity.ts +4 -6
  110. package/src/runtime/middleware/actor-token.ts +2 -8
  111. package/src/runtime/routes/channel-route-shared.ts +0 -1
  112. package/src/runtime/routes/inbound-message-handler.ts +3 -4
  113. package/src/runtime/tool-grant-request-helper.ts +1 -1
  114. package/src/tools/credentials/policy-validate.ts +22 -0
  115. package/src/tools/guardian-control-plane-policy.ts +2 -2
  116. package/src/tools/types.ts +1 -1
@@ -6,26 +6,7 @@ metadata: {"vellum": {"emoji": "📞", "requires": {"config": ["calls.enabled"]}
6
6
  includes: ["public-ingress"]
7
7
  ---
8
8
 
9
- You are helping the user set up and manage phone calls via Twilio. This skill covers the full lifecycle: Twilio account setup, credential storage, public ingress configuration, enabling the calls feature, placing outbound calls, receiving inbound calls, and monitoring live transcripts.
10
-
11
- ## Prerequisites — Shared Twilio Setup
12
-
13
- Twilio credentials and phone number configuration are shared between voice calls and SMS messaging. If Twilio is not yet configured, install and load the **twilio-setup** skill first:
14
-
15
- - Call `vellum_skills_catalog` with `action: "install"`, `skill_id: "twilio-setup"`, and `overwrite: true` (so it succeeds even if already installed).
16
- - Then call `skill_load` with `skill: "twilio-setup"`.
17
-
18
- The twilio-setup skill handles credential storage, phone number provisioning/assignment, and public ingress setup. Once complete, return here to enable the calls feature and start making calls.
19
-
20
- Check if Twilio is already configured:
21
-
22
- ```bash
23
- TOKEN=$(cat ~/.vellum/http-token)
24
- curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/config" \
25
- -H "Authorization: Bearer $TOKEN"
26
- ```
27
-
28
- If `hasCredentials` is `true` and `phoneNumber` is set, skip directly to **Step 5: Enable Calls** below.
9
+ You are helping the user set up and manage phone calls via Twilio. This skill covers enabling the calls feature, placing outbound calls, receiving inbound calls, and interacting with live calls. Twilio credential storage, phone number provisioning, and public ingress are handled by the **twilio-setup** skill.
29
10
 
30
11
  ## Overview
31
12
 
@@ -39,7 +20,7 @@ When a call is placed:
39
20
  2. Twilio connects to the gateway's voice webhook, which returns TwiML
40
21
  3. Twilio opens a ConversationRelay WebSocket for real-time voice streaming
41
22
  4. An LLM-driven orchestrator manages the conversation — receiving caller speech (transcribed by Deepgram), generating responses via Claude, and streaming text back for TTS playback
42
- 5. The transcript is relayed live to the user's conversation thread
23
+ 5. The full transcript is stored in the database for later retrieval
43
24
 
44
25
  ### Inbound calls
45
26
 
@@ -61,9 +42,9 @@ You can keep using Twilio only — no changes needed. Enabling ElevenLabs can im
61
42
 
62
43
  The user's assistant gets its own personal phone number through Twilio. All implicit calls (without an explicit mode) always use this assistant number. Optionally, users can call from their own phone number if it's authorized with the Twilio account — this must be explicitly requested per call via `caller_identity_mode="user_number"`.
63
44
 
64
- ## Step 1: Check Current Configuration
45
+ ## Step 1: Verify Twilio Setup
65
46
 
66
- First, check whether Twilio is already configured:
47
+ Check whether Twilio credentials, phone number, and public ingress are already configured:
67
48
 
68
49
  ```bash
69
50
  TOKEN=$(cat ~/.vellum/http-token)
@@ -71,73 +52,20 @@ curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/config" \
71
52
  -H "Authorization: Bearer $TOKEN"
72
53
  ```
73
54
 
74
- Also check calls feature status:
75
-
76
55
  ```bash
77
56
  vellum config get calls.enabled
78
57
  ```
79
58
 
80
- If the config response shows `hasCredentials: true` and `phoneNumber` is set, and `calls.enabled` is `true`, skip to the **Making Calls** section. If credentials are partially configured, skip to whichever step is still needed.
81
-
82
- ## Step 2: Create a Twilio Account
83
-
84
- If the user doesn't have a Twilio account yet, guide them through setup:
85
-
86
- 1. Tell the user: **"You'll need a Twilio account to make phone calls. Sign up at https://www.twilio.com/try-twilio — it's free to start and includes trial credit."**
87
- 2. Once they have an account, they need two pieces of information:
88
- - **Account SID** — found on the Twilio Console dashboard at https://console.twilio.com
89
- - **Auth Token** — found on the same dashboard (click "Show" to reveal it)
90
-
91
- Tell the user: **"The assistant will get its own personal phone number through Twilio — the number that shows up on caller ID when calls are placed."**
92
-
93
- ## Step 3: Store Twilio Credentials
94
-
95
- **IMPORTANT — Secure credential collection only:** Never use credentials pasted in plaintext chat. Always collect credentials through the secure credential prompt flow:
96
-
97
- - Call `credential_store` with `action: "prompt"`, `service: "twilio"`, `field: "account_sid"`, `label: "Twilio Account SID"`, `description: "Enter your Account SID from the Twilio Console dashboard"`, and `placeholder: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"`.
98
- - Call `credential_store` with `action: "prompt"`, `service: "twilio"`, `field: "auth_token"`, `label: "Twilio Auth Token"`, `description: "Enter your Auth Token from the Twilio Console dashboard"`, and `placeholder: "your_auth_token"`.
99
-
100
- After both credentials are collected, send them to the gateway:
101
-
102
- ```bash
103
- TOKEN=$(cat ~/.vellum/http-token)
104
- curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/twilio/credentials" \
105
- -H "Authorization: Bearer $TOKEN" \
106
- -H "Content-Type: application/json" \
107
- -d '{"accountSid":"<value from credential_store for twilio/account_sid>","authToken":"<value from credential_store for twilio/auth_token>"}'
108
- ```
59
+ If `hasCredentials` is `true`, `phoneNumber` is set, and `calls.enabled` is `true`, skip to the **Making Outbound Calls** section.
109
60
 
110
- The endpoint validates the credentials against the Twilio API before storing them. If credentials are invalid, ask the user to re-enter via the secure prompt.
61
+ If Twilio is not yet configured, load the **twilio-setup** skill it handles credential storage, phone number provisioning, and public ingress setup:
111
62
 
112
- **Important:** Credentials are stored in the OS keychain (macOS Keychain / Linux secret-service) or encrypted at rest. They are never logged or exposed in plaintext.
113
-
114
- ## Step 4: Set Up Public Ingress
115
-
116
- Twilio needs a publicly reachable URL to send voice webhooks and establish the ConversationRelay WebSocket. The **public-ingress** skill handles this via ngrok.
117
-
118
- Check if ingress is already configured:
119
-
120
- ```bash
121
- vellum config get ingress.publicBaseUrl
122
- vellum config get ingress.enabled
123
- ```
124
-
125
- If not configured, load and run the public-ingress skill:
126
-
127
- ```
128
- skill_load skill=public-ingress
129
- ```
130
-
131
- Follow the public-ingress skill's instructions to set up the ngrok tunnel. Once complete, the gateway will be reachable at the configured `ingress.publicBaseUrl`.
132
-
133
- **Twilio needs these webhook endpoints (handled automatically by the gateway):**
134
- - Voice webhook: `{publicBaseUrl}/webhooks/twilio/voice`
135
- - Status callback: `{publicBaseUrl}/webhooks/twilio/status`
136
- - ConversationRelay WebSocket: `{publicBaseUrl}/webhooks/twilio/relay` (wss://)
63
+ - Call `vellum_skills_catalog` with `action: "install"`, `skill_id: "twilio-setup"`, and `overwrite: true`.
64
+ - Then call `skill_load` with `skill: "twilio-setup"`.
137
65
 
138
- No manual Twilio webhook configuration is needed — the assistant registers webhook URLs dynamically when placing each call.
66
+ Once twilio-setup completes, return here to enable calls.
139
67
 
140
- ## Step 5: Enable Calls
68
+ ## Step 2: Enable Calls
141
69
 
142
70
  Enable the calls feature:
143
71
 
@@ -150,7 +78,7 @@ Verify:
150
78
  vellum config get calls.enabled
151
79
  ```
152
80
 
153
- ## Step 6: Verify Setup (Test Call)
81
+ ## Step 3: Verify Setup (Test Call)
154
82
 
155
83
  Before making real calls, offer a quick verification:
156
84
 
@@ -176,7 +104,7 @@ If the user wants a specific call to appear as coming from their own phone numbe
176
104
  credential_store action=store service=twilio field=user_phone_number value=+14155559999
177
105
  ```
178
106
 
179
- **To use it for a specific call**, pass `caller_identity_mode: 'user_number'` when calling `call_start` — see the Making Calls section for examples. User-number mode cannot be set as a global default; it must be requested explicitly per call.
107
+ **To use it for a specific call**, pass `caller_identity_mode: 'user_number'` when calling `call_start` — see the Making Outbound Calls section for examples. User-number mode cannot be set as a global default; it must be requested explicitly per call.
180
108
 
181
109
  ### Configuration reference
182
110
 
@@ -351,7 +279,7 @@ Once Twilio is configured and the assistant has a phone number, inbound calls wo
351
279
  3. The LLM-driven orchestrator answers in receptionist mode — greeting the caller warmly and asking how it can help
352
280
  4. The conversation proceeds naturally, with ASK_GUARDIAN dispatches to consult the user when needed
353
281
 
354
- No additional configuration is needed beyond the standard Twilio setup (Steps 1-5 above). As long as `calls.enabled` is `true` and the phone number has been provisioned/assigned, inbound calls are handled automatically.
282
+ No additional configuration is needed beyond Twilio setup and `calls.enabled` being `true`. As long as the phone number has been provisioned/assigned, inbound calls are handled automatically.
355
283
 
356
284
  ### Guardian voice verification for inbound calls
357
285
 
@@ -361,31 +289,9 @@ Once a guardian binding exists for the voice channel, inbound callers may be pro
361
289
 
362
290
  This feature is separate from the outbound DTMF callee verification. It uses the channel guardian verification system rather than the per-call verification config.
363
291
 
364
- ## Live Call Monitoring
292
+ ## Interacting with a Live Call
365
293
 
366
- ### Showing the live transcript
367
-
368
- By default, always show the live transcript of the call as it happens. When a call is in progress:
369
-
370
- 1. After placing the call with `call_start`, immediately begin polling with `call_status` to track the call state
371
- 2. The system fires transcript notifications as the conversation unfolds — both caller speech and assistant responses appear in real time in the conversation thread
372
- 3. Present each transcript entry clearly as it arrives:
373
-
374
- ```
375
- 📞 Call in progress...
376
-
377
- 🗣️ Assistant: "Hi, I'm calling on behalf of John to make a dinner reservation for tonight."
378
- 👤 Caller: "Sure, what time would you like?"
379
- 🗣️ Assistant: "We'd like a table for two at 7pm, please."
380
- 👤 Caller: "Let me check... yes, we have availability at 7pm."
381
- 🗣️ Assistant: "Wonderful! The reservation would be under John Smith."
382
- ```
383
-
384
- 4. Continue monitoring until the call completes or fails
385
-
386
- ### Interacting with a live call
387
-
388
- During an active call, the user can interact with the AI voice agent via the HTTP API endpoints:
294
+ During an active call, the user can interact with the AI voice agent via the HTTP API endpoints. After placing a call with `call_start`, use `call_status` to poll the call state.
389
295
 
390
296
  #### Answering questions
391
297
 
@@ -424,7 +330,7 @@ When there is **no pending question** but the call is still active, the user can
424
330
 
425
331
  The instruction is injected into the AI voice agent's conversation context as high-priority input, and the agent adjusts its behavior accordingly.
426
332
 
427
- **Note:** Mid-call steering via the desktop chat thread is no longer supported. The desktop thread only receives pointer/status messages about the call. To steer a call, use the HTTP API endpoints directly.
333
+ **Note:** Steering is done via the HTTP API, not the desktop chat thread. The desktop thread only receives pointer/status messages about the call.
428
334
 
429
335
  ### Call status values
430
336
 
@@ -585,18 +491,10 @@ vellum config set calls.disclosure.text "Just so you know, this is an assistant
585
491
  vellum config set calls.userConsultTimeoutSeconds 300
586
492
  ```
587
493
 
588
- ## Accepted Regressions
589
-
590
- The following behavioral changes were introduced with the cross-channel guardian architecture (voice-cross-guardian):
591
-
592
- - **No more mid-call steering via desktop chat.** The call bridge that routed desktop chat messages to the active call has been removed. The desktop chat thread only receives pointer/status messages about the call. To steer a call, use the HTTP API endpoints directly (`POST /v1/calls/:id/instruction`).
593
- - **No live transcript mirror in the initiating chat.** The initiating desktop conversation no longer receives a real-time mirror of the call transcript. The initiating chat only gets pointer/status messages (call started, call ended, question asked, etc.).
594
- - **Guardian questions are dispatched cross-channel.** Rather than appearing only in the initiating desktop thread, ASK_GUARDIAN questions are now dispatched to all configured guardian channels (mac desktop, Telegram, SMS) simultaneously. The first channel to respond wins.
595
-
596
494
  ## Troubleshooting
597
495
 
598
496
  ### "Twilio credentials not configured"
599
- Run Step 3 to store your Account SID and Auth Token via the secure credential prompt flow, or load the `twilio-setup` skill.
497
+ Load the `twilio-setup` skill to store your Account SID and Auth Token.
600
498
 
601
499
  ### "Calls feature is disabled"
602
500
  Run `vellum config set calls.enabled true`.
@@ -410,7 +410,7 @@ function buildToolPermissionSection(): string {
410
410
  '',
411
411
  '### Always-Available Tools (No Approval Required)',
412
412
  '',
413
- '- **file_read** on your workspace directory — You can freely read any file under your `.vellum` workspace at any time. Use this proactively to check files, load context, and inform your responses without asking.',
413
+ '- **file_read** on your workspace directory — You can freely read any file under your `.vellum` workspace at any time. Use this proactively to check files, load context, and inform your responses without asking. **Always use `file_read` for workspace files (IDENTITY.md, USER.md, SOUL.md, etc.), never `host_file_read`.**',
414
414
  '- **web_search** — You can search the web at any time without approval. Use this to look up documentation, current information, or anything you need.',
415
415
  ].join('\n');
416
416
  }
@@ -688,7 +688,7 @@ function buildConfigSection(): string {
688
688
  return [
689
689
  '## Configuration',
690
690
  `- **Active model**: \`${config.model}\` (provider: ${config.provider})`,
691
- `${configPreamble} Key files you may read or edit include but are not limited to:`,
691
+ `${configPreamble} **Always use \`file_read\` and \`file_edit\` (not \`host_file_read\` / \`host_file_edit\`) for these files** — they are inside your sandbox working directory and do not require host access or user approval:`,
692
692
  '',
693
693
  '- `IDENTITY.md` — Your name, nature, personality, and emoji. Updated during the first-run ritual.',
694
694
  '- `SOUL.md` — Core principles, personality, and evolution guidance. Your behavioral foundation.',
@@ -723,6 +723,8 @@ function buildConfigSection(): string {
723
723
  '- They rename you or change your role',
724
724
  '- Your avatar appearance changes (update the `## Avatar` section with a description of the new look)',
725
725
  '',
726
+ 'When reading or updating workspace files, always use the sandbox tools (`file_read`, `file_edit`). Never use `host_file_read` or `host_file_edit` for workspace files — those are for host-only resources outside your workspace.',
727
+ '',
726
728
  'When updating, read the file first, then make a targeted edit. Include all useful information, but don\'t bloat the files over time',
727
729
  ].join('\n');
728
730
  }
@@ -46,7 +46,7 @@ In a multi-assistant environment (multiple assistants sharing the same daemon),
46
46
  - `DELETE /v1/integrations/twilio/credentials` — Removes the globally stored Account SID and Auth Token. This affects all assistants.
47
47
 
48
48
  **Assistant-scoped actions** (use `assistantId` query parameter to scope phone number configuration per assistant):
49
- - `GET /v1/integrations/twilio/config` — Returns the phone number assigned to the specified assistant (falls back to the legacy global number if no per-assistant mapping exists).
49
+ - `GET /v1/integrations/twilio/config` — Returns the phone number assigned to the specified assistant.
50
50
  - `POST /v1/integrations/twilio/numbers/assign` — Assigns a phone number to a specific assistant via the per-assistant mapping.
51
51
  - `POST /v1/integrations/twilio/numbers/provision` — Provisions a new number and assigns it to the specified assistant.
52
52
  - `GET /v1/integrations/twilio/numbers` — Lists all phone numbers on the shared Twilio account (uses global credentials).
@@ -499,6 +499,7 @@ export class ComputerUseSession {
499
499
  workingDir: getSandboxWorkingDir(),
500
500
  sessionId: this.sessionId,
501
501
  conversationId: this.sessionId,
502
+ guardianTrustClass: 'guardian',
502
503
  proxyToolResolver: proxyResolver,
503
504
  allowedToolNames,
504
505
  requestSecret: async (params) => {
@@ -299,7 +299,7 @@ export async function runAgentLoopImpl(
299
299
  conflictGate: ctx.conflictGate,
300
300
  scopeId: ctx.memoryPolicy.scopeId,
301
301
  includeDefaultFallback: ctx.memoryPolicy.includeDefaultFallback,
302
- guardianTrustClass: ctx.guardianContext?.trustClass,
302
+ guardianTrustClass: ctx.guardianContext?.trustClass ?? 'guardian',
303
303
  isInteractive: options?.isInteractive ?? (!ctx.hasNoClient && !ctx.headlessLock),
304
304
  },
305
305
  content,
@@ -34,7 +34,7 @@ export interface MemoryPrepareContext {
34
34
  conflictGate: ConflictGate;
35
35
  scopeId: string;
36
36
  includeDefaultFallback: boolean;
37
- guardianTrustClass?: 'guardian' | 'trusted_contact' | 'unknown';
37
+ guardianTrustClass: 'guardian' | 'trusted_contact' | 'unknown';
38
38
  /** When false (e.g. scheduled tasks), skip conflict clarification prompts. */
39
39
  isInteractive?: boolean;
40
40
  }
@@ -64,7 +64,7 @@ export async function prepareMemoryContext(
64
64
  // Provenance-based trust gating: untrusted actors skip all memory operations
65
65
  // (recall, dynamic profile, conflict gate) to prevent untrusted content from
66
66
  // influencing memory-augmented responses.
67
- const isTrustedActor = ctx.guardianTrustClass === 'guardian' || ctx.guardianTrustClass === undefined;
67
+ const isTrustedActor = ctx.guardianTrustClass === 'guardian';
68
68
 
69
69
  if (!isTrustedActor) {
70
70
  return {
@@ -38,8 +38,8 @@ export interface GuardianRuntimeContext {
38
38
  trustClass: 'guardian' | 'trusted_contact' | 'unknown';
39
39
  guardianChatId?: string;
40
40
  guardianExternalUserId?: string;
41
- /** Canonical principal ID for the guardian binding. Nullable for backward compatibility — M5 will make this required. */
42
- guardianPrincipalId?: string | null;
41
+ /** Canonical principal ID for the guardian binding. */
42
+ guardianPrincipalId?: string;
43
43
  requesterIdentifier?: string;
44
44
  requesterDisplayName?: string;
45
45
  requesterSenderDisplayName?: string;
@@ -110,7 +110,7 @@ export function createToolExecutor(
110
110
  assistantId: ctx.assistantId,
111
111
  requestId: ctx.currentRequestId,
112
112
  taskRunId: ctx.taskRunId,
113
- guardianTrustClass: ctx.guardianContext?.trustClass,
113
+ guardianTrustClass: ctx.guardianContext?.trustClass ?? 'guardian',
114
114
  executionChannel: ctx.guardianContext?.sourceChannel,
115
115
  callSessionId: ctx.callSessionId,
116
116
  requesterExternalUserId: ctx.guardianContext?.requesterExternalUserId,
package/src/mcp/client.ts CHANGED
@@ -1,10 +1,13 @@
1
+ import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js';
1
2
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
3
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
3
4
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
4
5
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
5
6
 
6
7
  import type { McpTransport } from '../config/mcp-schema.js';
8
+ import { getSecureKeyAsync } from '../security/secure-keys.js';
7
9
  import { getLogger } from '../util/logger.js';
10
+ import { McpOAuthProvider } from './mcp-oauth-provider.js';
8
11
 
9
12
  const log = getLogger('mcp-client');
10
13
 
@@ -26,9 +29,16 @@ export class McpClient {
26
29
  private client: Client;
27
30
  private transport: StdioClientTransport | SSEClientTransport | StreamableHTTPClientTransport | null = null;
28
31
  private connected = false;
32
+ private oauthProvider: McpOAuthProvider | null = null;
33
+ private quiet: boolean;
29
34
 
30
- constructor(serverId: string) {
35
+ get isConnected(): boolean {
36
+ return this.connected;
37
+ }
38
+
39
+ constructor(serverId: string, opts?: { quiet?: boolean }) {
31
40
  this.serverId = serverId;
41
+ this.quiet = opts?.quiet ?? false;
32
42
  this.client = new Client({
33
43
  name: 'vellum-assistant',
34
44
  version: '1.0.0',
@@ -38,8 +48,22 @@ export class McpClient {
38
48
  async connect(transportConfig: McpTransport): Promise<void> {
39
49
  if (this.connected) return;
40
50
 
41
- console.log(`[MCP] Connecting to server "${this.serverId}"...`);
51
+ const isHttpTransport = transportConfig.type === 'sse' || transportConfig.type === 'streamable-http';
52
+
53
+ // For HTTP transports, only attach an OAuth provider if cached tokens exist.
54
+ // This avoids triggering client registration (which binds to a random port)
55
+ // during daemon startup. If no tokens, try without auth — if the server
56
+ // requires it, skip silently.
57
+ if (isHttpTransport) {
58
+ const cachedTokens = await getSecureKeyAsync(`mcp:${this.serverId}:tokens`);
59
+ if (cachedTokens) {
60
+ this.oauthProvider = new McpOAuthProvider(this.serverId, transportConfig.url);
61
+ }
62
+ }
63
+
64
+ if (!this.quiet) console.log(`[MCP] Connecting to server "${this.serverId}"...`);
42
65
  this.transport = this.createTransport(transportConfig);
66
+
43
67
  try {
44
68
  await Promise.race([
45
69
  this.client.connect(this.transport),
@@ -48,13 +72,31 @@ export class McpClient {
48
72
  ),
49
73
  ]);
50
74
  } catch (err) {
51
- // Clean up the transport on failure (e.g., kill spawned stdio process)
52
75
  try { await this.client.close(); } catch { /* ignore cleanup errors */ }
53
76
  this.transport = null;
77
+
78
+ if (isHttpTransport) {
79
+ const isAuthError = err instanceof UnauthorizedError
80
+ || (err instanceof Error && /\b(401|403|unauthorized|forbidden)\b/i.test(err.message));
81
+
82
+ if (isAuthError) {
83
+ // Auth-related — user can run `vellum mcp auth <name>` to authenticate.
84
+ log.info({ serverId: this.serverId, err }, 'MCP server requires authentication');
85
+ if (!this.quiet) console.log(`[MCP] Server "${this.serverId}" requires authentication. Run "vellum mcp auth ${this.serverId}" to authenticate.`);
86
+ return;
87
+ }
88
+
89
+ // Non-auth error (DNS, TLS, timeout, etc.) — log and re-throw
90
+ log.error({ serverId: this.serverId, err }, 'MCP server connection failed');
91
+ if (!this.quiet) console.error(`[MCP] Server "${this.serverId}" connection failed: ${err instanceof Error ? err.message : err}`);
92
+ throw err;
93
+ }
94
+
54
95
  throw err;
55
96
  }
97
+
56
98
  this.connected = true;
57
- console.log(`[MCP] Server "${this.serverId}" connected successfully`);
99
+ if (!this.quiet) console.log(`[MCP] Server "${this.serverId}" connected successfully`);
58
100
  log.info({ serverId: this.serverId }, 'MCP client connected');
59
101
  }
60
102
 
@@ -140,13 +182,20 @@ export class McpClient {
140
182
  case 'sse':
141
183
  return new SSEClientTransport(
142
184
  new URL(config.url),
143
- { requestInit: config.headers ? { headers: config.headers } : undefined },
185
+ {
186
+ authProvider: this.oauthProvider ?? undefined,
187
+ requestInit: config.headers ? { headers: config.headers } : undefined,
188
+ },
144
189
  );
145
190
  case 'streamable-http':
146
191
  return new StreamableHTTPClientTransport(
147
192
  new URL(config.url),
148
- { requestInit: config.headers ? { headers: config.headers } : undefined },
193
+ {
194
+ authProvider: this.oauthProvider ?? undefined,
195
+ requestInit: config.headers ? { headers: config.headers } : undefined,
196
+ },
149
197
  );
150
198
  }
151
199
  }
152
200
  }
201
+
@@ -27,8 +27,17 @@ export class McpServerManager {
27
27
 
28
28
  try {
29
29
  console.log(`[MCP] Starting server "${serverId}" (transport: ${serverConfig.transport.type})`);
30
+ if (serverConfig.transport.type === 'sse' || serverConfig.transport.type === 'streamable-http') {
31
+ log.debug({ serverId }, 'HTTP transport — OAuth provider will be available if server requires authentication');
32
+ }
30
33
  const client = new McpClient(serverId);
31
34
  await client.connect(serverConfig.transport);
35
+
36
+ if (!client.isConnected) {
37
+ // Server requires authentication — connect() logged guidance
38
+ continue;
39
+ }
40
+
32
41
  this.clients.set(serverId, client);
33
42
  this.serverConfigs.set(serverId, serverConfig);
34
43