@vellumai/assistant 0.11.3 → 0.11.4-staging.1

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 (324) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/docs/architecture/memory.md +11 -0
  3. package/docs/architecture/turn-actor.md +70 -0
  4. package/docs/flux-turn-detection-spike.md +243 -0
  5. package/docs/stt-provider-onboarding.md +3 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  8. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  10. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  11. package/openapi.yaml +140 -38
  12. package/package.json +1 -1
  13. package/scripts/voice-ttft-spike.ts +3 -3
  14. package/src/__tests__/app-compiler.test.ts +38 -3
  15. package/src/__tests__/attachments-store.test.ts +21 -12
  16. package/src/__tests__/byok-default-profile-ensure.test.ts +2 -0
  17. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  18. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  19. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  20. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  21. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  22. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  23. package/src/__tests__/config-schema.test.ts +25 -10
  24. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  26. package/src/__tests__/conversation-agent-loop.test.ts +28 -20
  27. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  28. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  29. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  30. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  31. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  32. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  33. package/src/__tests__/conversation-queue.test.ts +55 -4
  34. package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
  35. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  36. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  37. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  38. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  39. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  40. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  41. package/src/__tests__/edit-propagation.test.ts +0 -7
  42. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  43. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  44. package/src/__tests__/injector-chain.test.ts +63 -41
  45. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  46. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  47. package/src/__tests__/llm-schema.test.ts +5 -2
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  49. package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
  50. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  51. package/src/__tests__/mtime-cache.test.ts +1 -1
  52. package/src/__tests__/non-member-access-request.test.ts +0 -20
  53. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
  55. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  56. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  57. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  58. package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
  59. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  60. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  61. package/src/__tests__/skills.test.ts +32 -0
  62. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  63. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  64. package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
  65. package/src/__tests__/turn-events-store.test.ts +43 -0
  66. package/src/__tests__/user-plugin-loader.test.ts +1 -1
  67. package/src/__tests__/visible-app-context.test.ts +16 -9
  68. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  69. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  70. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  71. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  72. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  73. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  74. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  75. package/src/acp/__tests__/auth-required.test.ts +161 -0
  76. package/src/acp/acp-claude-oauth.ts +19 -2
  77. package/src/acp/agent-process.test.ts +100 -0
  78. package/src/acp/agent-process.ts +29 -26
  79. package/src/acp/auth-required.ts +102 -0
  80. package/src/acp/session-manager.test.ts +119 -0
  81. package/src/acp/session-manager.ts +68 -2
  82. package/src/api/events/acp-auth-required.ts +55 -0
  83. package/src/api/index.ts +7 -0
  84. package/src/apps/app-store.ts +3 -0
  85. package/src/bundler/package-resolver.ts +2 -30
  86. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  87. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  88. package/src/calls/call-controller.ts +9 -2
  89. package/src/calls/call-setup-flow.ts +0 -1
  90. package/src/calls/media-stream-stt-session.ts +15 -0
  91. package/src/calls/voice-session-bridge.ts +71 -16
  92. package/src/calls/voice-triage-escalate.ts +104 -2
  93. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  94. package/src/channels/config.ts +13 -0
  95. package/src/channels/plugin-channel-declarations.ts +108 -0
  96. package/src/channels/types.ts +30 -0
  97. package/src/cli/AGENTS.md +5 -2
  98. package/src/cli/commands/credentials.help.ts +2 -2
  99. package/src/cli/commands/inference-providers.ts +1 -1
  100. package/src/cli/commands/mcp.help.ts +13 -4
  101. package/src/cli/commands/mcp.ts +9 -0
  102. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  103. package/src/cli/commands/memory/index.help.ts +43 -1
  104. package/src/cli/commands/memory/memory-v3.ts +64 -0
  105. package/src/cli/commands/stt.help.ts +27 -2
  106. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  107. package/src/cli/lib/bundled-marketplace.json +13 -0
  108. package/src/cli/lib/upgrade-plugin.ts +42 -0
  109. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  110. package/src/config/__tests__/default-provider.test.ts +6 -1
  111. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  112. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  113. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  114. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  115. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  116. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  117. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  118. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  119. package/src/config/call-site-defaults.ts +4 -5
  120. package/src/config/default-profile-catalog.ts +82 -11
  121. package/src/config/default-profile-names.ts +4 -1
  122. package/src/config/default-provider-resolution.ts +4 -0
  123. package/src/config/llm-context-resolution.ts +11 -3
  124. package/src/config/llm-resolver.ts +28 -1
  125. package/src/config/profile-materialization.ts +70 -22
  126. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  127. package/src/config/schemas/call-site-catalog.ts +4 -4
  128. package/src/config/schemas/live-voice.ts +57 -23
  129. package/src/config/schemas/llm.ts +59 -32
  130. package/src/config/schemas/mcp.ts +23 -0
  131. package/src/config/schemas/plugin-updates.ts +6 -2
  132. package/src/config/schemas/stt.ts +1 -0
  133. package/src/context/outbound-sanitize.ts +96 -1
  134. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  135. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  136. package/src/daemon/conversation-agent-loop.ts +17 -6
  137. package/src/daemon/conversation-messaging.ts +5 -1
  138. package/src/daemon/conversation-notifiers.ts +9 -1
  139. package/src/daemon/conversation-process.ts +9 -6
  140. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  141. package/src/daemon/conversation-tool-setup.ts +1 -2
  142. package/src/daemon/conversation.ts +48 -0
  143. package/src/daemon/mcp-reload-service.ts +36 -6
  144. package/src/daemon/process-message.ts +13 -3
  145. package/src/daemon/providers-setup.ts +6 -3
  146. package/src/daemon/trust-context-types.ts +29 -0
  147. package/src/daemon/wake-conversation-ops.ts +3 -2
  148. package/src/documents/document-store.ts +138 -5
  149. package/src/hooks/hook-loader.ts +3 -3
  150. package/src/hooks/registry.ts +50 -6
  151. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  152. package/src/inbound/oauth-callback-url.ts +61 -0
  153. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  154. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  155. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  156. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  157. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  158. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  159. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  160. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  161. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  162. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  163. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  164. package/src/live-voice/live-voice-archive.ts +2 -0
  165. package/src/live-voice/live-voice-metrics.ts +57 -32
  166. package/src/live-voice/live-voice-photo.ts +1 -2
  167. package/src/live-voice/live-voice-session.ts +535 -314
  168. package/src/live-voice/progress-narration.ts +277 -0
  169. package/src/live-voice/protocol.ts +21 -1
  170. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  171. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  172. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  173. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  174. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  175. package/src/mcp/client.ts +16 -11
  176. package/src/mcp/effective-config.ts +113 -0
  177. package/src/mcp/manager.ts +11 -6
  178. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  179. package/src/mcp/mcp-oauth-provider.ts +205 -240
  180. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  181. package/src/monitoring/plugin-auto-update.ts +128 -24
  182. package/src/notifications/signal.ts +1 -0
  183. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  184. package/src/permissions/confirmation-guardian-request.ts +2 -2
  185. package/src/permissions/question-guardian-request.test.ts +14 -6
  186. package/src/permissions/question-guardian-request.ts +1 -2
  187. package/src/persistence/attachments-store.ts +8 -1
  188. package/src/persistence/bookmark-crud.ts +3 -7
  189. package/src/persistence/conversation-attention-store.ts +16 -45
  190. package/src/persistence/conversation-crud.ts +33 -4
  191. package/src/persistence/conversation-lineage.ts +9 -0
  192. package/src/persistence/conversation-queries.ts +108 -41
  193. package/src/persistence/delivery-crud.ts +38 -29
  194. package/src/persistence/external-conversation-store.ts +32 -4
  195. package/src/persistence/llm-request-log-store.ts +4 -10
  196. package/src/persistence/llm-usage-store.ts +8 -3
  197. package/src/persistence/message-reads.test.ts +197 -0
  198. package/src/persistence/message-reads.ts +211 -0
  199. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  200. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  201. package/src/persistence/real-user-turn-filter.ts +27 -3
  202. package/src/persistence/steps.ts +9 -0
  203. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  204. package/src/plugin-api/conversation-turn.ts +168 -5
  205. package/src/plugin-api/index.ts +21 -5
  206. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  207. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  208. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  209. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  210. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  211. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  212. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  213. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  214. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  215. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  216. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  217. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  218. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  219. package/src/plugins/defaults/memory/indexer.ts +3 -1
  220. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  221. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  222. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  223. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  224. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  225. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  226. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  227. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  228. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  229. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  230. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  231. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  232. package/src/plugins/defaults/memory/worker.ts +6 -3
  233. package/src/plugins/external-plugin-loader.ts +47 -0
  234. package/src/plugins/mcp-servers.ts +361 -0
  235. package/src/plugins/mtime-cache.ts +23 -49
  236. package/src/plugins/worker-plugin-surface.ts +33 -0
  237. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  238. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  239. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  240. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  241. package/src/providers/call-site-routing.ts +30 -3
  242. package/src/providers/connection-resolution.ts +194 -11
  243. package/src/providers/inference/auth.ts +6 -0
  244. package/src/providers/inference/connection-availability.ts +24 -2
  245. package/src/providers/inference/connections.ts +2 -0
  246. package/src/providers/model-intents.ts +26 -6
  247. package/src/providers/openai/codex-models.ts +2 -1
  248. package/src/providers/provider-send-message.ts +32 -3
  249. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  250. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  251. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  252. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  253. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  254. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  255. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  256. package/src/providers/speech-to-text/resolve.ts +25 -2
  257. package/src/routes/worker.ts +17 -5
  258. package/src/runtime/access-request-helper.ts +9 -12
  259. package/src/runtime/agent-wake.ts +3 -3
  260. package/src/runtime/pre-first-message-gate.ts +4 -0
  261. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  262. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  263. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  264. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  265. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  266. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  267. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  268. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  269. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  270. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  271. package/src/runtime/routes/channel-route-shared.ts +0 -6
  272. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  273. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  274. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  275. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  276. package/src/runtime/routes/default-provider-routes.ts +15 -0
  277. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  278. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  279. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  280. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  281. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  282. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  283. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  284. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  285. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  286. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  287. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  288. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  289. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  291. package/src/runtime/routes/log-export-routes.ts +3 -0
  292. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  293. package/src/runtime/routes/plugins-routes.ts +21 -3
  294. package/src/runtime/routes/stt-routes.ts +31 -25
  295. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  296. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  297. package/src/runtime/routes/user-route-import.ts +108 -0
  298. package/src/schedule/worker.ts +6 -0
  299. package/src/security/oauth2.ts +6 -22
  300. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  301. package/src/stt/__tests__/types.test.ts +94 -0
  302. package/src/stt/daemon-batch-transcriber.ts +10 -0
  303. package/src/stt/stt-stream-session.ts +8 -4
  304. package/src/stt/types.ts +103 -0
  305. package/src/subagent/manager.ts +1 -3
  306. package/src/subagent/types.ts +7 -6
  307. package/src/tools/acp/spawn.test.ts +97 -0
  308. package/src/tools/acp/spawn.ts +32 -0
  309. package/src/tools/document/document-tool.ts +12 -3
  310. package/src/tools/registry.ts +2 -1
  311. package/src/tools/workflows/run-workflow.ts +1 -2
  312. package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
  313. package/src/tts/reasoning-tag-filter.ts +110 -0
  314. package/src/workspace/byok-default-profile-ensure.ts +61 -18
  315. package/src/workspace/custom-profile-ensure.ts +4 -24
  316. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  317. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  318. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  319. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  320. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  321. package/src/workspace/migrations/registry.ts +8 -0
  322. package/src/workspace/provider-commit-message-generator.ts +7 -5
  323. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  324. package/src/live-voice/front-decision.ts +0 -476
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Reuse and invalidation of the stored dynamic client registration.
3
+ *
4
+ * Dynamic registration writes a record on the authorization server, so
5
+ * registering per attempt leaves records behind that nobody cleans up. The
6
+ * stored registration is therefore reused, and only withheld when it
7
+ * provably no longer applies. Both halves matter: reusing one bound to a
8
+ * stale redirect URI produces authorization requests the server rejects,
9
+ * and re-registering unnecessarily is the accumulation this exists to stop.
10
+ */
11
+
12
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
13
+
14
+ const CALLBACK_URL =
15
+ "https://platform.example/v1/gateway/callbacks/abc/webhooks/oauth/callback/";
16
+
17
+ let resolvedCallbackUrl = CALLBACK_URL;
18
+
19
+ mock.module("../../inbound/platform-callback-registration.js", () => ({
20
+ resolveCallbackUrl: async () => resolvedCallbackUrl,
21
+ }));
22
+
23
+ mock.module("../../inbound/public-ingress-urls.js", () => ({
24
+ getOAuthCallbackUrl: () => resolvedCallbackUrl,
25
+ }));
26
+
27
+ mock.module("../../config/loader.js", () => ({
28
+ loadConfig: () => ({}),
29
+ }));
30
+
31
+ /** In-memory stand-in for the credential store. */
32
+ const store = new Map<string, string>();
33
+
34
+ mock.module("../../security/secure-keys.js", () => ({
35
+ getSecureKeyAsync: async (k: string) => store.get(k) ?? null,
36
+ setSecureKeyAsync: async (k: string, v: string) => {
37
+ store.set(k, v);
38
+ return true;
39
+ },
40
+ deleteSecureKeyAsync: async (k: string) =>
41
+ store.delete(k) ? "deleted" : "not-found",
42
+ }));
43
+
44
+ mock.module("../../daemon/identity-helpers.js", () => ({
45
+ getAssistantName: () => "Jarvis",
46
+ }));
47
+
48
+ mock.module("../../config/env.js", () => ({
49
+ getPlatformAssistantId: () => "019d6d4f-6dbd-779f-91d3-cb273b9429a5",
50
+ }));
51
+
52
+ const { McpOAuthProvider } = await import("../mcp-oauth-provider.js");
53
+
54
+ const REGISTRATION = { client_id: "generated-client-id" };
55
+
56
+ function newProvider() {
57
+ return new McpOAuthProvider(
58
+ "unabyss",
59
+ "https://mcp.unabyss.com",
60
+ /* interactive */ false,
61
+ );
62
+ }
63
+
64
+ /** Seed discovery state the way the SDK would after running discovery. */
65
+ async function seedDiscovery(
66
+ provider: InstanceType<typeof McpOAuthProvider>,
67
+ issuer: string,
68
+ ): Promise<void> {
69
+ await provider.saveDiscoveryState({
70
+ authorizationServerUrl: issuer,
71
+ authorizationServerMetadata: { issuer },
72
+ } as never);
73
+ }
74
+
75
+ /** Run one registration the way a completed flow would. */
76
+ async function register(issuer = "https://mcp.unabyss.com"): Promise<void> {
77
+ const provider = newProvider();
78
+ await provider.startCallbackServer();
79
+ await seedDiscovery(provider, issuer);
80
+ await provider.saveClientInformation(REGISTRATION as never);
81
+ provider.stopCallbackServer();
82
+ }
83
+
84
+ beforeEach(() => {
85
+ store.clear();
86
+ resolvedCallbackUrl = CALLBACK_URL;
87
+ });
88
+
89
+ describe("McpOAuthProvider client registration reuse", () => {
90
+ test("a stored registration is reused on the next flow", async () => {
91
+ await register();
92
+
93
+ const next = newProvider();
94
+ await next.startCallbackServer();
95
+ await seedDiscovery(next, "https://mcp.unabyss.com");
96
+
97
+ // Returning the stored value is what keeps the server from accruing a
98
+ // fresh client record on every `assistant mcp auth`.
99
+ expect(await next.clientInformation()).toEqual(REGISTRATION as never);
100
+ });
101
+
102
+ test("no stored registration means the SDK registers", async () => {
103
+ const provider = newProvider();
104
+ await provider.startCallbackServer();
105
+ expect(await provider.clientInformation()).toBeUndefined();
106
+ });
107
+
108
+ test("a changed redirect URI withholds the registration", async () => {
109
+ await register();
110
+
111
+ // The assistant moved, so its callback URL moved with it. The stored
112
+ // client is registered for the old one and the server would reject it.
113
+ resolvedCallbackUrl = "https://other.example/webhooks/oauth/callback";
114
+ const next = newProvider();
115
+ await next.startCallbackServer();
116
+ await seedDiscovery(next, "https://mcp.unabyss.com");
117
+
118
+ expect(await next.clientInformation()).toBeUndefined();
119
+ });
120
+
121
+ test("a changed authorization server withholds the registration", async () => {
122
+ await register("https://auth.unabyss.com");
123
+
124
+ const next = newProvider();
125
+ await next.startCallbackServer();
126
+ await seedDiscovery(next, "https://different-auth.example");
127
+
128
+ expect(await next.clientInformation()).toBeUndefined();
129
+ });
130
+
131
+ test("a silent reconnect reuses the registration without a redirect URI", async () => {
132
+ await register();
133
+
134
+ // `McpClient.connect` never prepares a callback, so `_redirectUrl` is
135
+ // unset. Treating that as a mismatch would turn every background
136
+ // reconnect into a registration.
137
+ const silent = newProvider();
138
+ await seedDiscovery(silent, "https://mcp.unabyss.com");
139
+
140
+ expect(await silent.clientInformation()).toEqual(REGISTRATION as never);
141
+ });
142
+
143
+ test("a registration stored before bindings existed is kept", async () => {
144
+ // Upgrading must not invalidate every existing registration at once.
145
+ store.set(
146
+ "mcp:unabyss:client_info",
147
+ JSON.stringify({ client_id: "legacy" }),
148
+ );
149
+
150
+ const provider = newProvider();
151
+ await provider.startCallbackServer();
152
+ await seedDiscovery(provider, "https://mcp.unabyss.com");
153
+
154
+ expect(await provider.clientInformation()).toEqual({
155
+ client_id: "legacy",
156
+ } as never);
157
+ });
158
+
159
+ test("invalidating the client drops its binding too", async () => {
160
+ await register();
161
+ expect(store.has("mcp:unabyss:client_binding")).toBe(true);
162
+
163
+ const provider = newProvider();
164
+ await provider.invalidateCredentials("client");
165
+
166
+ expect(store.has("mcp:unabyss:client_info")).toBe(false);
167
+ // A surviving binding would let the next registration inherit the
168
+ // previous one's issuer and redirect URI.
169
+ expect(store.has("mcp:unabyss:client_binding")).toBe(false);
170
+ });
171
+ });
172
+
173
+ describe("McpOAuthProvider client metadata", () => {
174
+ test("registers under the assistant's own name", async () => {
175
+ const provider = newProvider();
176
+ expect(provider.clientMetadata.client_name).toEqual("Jarvis");
177
+ });
178
+
179
+ test("carries the assistant id as software_id", async () => {
180
+ const provider = newProvider();
181
+ expect(provider.clientMetadata.software_id).toEqual(
182
+ "019d6d4f-6dbd-779f-91d3-cb273b9429a5",
183
+ );
184
+ });
185
+
186
+ test("presents an anonymously fetchable logo", async () => {
187
+ // The consent screen loads this without credentials, so it cannot be
188
+ // the assistant's own avatar, which is served behind authentication.
189
+ const provider = newProvider();
190
+ expect(provider.clientMetadata.logo_uri).toEqual(
191
+ "https://www.vellum.ai/favicon.svg",
192
+ );
193
+ });
194
+
195
+ test("registers the resolved callback URL as the redirect URI", async () => {
196
+ const provider = newProvider();
197
+ await provider.startCallbackServer();
198
+ expect(provider.clientMetadata.redirect_uris).toEqual([CALLBACK_URL]);
199
+ });
200
+ });
@@ -16,22 +16,24 @@ mock.module("../../inbound/public-ingress-urls.js", () => ({
16
16
 
17
17
  mock.module("../../config/loader.js", () => ({
18
18
  loadConfig: () => ({}),
19
+ // `clientMetadata` reads the assistant's name, which pulls the persona
20
+ // resolver into the provider's import graph, and that reads config.
21
+ getConfig: () => ({}),
19
22
  }));
20
23
 
21
24
  // ── Import SUT after mocks ────────────────────────────────────────────────────
22
25
 
23
26
  const { McpOAuthProvider } = await import("../mcp-oauth-provider.js");
24
27
 
25
- function newGatewayProvider() {
28
+ function newProvider() {
26
29
  return new McpOAuthProvider(
27
30
  "comms",
28
31
  "https://comms.example/mcp",
29
32
  /* interactive */ false,
30
- "gateway",
31
33
  );
32
34
  }
33
35
 
34
- describe("McpOAuthProvider gateway callback", () => {
36
+ describe("McpOAuthProvider callback", () => {
35
37
  test("stopCallbackServer() before a consumer attaches does not emit an unhandled rejection", async () => {
36
38
  // stopCallbackServer() rejects the deferred code promise. When no consumer
37
39
  // is attached, that rejection must stay observed so it does not surface as
@@ -40,7 +42,7 @@ describe("McpOAuthProvider gateway callback", () => {
40
42
  const onUnhandled = (reason: unknown) => unhandled.push(reason);
41
43
  process.on("unhandledRejection", onUnhandled);
42
44
  try {
43
- const provider = newGatewayProvider();
45
+ const provider = newProvider();
44
46
 
45
47
  // Intentionally do NOT consume the returned codePromise — this mirrors the
46
48
  // early-exit paths where connect() throws before the OAuth tail is wired up.
@@ -53,7 +55,7 @@ describe("McpOAuthProvider gateway callback", () => {
53
55
  const cancelRejections = unhandled.filter(
54
56
  (r) =>
55
57
  r instanceof Error &&
56
- r.message.includes("MCP OAuth gateway callback cancelled"),
58
+ r.message.includes("MCP OAuth callback cancelled"),
57
59
  );
58
60
  expect(cancelRejections).toHaveLength(0);
59
61
  } finally {
@@ -62,14 +64,12 @@ describe("McpOAuthProvider gateway callback", () => {
62
64
  });
63
65
 
64
66
  test("stopCallbackServer() still rejects a consumer that is awaiting the code", async () => {
65
- const provider = newGatewayProvider();
67
+ const provider = newProvider();
66
68
  await provider.startCallbackServer();
67
69
 
68
70
  const codePromise = provider.waitForCode();
69
71
  provider.stopCallbackServer();
70
72
 
71
- await expect(codePromise).rejects.toThrow(
72
- "MCP OAuth gateway callback cancelled",
73
- );
73
+ await expect(codePromise).rejects.toThrow("MCP OAuth callback cancelled");
74
74
  });
75
75
  });
@@ -0,0 +1,95 @@
1
+ /**
2
+ * A plugin-declared server must connect without resolving
3
+ * `mcp:<serverId>:*` from the workspace credential store.
4
+ *
5
+ * A plugin controls both its server key and its URL, so a stored
6
+ * credential whose id happens to match would otherwise be sent to an
7
+ * endpoint the plugin chose. The listing route avoids this by never
8
+ * probing a plugin server; the connect path avoids it by reading the
9
+ * server's own `source`, so the rule cannot be lost by a caller that
10
+ * forgets to pass something.
11
+ */
12
+
13
+ import { beforeEach, describe, expect, jest, mock, test } from "bun:test";
14
+
15
+ const getSecureKeyAsync = jest.fn(async (_key: string) => "stored-oauth-token");
16
+ const getMcpHeaders = jest.fn(async (_serverId: string) => ({
17
+ Authorization: "Bearer workspace-secret",
18
+ }));
19
+
20
+ mock.module("../../security/secure-keys.js", () => ({
21
+ getSecureKeyAsync,
22
+ setSecureKeyAsync: jest.fn(async () => true),
23
+ deleteSecureKeyAsync: jest.fn(async () => "deleted"),
24
+ }));
25
+
26
+ mock.module("../mcp-header-store.js", () => ({
27
+ getMcpHeaders,
28
+ setMcpHeaders: jest.fn(async () => {}),
29
+ deleteMcpHeaders: jest.fn(async () => true),
30
+ }));
31
+
32
+ // `McpOAuthProvider` is left real: its constructor only assigns fields, and
33
+ // mocking the module here would bleed into the sibling provider tests.
34
+ const { McpServerManager } = await import("../manager.js");
35
+
36
+ /** Refused immediately, so `connect` fails after the credential lookups. */
37
+ const UNREACHABLE = "http://127.0.0.1:1/mcp";
38
+
39
+ function httpServer(source: "workspace" | "plugin") {
40
+ return {
41
+ transport: { type: "streamable-http" as const, url: UNREACHABLE },
42
+ enabled: true,
43
+ defaultRiskLevel: "low" as const,
44
+ maxTools: 20,
45
+ source,
46
+ };
47
+ }
48
+
49
+ describe("plugin-declared MCP servers", () => {
50
+ beforeEach(() => {
51
+ getSecureKeyAsync.mockClear();
52
+ getMcpHeaders.mockClear();
53
+ });
54
+
55
+ test("connect without resolving workspace credentials", async () => {
56
+ const manager = new McpServerManager();
57
+
58
+ await manager.start({
59
+ servers: { unabyss: httpServer("plugin") },
60
+ globalMaxTools: 50,
61
+ });
62
+
63
+ expect(getMcpHeaders).not.toHaveBeenCalled();
64
+ expect(getSecureKeyAsync).not.toHaveBeenCalled();
65
+ });
66
+
67
+ test("workspace servers keep resolving theirs", async () => {
68
+ const manager = new McpServerManager();
69
+
70
+ await manager.start({
71
+ servers: { "from-workspace": httpServer("workspace") },
72
+ globalMaxTools: 50,
73
+ });
74
+
75
+ expect(getMcpHeaders).toHaveBeenCalledWith("from-workspace");
76
+ expect(getSecureKeyAsync).toHaveBeenCalledWith("mcp:from-workspace:tokens");
77
+ });
78
+
79
+ test("isolation is per-server, not per-start", async () => {
80
+ // Both kinds start together in one call — the plugin one must not
81
+ // widen credential access for the workspace one, or vice versa.
82
+ const manager = new McpServerManager();
83
+
84
+ await manager.start({
85
+ servers: {
86
+ unabyss: httpServer("plugin"),
87
+ "from-workspace": httpServer("workspace"),
88
+ },
89
+ globalMaxTools: 50,
90
+ });
91
+
92
+ const lookedUpIds = getMcpHeaders.mock.calls.map(([id]) => id);
93
+ expect(lookedUpIds).toEqual(["from-workspace"]);
94
+ });
95
+ });
package/src/mcp/client.ts CHANGED
@@ -5,8 +5,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
5
5
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
6
  import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
7
7
 
8
- import { getIsPlatform } from "../config/env-registry.js";
9
- import type { McpTransport } from "../config/schemas/mcp.js";
8
+ import type { McpServerSource, McpTransport } from "../config/schemas/mcp.js";
10
9
  import { getSecureKeyAsync } from "../security/secure-keys.js";
11
10
  import { getLogger } from "../util/logger.js";
12
11
  import { getMcpHeaders } from "./mcp-header-store.js";
@@ -55,6 +54,12 @@ export interface McpCallResult {
55
54
 
56
55
  export class McpClient {
57
56
  readonly serverId: string;
57
+ /**
58
+ * Where this server was declared. Only a `workspace` server resolves
59
+ * `mcp:<serverId>:tokens` / `mcp:<serverId>:headers` from the credential
60
+ * store — see {@link McpServerSource} for why a plugin server must not.
61
+ */
62
+ readonly source: McpServerSource;
58
63
  private client: Client;
59
64
  private transport:
60
65
  | StdioClientTransport
@@ -74,8 +79,9 @@ export class McpClient {
74
79
  return this.connected;
75
80
  }
76
81
 
77
- constructor(serverId: string) {
82
+ constructor(serverId: string, source: McpServerSource = "workspace") {
78
83
  this.serverId = serverId;
84
+ this.source = source;
79
85
  this.client = new Client({
80
86
  name: "vellum-assistant",
81
87
  version: "1.0.0",
@@ -100,22 +106,21 @@ export class McpClient {
100
106
  const isHttpTransport =
101
107
  transportConfig.type === "sse" ||
102
108
  transportConfig.type === "streamable-http";
109
+ const usesStoredCredentials = this.source === "workspace";
103
110
 
104
- // For HTTP transports, only attach an OAuth provider if cached tokens exist.
105
- // This avoids triggering client registration (which binds to a random port)
106
- // during daemon startup. If no tokens, try without auth — if the server
107
- // requires it, skip silently.
108
- if (isHttpTransport) {
111
+ // For HTTP transports, only attach an OAuth provider if cached tokens
112
+ // exist. This avoids triggering client registration during daemon
113
+ // startup. If no tokens, try without auth: if the server requires it,
114
+ // skip silently.
115
+ if (isHttpTransport && usesStoredCredentials) {
109
116
  const cachedTokens = await getSecureKeyAsync(
110
117
  `mcp:${this.serverId}:tokens`,
111
118
  );
112
119
  if (cachedTokens) {
113
- const callbackTransport = getIsPlatform() ? "gateway" : "loopback";
114
120
  this.oauthProvider = new McpOAuthProvider(
115
121
  this.serverId,
116
122
  transportConfig.url,
117
123
  /* interactive */ false,
118
- callbackTransport,
119
124
  );
120
125
  }
121
126
  }
@@ -123,7 +128,7 @@ export class McpClient {
123
128
  // Resolve static auth headers from credential store, falling back to
124
129
  // any legacy headers in the transport config for backward compatibility.
125
130
  let effectiveConfig = transportConfig;
126
- if (isHttpTransport) {
131
+ if (isHttpTransport && usesStoredCredentials) {
127
132
  const storedHeaders = await getMcpHeaders(this.serverId);
128
133
  if (storedHeaders) {
129
134
  effectiveConfig = {
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The MCP configuration the daemon actually runs.
3
+ *
4
+ * Two sources contribute servers: the workspace `config.json`, which the
5
+ * user owns, and each installed plugin's `mcp.json`, which its author
6
+ * owns. Both must reach the MCP manager, so every consumer that starts
7
+ * servers builds its config through here rather than reading
8
+ * `config.mcp` directly — a path that only reads the workspace half
9
+ * leaves a plugin's tools silently missing.
10
+ *
11
+ * Every server comes out attributed with its {@link McpServerSource}, so
12
+ * the one place origin still matters downstream — a plugin server must
13
+ * never resolve `mcp:<serverId>:*` from the credential store — reads it
14
+ * off the server rather than from a caller-supplied list.
15
+ */
16
+
17
+ import {
18
+ type McpConfig,
19
+ McpConfigSchema,
20
+ type ResolvedMcpConfig,
21
+ type ResolvedMcpServerConfig,
22
+ } from "../config/schemas/mcp.js";
23
+ import { readPluginMcpServers } from "../plugins/mcp-servers.js";
24
+ import { getLogger } from "../util/logger.js";
25
+
26
+ const log = getLogger("mcp-effective-config");
27
+
28
+ /**
29
+ * Fingerprint of the plugin-declared servers in the config last built here.
30
+ * Null until the first build, which happens during daemon startup.
31
+ */
32
+ let lastBuiltPluginFingerprint: string | null = null;
33
+
34
+ /**
35
+ * Merge the workspace MCP config with the servers plugins declare.
36
+ *
37
+ * A workspace entry outranks a plugin's declaration of the same id: the
38
+ * user's own configuration is the more specific statement, and it is the
39
+ * one they can edit. `internal_mcp_list` applies the same precedence, so
40
+ * what the listing shows is what the daemon runs.
41
+ */
42
+ export function buildEffectiveMcpConfig(
43
+ workspaceConfig?: McpConfig,
44
+ ): ResolvedMcpConfig {
45
+ // An absent `mcp` key still has to yield the schema's own defaults
46
+ // (`globalMaxTools`), since plugin servers alone are enough to need them.
47
+ const base = workspaceConfig ?? McpConfigSchema.parse({});
48
+ const servers: Record<string, ResolvedMcpServerConfig> = {};
49
+ for (const [id, config] of Object.entries(base.servers)) {
50
+ servers[id] = { ...config, source: "workspace" };
51
+ }
52
+
53
+ const { servers: pluginServers, issues } = readPluginMcpServers();
54
+ for (const issue of issues) {
55
+ log.warn(
56
+ {
57
+ plugin: issue.pluginName,
58
+ ...(issue.serverKey && { serverKey: issue.serverKey }),
59
+ },
60
+ `Plugin MCP declaration problem: ${issue.message}`,
61
+ );
62
+ }
63
+
64
+ for (const server of pluginServers) {
65
+ if (Object.hasOwn(servers, server.id)) {
66
+ log.warn(
67
+ { plugin: server.pluginName, serverId: server.id },
68
+ "Plugin MCP server shadowed by a workspace server of the same id; skipping",
69
+ );
70
+ continue;
71
+ }
72
+ servers[server.id] = server.config;
73
+ }
74
+
75
+ lastBuiltPluginFingerprint = fingerprintPluginServers();
76
+
77
+ return { ...base, servers };
78
+ }
79
+
80
+ /**
81
+ * Whether the plugin-declared servers on disk differ from the ones in the
82
+ * config last built by {@link buildEffectiveMcpConfig}.
83
+ *
84
+ * The plugin source reconcile asks this after an install / uninstall /
85
+ * upgrade / enable / disable, so an MCP reload happens exactly when a
86
+ * plugin actually changed the server set — not on every unrelated plugin
87
+ * edit, which would tear down healthy workspace connections for nothing.
88
+ *
89
+ * True before the first build, since nothing has been applied yet.
90
+ */
91
+ export function pluginMcpServersChangedSinceLastBuild(): boolean {
92
+ return fingerprintPluginServers() !== lastBuiltPluginFingerprint;
93
+ }
94
+
95
+ /**
96
+ * Stable digest of every plugin-declared server: its id and the config it
97
+ * resolves to. Covers a server appearing, disappearing, or changing its
98
+ * transport — a disabled or uninstalled plugin contributes nothing, so it
99
+ * drops out of the digest the same way a deleted `mcp.json` entry does.
100
+ */
101
+ function fingerprintPluginServers(): string {
102
+ const { servers } = readPluginMcpServers();
103
+ return JSON.stringify(
104
+ [...servers]
105
+ .sort((a, b) => a.id.localeCompare(b.id))
106
+ .map((s) => [s.id, s.config]),
107
+ );
108
+ }
109
+
110
+ /** Test seam: forget the last built config, as on a fresh daemon. */
111
+ export function resetEffectiveMcpConfigForTests(): void {
112
+ lastBuiltPluginFingerprint = null;
113
+ }
@@ -1,4 +1,7 @@
1
- import type { McpConfig, McpServerConfig } from "../config/schemas/mcp.js";
1
+ import type {
2
+ ResolvedMcpConfig,
3
+ ResolvedMcpServerConfig,
4
+ } from "../config/schemas/mcp.js";
2
5
  import { getLogger } from "../util/logger.js";
3
6
  import { McpClient, type McpToolInfo } from "./client.js";
4
7
 
@@ -6,15 +9,15 @@ const log = getLogger("mcp-manager");
6
9
 
7
10
  export interface McpServerToolInfo {
8
11
  serverId: string;
9
- serverConfig: McpServerConfig;
12
+ serverConfig: ResolvedMcpServerConfig;
10
13
  tools: McpToolInfo[];
11
14
  }
12
15
 
13
16
  export class McpServerManager {
14
17
  private clients = new Map<string, McpClient>();
15
- private serverConfigs = new Map<string, McpServerConfig>();
18
+ private serverConfigs = new Map<string, ResolvedMcpServerConfig>();
16
19
 
17
- async start(config: McpConfig): Promise<McpServerToolInfo[]> {
20
+ async start(config: ResolvedMcpConfig): Promise<McpServerToolInfo[]> {
18
21
  const results: McpServerToolInfo[] = [];
19
22
 
20
23
  console.log(
@@ -40,7 +43,9 @@ export class McpServerManager {
40
43
  "HTTP transport — OAuth provider will be available if server requires authentication",
41
44
  );
42
45
  }
43
- const client = new McpClient(serverId);
46
+ // The server's own origin decides whether it may resolve
47
+ // `mcp:<serverId>:*` from the credential store.
48
+ const client = new McpClient(serverId, serverConfig.source);
44
49
  await client.connect(serverConfig.transport);
45
50
 
46
51
  if (!client.isConnected) {
@@ -142,7 +147,7 @@ export class McpServerManager {
142
147
 
143
148
  private filterTools(
144
149
  tools: McpToolInfo[],
145
- config: McpServerConfig,
150
+ config: ResolvedMcpServerConfig,
146
151
  ): McpToolInfo[] {
147
152
  let filtered = tools;
148
153
 
@@ -13,7 +13,6 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
13
13
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
14
14
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
15
15
 
16
- import { getIsContainerized } from "../config/env-registry.js";
17
16
  import { reloadMcpServers } from "../daemon/mcp-reload-service.js";
18
17
  import { getLogger } from "../util/logger.js";
19
18
  import {
@@ -22,10 +21,7 @@ import {
22
21
  setMcpAuthPending,
23
22
  } from "./mcp-auth-state.js";
24
23
  import { getMcpHeaders } from "./mcp-header-store.js";
25
- import {
26
- type McpOAuthCallbackTransport,
27
- McpOAuthProvider,
28
- } from "./mcp-oauth-provider.js";
24
+ import { McpOAuthProvider } from "./mcp-oauth-provider.js";
29
25
 
30
26
  const log = getLogger("mcp-auth-orchestrator");
31
27
 
@@ -53,20 +49,11 @@ export async function orchestrateMcpOAuthConnect(args: {
53
49
  }): Promise<OrchestrateMcpOAuthConnectResult> {
54
50
  const { serverId, transport } = args;
55
51
 
56
- // Containerized deployments (platform-managed AND self-hosted Docker) must
57
- // use the gateway transport: the browser is outside the daemon container,
58
- // so a loopback callback inside the container is unreachable. Bare-metal
59
- // daemons can use loopback as before.
60
- const callbackTransport: McpOAuthCallbackTransport = getIsContainerized()
61
- ? "gateway"
62
- : "loopback";
63
-
64
52
  let capturedAuthUrl: string | undefined;
65
53
  const provider = new McpOAuthProvider(
66
54
  serverId,
67
55
  transport.url,
68
56
  /* interactive */ false,
69
- callbackTransport,
70
57
  {
71
58
  onAuthorizationUrl: (url) => {
72
59
  capturedAuthUrl = url;
@@ -74,8 +61,11 @@ export async function orchestrateMcpOAuthConnect(args: {
74
61
  },
75
62
  );
76
63
 
77
- // Clear stale credentials so the flow starts fresh
78
- await provider.invalidateCredentials("client");
64
+ // Re-discover, but keep the client registration. Discovery is a read and
65
+ // costs a request; registration is a write on the authorization server,
66
+ // and remaking it per attempt leaves a record behind every time. The
67
+ // provider drops a stored registration on its own when the redirect URI
68
+ // or the authorization server it was made against has changed.
79
69
  await provider.invalidateCredentials("discovery");
80
70
 
81
71
  // Register the pending callback in the daemon heap
@@ -146,10 +136,9 @@ export async function orchestrateMcpOAuthConnect(args: {
146
136
  // so the daemon can reconnect on the next MCP reload without re-connecting here.
147
137
  void (async () => {
148
138
  try {
149
- // Apply an explicit timeout: the loopback callback path has a built-in
150
- // 2-minute timer, but the gateway transport's deferred promise relies on
151
- // the caller for time-boxing. Without this race, gateway-mode tails leak
152
- // forever if the user never completes the OAuth handshake.
139
+ // Apply an explicit timeout: the deferred code promise relies on the
140
+ // caller for time-boxing. Without this race, a tail leaks forever if
141
+ // the user never completes the OAuth handshake.
153
142
  const code = await Promise.race([
154
143
  codePromise,
155
144
  new Promise<never>((_, reject) => {
@@ -215,6 +204,8 @@ export async function orchestrateMcpOAuthConnect(args: {
215
204
  return { auth_url: capturedAuthUrl };
216
205
  }
217
206
 
218
- // Matches the loopback callback timeout; keep both in lockstep so loopback
219
- // and gateway transports time-box OAuth identically.
207
+ // How long a user has to complete the browser handshake before the tail
208
+ // gives up. Shorter than the callback registry's own 5-minute TTL, so the
209
+ // failure surfaces here with a clear message rather than as a registry
210
+ // expiry the caller cannot attribute.
220
211
  const CALLBACK_TIMEOUT_MS = 2 * 60 * 1000;