@vellumai/assistant 0.7.1 → 0.7.2

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 (535) hide show
  1. package/ARCHITECTURE.md +32 -49
  2. package/Dockerfile +1 -0
  3. package/README.md +1 -2
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +9 -3
  5. package/bun.lock +26 -26
  6. package/docs/architecture/security.md +20 -0
  7. package/docs/plugins.md +7 -9
  8. package/knip.json +1 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
  10. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +39 -1
  11. package/node_modules/@vellumai/gateway-client/src/types.ts +11 -0
  12. package/node_modules/@vellumai/service-contracts/package.json +2 -0
  13. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
  15. package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
  16. package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
  17. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
  18. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +9 -0
  19. package/node_modules/@vellumai/twilio-client/bun.lock +24 -0
  20. package/node_modules/@vellumai/twilio-client/package.json +18 -0
  21. package/node_modules/@vellumai/twilio-client/src/__tests__/twilio-client.test.ts +128 -0
  22. package/node_modules/@vellumai/twilio-client/src/index.ts +179 -0
  23. package/node_modules/@vellumai/twilio-client/tsconfig.json +20 -0
  24. package/openapi.yaml +565 -12
  25. package/package.json +6 -3
  26. package/src/__tests__/app-builder-tool-scripts.test.ts +3 -3
  27. package/src/__tests__/app-bundler.test.ts +170 -1
  28. package/src/__tests__/app-control-flow.test.ts +374 -0
  29. package/src/__tests__/app-control-no-global-cgevent.test.ts +98 -0
  30. package/src/__tests__/app-control-tool-schemas.test.ts +621 -0
  31. package/src/__tests__/app-executors.test.ts +30 -43
  32. package/src/__tests__/approval-routes-http.test.ts +23 -6
  33. package/src/__tests__/assistant-event-hub-machine-name.test.ts +146 -0
  34. package/src/__tests__/assistant-event-hub-targeted.test.ts +257 -0
  35. package/src/__tests__/assistant-event-hub.test.ts +109 -2
  36. package/src/__tests__/assistant-event.test.ts +10 -0
  37. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -2
  38. package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -7
  39. package/src/__tests__/background-shell-host-bash.test.ts +14 -15
  40. package/src/__tests__/bootstrap-turn-cleanup.test.ts +44 -0
  41. package/src/__tests__/btw-routes.test.ts +13 -4
  42. package/src/__tests__/call-controller.test.ts +49 -1
  43. package/src/__tests__/call-domain.test.ts +0 -2
  44. package/src/__tests__/call-routes-http.test.ts +0 -2
  45. package/src/__tests__/channel-readiness-service.test.ts +59 -1
  46. package/src/__tests__/checker.test.ts +3 -4
  47. package/src/__tests__/config-loader-backfill.test.ts +90 -155
  48. package/src/__tests__/config-loader-platform-defaults.test.ts +196 -0
  49. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  50. package/src/__tests__/config-set-platform-guard.test.ts +48 -4
  51. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +2 -2
  52. package/src/__tests__/config-watcher.test.ts +2 -2
  53. package/src/__tests__/conversation-app-control-instantiation.test.ts +392 -0
  54. package/src/__tests__/conversation-app-control-lifecycle.test.ts +237 -0
  55. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  56. package/src/__tests__/conversation-lifecycle.test.ts +36 -0
  57. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +283 -0
  58. package/src/__tests__/conversation-routes-disk-view.test.ts +6 -0
  59. package/src/__tests__/conversation-routes-guardian-reply.test.ts +120 -72
  60. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  61. package/src/__tests__/conversation-slash-commands.test.ts +0 -4
  62. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
  63. package/src/__tests__/conversation-surfaces-app-control.test.ts +317 -0
  64. package/src/__tests__/credential-execution-feature-gates.test.ts +5 -12
  65. package/src/__tests__/credential-execution-managed-contract.test.ts +3 -131
  66. package/src/__tests__/credentials-cli.test.ts +5 -12
  67. package/src/__tests__/cu-unified-flow.test.ts +185 -23
  68. package/src/__tests__/daemon-credential-client.test.ts +101 -19
  69. package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
  70. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  71. package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
  72. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -2
  73. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -2
  74. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -1
  75. package/src/__tests__/heartbeat-service.test.ts +718 -1
  76. package/src/__tests__/helpers/call-route-handler.ts +7 -1
  77. package/src/__tests__/host-app-control-proxy.test.ts +602 -0
  78. package/src/__tests__/host-app-control-routes.test.ts +263 -0
  79. package/src/__tests__/host-bash-proxy.test.ts +246 -47
  80. package/src/__tests__/host-bash-routes.test.ts +294 -0
  81. package/src/__tests__/host-browser-proxy.test.ts +24 -22
  82. package/src/__tests__/host-browser-routes.test.ts +39 -13
  83. package/src/__tests__/host-cu-proxy.test.ts +41 -52
  84. package/src/__tests__/host-cu-routes-targeted.test.ts +300 -0
  85. package/src/__tests__/host-file-edit-tool.test.ts +47 -1
  86. package/src/__tests__/host-file-proxy-targeted.test.ts +339 -0
  87. package/src/__tests__/host-file-proxy.test.ts +37 -43
  88. package/src/__tests__/host-file-read-tool.test.ts +17 -0
  89. package/src/__tests__/host-file-routes-targeted.test.ts +262 -0
  90. package/src/__tests__/host-file-write-tool.test.ts +42 -1
  91. package/src/__tests__/host-proxy-base.test.ts +312 -0
  92. package/src/__tests__/host-shell-tool.test.ts +22 -4
  93. package/src/__tests__/host-transfer-proxy-targeted.test.ts +583 -0
  94. package/src/__tests__/host-transfer-proxy.test.ts +121 -22
  95. package/src/__tests__/host-transfer-routes-targeted.test.ts +447 -0
  96. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  97. package/src/__tests__/identity-intro-cache.test.ts +29 -0
  98. package/src/__tests__/identity-routes.test.ts +103 -1
  99. package/src/__tests__/init-feature-flag-overrides.test.ts +26 -3
  100. package/src/__tests__/inline-command-runner.test.ts +0 -1
  101. package/src/__tests__/inline-skill-load-permissions.test.ts +5 -11
  102. package/src/__tests__/integration-status.test.ts +85 -5
  103. package/src/__tests__/intent-routing.test.ts +0 -1
  104. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +95 -5
  105. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +17 -0
  106. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
  107. package/src/__tests__/mcp-auth-routes.test.ts +197 -0
  108. package/src/__tests__/mcp-cli.test.ts +338 -2
  109. package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
  110. package/src/__tests__/migration-import-commit-http.test.ts +108 -2
  111. package/src/__tests__/mock-gateway-ipc.ts +1 -0
  112. package/src/__tests__/oauth-cli.test.ts +0 -2
  113. package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
  114. package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
  115. package/src/__tests__/platform-bash-auto-approve.test.ts +5 -9
  116. package/src/__tests__/prechat-onboarding-contract.test.ts +3 -1
  117. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  118. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  119. package/src/__tests__/public-ingress-urls.test.ts +97 -0
  120. package/src/__tests__/require-fresh-approval.test.ts +0 -1
  121. package/src/__tests__/retry-backoff.test.ts +87 -0
  122. package/src/__tests__/runtime-events-sse.test.ts +10 -6
  123. package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
  124. package/src/__tests__/schedule-retry.test.ts +715 -0
  125. package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
  126. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  127. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  128. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
  129. package/src/__tests__/skill-feature-flags.test.ts +43 -41
  130. package/src/__tests__/skill-load-feature-flag.test.ts +13 -14
  131. package/src/__tests__/skill-load-inline-command.test.ts +0 -51
  132. package/src/__tests__/skill-load-inline-includes.test.ts +0 -43
  133. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  134. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
  135. package/src/__tests__/slack-channel-config.test.ts +9 -14
  136. package/src/__tests__/system-prompt-ask-mode.test.ts +0 -1
  137. package/src/__tests__/system-prompt.test.ts +0 -1
  138. package/src/__tests__/telegram-config.test.ts +0 -1
  139. package/src/__tests__/test-preload.ts +8 -0
  140. package/src/__tests__/tool-approval-handler.test.ts +3 -4
  141. package/src/__tests__/tool-audit-listener.test.ts +48 -0
  142. package/src/__tests__/tool-execute-pipeline.test.ts +0 -1
  143. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  145. package/src/__tests__/tool-executor.test.ts +0 -1
  146. package/src/__tests__/twilio-config.test.ts +3 -16
  147. package/src/__tests__/twilio-routes.test.ts +3 -5
  148. package/src/__tests__/twilio-validation.test.ts +93 -0
  149. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -4
  150. package/src/__tests__/verification-control-plane-policy.test.ts +2 -4
  151. package/src/__tests__/voice-ingress-preflight.test.ts +19 -0
  152. package/src/__tests__/workspace-migration-006-services-config.test.ts +3 -2
  153. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -5
  154. package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
  155. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
  156. package/src/backup/__tests__/paths.test.ts +0 -22
  157. package/src/backup/__tests__/restore.test.ts +51 -151
  158. package/src/backup/paths.ts +2 -18
  159. package/src/backup/restore.ts +107 -231
  160. package/src/bundler/app-bundler.ts +51 -3
  161. package/src/calls/relay-server.ts +4 -44
  162. package/src/calls/twilio-config.ts +2 -17
  163. package/src/calls/twilio-rest.ts +33 -105
  164. package/src/calls/twilio-routes.ts +11 -12
  165. package/src/channels/types.ts +8 -7
  166. package/src/cli/commands/__tests__/backup.test.ts +6 -277
  167. package/src/cli/commands/__tests__/gateway.test.ts +288 -0
  168. package/src/cli/commands/__tests__/memory-v2.test.ts +4 -0
  169. package/src/cli/commands/__tests__/webhooks.test.ts +0 -1
  170. package/src/cli/commands/backup.ts +6 -331
  171. package/src/cli/commands/clients.ts +36 -37
  172. package/src/cli/commands/contacts.ts +73 -0
  173. package/src/cli/commands/conversations.ts +2 -5
  174. package/src/cli/commands/credentials.ts +15 -7
  175. package/src/cli/commands/domain.ts +66 -15
  176. package/src/cli/commands/gateway.ts +183 -0
  177. package/src/cli/commands/keys.ts +9 -6
  178. package/src/cli/commands/mcp.ts +116 -156
  179. package/src/cli/commands/memory-v2.ts +296 -1
  180. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  181. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -2
  182. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -2
  183. package/src/cli/commands/platform/__tests__/status.test.ts +13 -15
  184. package/src/cli/commands/platform/disconnect.ts +5 -4
  185. package/src/cli/commands/platform/index.ts +0 -18
  186. package/src/cli/lib/daemon-credential-client.ts +110 -28
  187. package/src/cli/program.ts +2 -0
  188. package/src/config/assistant-feature-flags.ts +67 -10
  189. package/src/config/bundled-skills/acp/SKILL.md +6 -0
  190. package/src/config/bundled-skills/acp/TOOLS.json +1 -22
  191. package/src/config/bundled-skills/app-builder/SKILL.md +14 -109
  192. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -28
  193. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -10
  194. package/src/config/bundled-skills/app-control/SKILL.md +75 -0
  195. package/src/config/bundled-skills/app-control/TOOLS.json +299 -0
  196. package/src/config/bundled-skills/app-control/tools/app-control-click.ts +12 -0
  197. package/src/config/bundled-skills/app-control/tools/app-control-combo.ts +12 -0
  198. package/src/config/bundled-skills/app-control/tools/app-control-drag.ts +12 -0
  199. package/src/config/bundled-skills/app-control/tools/app-control-observe.ts +12 -0
  200. package/src/config/bundled-skills/app-control/tools/app-control-press.ts +12 -0
  201. package/src/config/bundled-skills/app-control/tools/app-control-sequence.ts +12 -0
  202. package/src/config/bundled-skills/app-control/tools/app-control-start.ts +12 -0
  203. package/src/config/bundled-skills/app-control/tools/app-control-stop.ts +12 -0
  204. package/src/config/bundled-skills/app-control/tools/app-control-type.ts +12 -0
  205. package/src/config/bundled-skills/computer-use/SKILL.md +6 -0
  206. package/src/config/bundled-skills/computer-use/TOOLS.json +67 -43
  207. package/src/config/bundled-skills/contacts/TOOLS.json +0 -16
  208. package/src/config/bundled-skills/document/TOOLS.json +0 -8
  209. package/src/config/bundled-skills/followups/TOOLS.json +0 -12
  210. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  211. package/src/config/bundled-skills/image-studio/TOOLS.json +0 -4
  212. package/src/config/bundled-skills/media-processing/TOOLS.json +0 -24
  213. package/src/config/bundled-skills/messaging/TOOLS.json +0 -40
  214. package/src/config/bundled-skills/phone-calls/TOOLS.json +0 -12
  215. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +19 -4
  216. package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
  217. package/src/config/bundled-skills/schedule/TOOLS.json +14 -14
  218. package/src/config/bundled-skills/sequences/TOOLS.json +0 -36
  219. package/src/config/bundled-skills/settings/SKILL.md +4 -0
  220. package/src/config/bundled-skills/settings/TOOLS.json +0 -12
  221. package/src/config/bundled-skills/skill-management/SKILL.md +6 -0
  222. package/src/config/bundled-skills/skill-management/TOOLS.json +0 -8
  223. package/src/config/bundled-skills/subagent/SKILL.md +6 -2
  224. package/src/config/bundled-skills/subagent/TOOLS.json +0 -20
  225. package/src/config/bundled-skills/transcribe/SKILL.md +4 -0
  226. package/src/config/bundled-skills/transcribe/TOOLS.json +0 -4
  227. package/src/config/bundled-tool-registry.ts +21 -0
  228. package/src/config/env-registry.ts +0 -2
  229. package/src/config/env.ts +19 -12
  230. package/src/config/feature-flag-registry.json +21 -133
  231. package/src/config/loader.ts +73 -99
  232. package/src/config/sanitize-for-transfer.ts +2 -0
  233. package/src/config/schemas/__tests__/memory-lifecycle.test.ts +80 -0
  234. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -4
  235. package/src/config/schemas/calls.ts +0 -9
  236. package/src/config/schemas/heartbeat.ts +63 -0
  237. package/src/config/schemas/ingress.ts +10 -6
  238. package/src/config/schemas/llm.ts +5 -10
  239. package/src/config/schemas/memory-lifecycle.ts +77 -24
  240. package/src/config/schemas/memory-v2.ts +48 -4
  241. package/src/config/schemas/platform.ts +6 -0
  242. package/src/config/schemas/services.ts +1 -15
  243. package/src/config/schemas/skills.ts +0 -6
  244. package/src/config/seed-inference-profiles.ts +1 -1
  245. package/src/contacts/contact-store.ts +0 -30
  246. package/src/contacts/contacts-write.ts +0 -27
  247. package/src/context/window-manager.ts +1 -2
  248. package/src/credential-execution/feature-gates.ts +10 -10
  249. package/src/credential-execution/process-manager.ts +12 -41
  250. package/src/daemon/__tests__/conversation-tool-setup.test.ts +126 -5
  251. package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
  252. package/src/daemon/config-watcher.ts +4 -3
  253. package/src/daemon/conversation-agent-loop-handlers.ts +21 -3
  254. package/src/daemon/conversation-agent-loop.ts +32 -28
  255. package/src/daemon/conversation-lifecycle.ts +8 -1
  256. package/src/daemon/conversation-process.ts +16 -11
  257. package/src/daemon/conversation-runtime-assembly.ts +2 -2
  258. package/src/daemon/conversation-surfaces.ts +125 -4
  259. package/src/daemon/conversation-tool-setup.ts +16 -55
  260. package/src/daemon/conversation.ts +21 -2
  261. package/src/daemon/doordash-steps.ts +1 -1
  262. package/src/daemon/handlers/shared.ts +4 -1
  263. package/src/daemon/host-app-control-proxy.ts +293 -0
  264. package/src/daemon/host-bash-proxy.ts +84 -74
  265. package/src/daemon/host-browser-proxy.ts +67 -82
  266. package/src/daemon/host-cu-proxy.ts +81 -86
  267. package/src/daemon/host-file-proxy.ts +93 -69
  268. package/src/daemon/host-proxy-base.ts +294 -0
  269. package/src/daemon/host-proxy-preactivation.ts +82 -0
  270. package/src/daemon/host-transfer-proxy.ts +247 -129
  271. package/src/daemon/lifecycle.ts +115 -117
  272. package/src/daemon/message-protocol.ts +3 -8
  273. package/src/daemon/message-types/contacts.ts +23 -1
  274. package/src/daemon/message-types/conversations.ts +11 -8
  275. package/src/daemon/message-types/host-app-control.ts +150 -0
  276. package/src/daemon/message-types/host-bash.ts +4 -0
  277. package/src/daemon/message-types/host-cu.ts +2 -0
  278. package/src/daemon/message-types/host-file.ts +4 -0
  279. package/src/daemon/message-types/host-transfer.ts +3 -0
  280. package/src/daemon/message-types/schedules.ts +8 -3
  281. package/src/daemon/message-types/skills.ts +2 -2
  282. package/src/daemon/process-message.ts +18 -1
  283. package/src/daemon/shutdown-handlers.ts +0 -3
  284. package/src/daemon/tool-setup-types.ts +51 -0
  285. package/src/daemon/tool-side-effects.ts +1 -1
  286. package/src/events/tool-audit-listener.ts +2 -1
  287. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +15 -7
  288. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +216 -0
  289. package/src/heartbeat/heartbeat-run-store.ts +236 -0
  290. package/src/heartbeat/heartbeat-service.ts +280 -49
  291. package/src/home/__tests__/post-connect-feed.test.ts +99 -0
  292. package/src/home/__tests__/relationship-state-writer.test.ts +11 -9
  293. package/src/home/__tests__/suggested-prompts.test.ts +89 -0
  294. package/src/home/post-connect-feed.ts +68 -0
  295. package/src/home/relationship-state-writer.ts +17 -92
  296. package/src/home/suggested-prompts.ts +46 -10
  297. package/src/inbound/public-ingress-urls.ts +32 -34
  298. package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
  299. package/src/ipc/assistant-server.ts +14 -1
  300. package/src/ipc/cli-client.ts +32 -1
  301. package/src/live-voice/live-voice-metrics.ts +10 -10
  302. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +304 -0
  303. package/src/mcp/mcp-auth-orchestrator.ts +213 -0
  304. package/src/mcp/mcp-auth-state.ts +133 -0
  305. package/src/mcp/mcp-oauth-provider.ts +19 -0
  306. package/src/memory/__tests__/jobs-store-job-classes.test.ts +24 -0
  307. package/src/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
  308. package/src/memory/__tests__/sparse-tokenize.test.ts +66 -0
  309. package/src/memory/anisotropy.test.ts +247 -0
  310. package/src/memory/anisotropy.ts +443 -0
  311. package/src/memory/auto-analysis-constants.ts +17 -0
  312. package/src/memory/auto-analysis-guard.ts +5 -15
  313. package/src/memory/canonical-guardian-store.ts +7 -7
  314. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +122 -0
  315. package/src/memory/context-search/agent-protocol.ts +6 -6
  316. package/src/memory/context-search/agent-runner.ts +32 -7
  317. package/src/memory/context-search/sources/memory-v2.ts +17 -5
  318. package/src/memory/conversation-crud.ts +1 -1
  319. package/src/memory/conversation-key-store.ts +2 -15
  320. package/src/memory/db-init.ts +4 -0
  321. package/src/memory/embedding-backend.ts +9 -21
  322. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +49 -4
  323. package/src/memory/graph/conversation-graph-memory.ts +1 -24
  324. package/src/memory/graph/graph-search.ts +8 -0
  325. package/src/memory/graph/retriever.ts +28 -0
  326. package/src/memory/graph/tools.ts +1 -1
  327. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +8 -2
  328. package/src/memory/jobs/embed-concept-page.ts +28 -2
  329. package/src/memory/jobs/embed-pkb-file.test.ts +2 -2
  330. package/src/memory/jobs-store.ts +66 -22
  331. package/src/memory/jobs-worker.ts +112 -63
  332. package/src/memory/memory-v2-activation-log-store.ts +1 -1
  333. package/src/memory/migrations/237-heartbeat-runs.ts +45 -0
  334. package/src/memory/migrations/238-schedule-retry-policy.ts +20 -0
  335. package/src/memory/migrations/index.ts +5 -0
  336. package/src/memory/migrations/registry.ts +8 -0
  337. package/src/memory/pkb/pkb-search.ts +7 -0
  338. package/src/memory/qdrant-client.ts +50 -20
  339. package/src/memory/schema/infrastructure.ts +15 -0
  340. package/src/memory/search/semantic.ts +7 -0
  341. package/src/memory/sparse-tokenize.ts +49 -0
  342. package/src/memory/v2/__tests__/activation.test.ts +77 -95
  343. package/src/memory/v2/__tests__/injection.test.ts +43 -21
  344. package/src/memory/v2/__tests__/sim.test.ts +166 -6
  345. package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
  346. package/src/memory/v2/__tests__/static-context.test.ts +0 -1
  347. package/src/memory/v2/activation.ts +69 -88
  348. package/src/memory/v2/consolidation-job.ts +3 -5
  349. package/src/memory/v2/constants.ts +7 -0
  350. package/src/memory/v2/injection.ts +86 -53
  351. package/src/memory/v2/prompts/consolidation.ts +312 -91
  352. package/src/memory/v2/qdrant.ts +99 -1
  353. package/src/memory/v2/sim.ts +126 -16
  354. package/src/memory/v2/skill-qdrant.ts +12 -3
  355. package/src/memory/v2/skill-store.ts +16 -1
  356. package/src/memory/v2/sparse-bm25.ts +245 -0
  357. package/src/memory/v2/static-context.ts +6 -5
  358. package/src/messaging/providers/gmail/types.ts +0 -49
  359. package/src/messaging/providers/slack/adapter.ts +1 -31
  360. package/src/messaging/providers/slack/types.ts +0 -32
  361. package/src/notifications/README.md +10 -10
  362. package/src/notifications/broadcaster.ts +1 -1
  363. package/src/notifications/guardian-question-mode.ts +5 -5
  364. package/src/oauth/connect-orchestrator.ts +4 -0
  365. package/src/oauth/credential-token-resolver.ts +1 -3
  366. package/src/oauth/manual-token-connection.ts +0 -4
  367. package/src/outbound-proxy/index.ts +1 -37
  368. package/src/outbound-proxy/logging.ts +1 -1
  369. package/src/outbound-proxy/policy.ts +6 -5
  370. package/src/outbound-proxy/router.ts +2 -1
  371. package/src/permissions/approval-policy.test.ts +6 -275
  372. package/src/permissions/approval-policy.ts +0 -51
  373. package/src/permissions/checker.test.ts +0 -1
  374. package/src/permissions/checker.ts +3 -17
  375. package/src/permissions/gateway-threshold-reader.ts +2 -0
  376. package/src/permissions/prompter.ts +34 -1
  377. package/src/permissions/secret-prompter.ts +6 -2
  378. package/src/prompts/bootstrap-cleanup.ts +27 -0
  379. package/src/prompts/system-prompt.ts +3 -18
  380. package/src/prompts/templates/SOUL.md +13 -1
  381. package/src/providers/speech-to-text/provider-catalog.ts +7 -8
  382. package/src/runtime/assistant-event-hub.ts +118 -96
  383. package/src/runtime/assistant-event.ts +1 -0
  384. package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
  385. package/src/runtime/auth/middleware.ts +0 -96
  386. package/src/runtime/auth/route-policy.ts +19 -0
  387. package/src/runtime/btw-sidechain.ts +2 -3
  388. package/src/runtime/channel-invite-transport.ts +2 -48
  389. package/src/runtime/channel-invite-transports/email.ts +1 -1
  390. package/src/runtime/channel-invite-transports/slack.ts +1 -1
  391. package/src/runtime/channel-invite-transports/telegram.ts +1 -1
  392. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  393. package/src/runtime/channel-invite-transports/whatsapp.ts +1 -1
  394. package/src/runtime/channel-invite-types.ts +54 -0
  395. package/src/runtime/channel-readiness-service.ts +32 -13
  396. package/src/runtime/http-server.ts +3 -329
  397. package/src/runtime/http-types.ts +0 -5
  398. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +413 -0
  399. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +260 -0
  400. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +189 -0
  401. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +153 -1
  402. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +451 -0
  403. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  404. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +515 -0
  405. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +437 -0
  406. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +319 -0
  407. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +51 -1
  408. package/src/runtime/migrations/migration-transport.ts +7 -7
  409. package/src/runtime/migrations/vbundle-builder.ts +327 -60
  410. package/src/runtime/migrations/vbundle-import-analyzer.ts +4 -4
  411. package/src/runtime/migrations/vbundle-import-policy.ts +172 -0
  412. package/src/runtime/migrations/vbundle-importer.ts +245 -68
  413. package/src/runtime/migrations/vbundle-streaming-importer.ts +326 -35
  414. package/src/runtime/migrations/vbundle-streaming-validator.ts +157 -4
  415. package/src/runtime/migrations/vbundle-tar-stream.ts +15 -6
  416. package/src/runtime/migrations/vbundle-validator.ts +114 -0
  417. package/src/runtime/pending-interactions.ts +35 -9
  418. package/src/runtime/routes/__tests__/backup-routes.test.ts +22 -150
  419. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
  420. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +242 -0
  421. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +112 -0
  422. package/src/runtime/routes/approval-interception-types.ts +13 -0
  423. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +1 -1
  424. package/src/runtime/routes/backup-routes.ts +15 -38
  425. package/src/runtime/routes/btw-routes.ts +14 -37
  426. package/src/runtime/routes/client-routes.ts +1 -0
  427. package/src/runtime/routes/contact-prompt-routes.ts +183 -0
  428. package/src/runtime/routes/conversation-query-routes.ts +36 -1
  429. package/src/runtime/routes/conversation-routes.ts +30 -13
  430. package/src/runtime/routes/document-pdf-renderer.ts +165 -0
  431. package/src/runtime/routes/documents-routes.ts +30 -0
  432. package/src/runtime/routes/errors.ts +19 -4
  433. package/src/runtime/routes/events-routes.ts +12 -6
  434. package/src/runtime/routes/gateway-log-routes.ts +79 -0
  435. package/src/runtime/routes/guardian-approval-interception.ts +2 -8
  436. package/src/runtime/routes/heartbeat-routes.ts +103 -38
  437. package/src/runtime/routes/host-app-control-routes.ts +134 -0
  438. package/src/runtime/routes/host-bash-routes.ts +36 -6
  439. package/src/runtime/routes/host-browser-routes.ts +108 -13
  440. package/src/runtime/routes/host-cu-routes.ts +44 -14
  441. package/src/runtime/routes/host-file-routes.ts +33 -10
  442. package/src/runtime/routes/host-transfer-routes.ts +64 -24
  443. package/src/runtime/routes/http-adapter.ts +1 -0
  444. package/src/runtime/routes/identity-intro-cache.ts +30 -0
  445. package/src/runtime/routes/identity-routes.ts +15 -43
  446. package/src/runtime/routes/inbound-message-handler.ts +1 -9
  447. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -7
  448. package/src/runtime/routes/inbound-stages/edit-intercept.ts +0 -8
  449. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +0 -20
  450. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
  451. package/src/runtime/routes/index.ts +8 -0
  452. package/src/runtime/routes/mcp-auth-routes.ts +132 -0
  453. package/src/runtime/routes/memory-item-routes.ts +10 -12
  454. package/src/runtime/routes/memory-v2-routes.ts +441 -1
  455. package/src/runtime/routes/migration-routes.ts +96 -0
  456. package/src/runtime/routes/schedule-routes.ts +7 -0
  457. package/src/runtime/verification-templates.ts +4 -7
  458. package/src/schedule/integration-status.ts +66 -2
  459. package/src/schedule/recurrence-engine.ts +4 -1
  460. package/src/schedule/retry-backoff.ts +18 -0
  461. package/src/schedule/retry-policy.ts +82 -0
  462. package/src/schedule/schedule-recovery.ts +64 -0
  463. package/src/schedule/schedule-store.ts +106 -2
  464. package/src/schedule/scheduler-types.ts +25 -0
  465. package/src/schedule/scheduler.ts +63 -38
  466. package/src/security/oauth-callback-registry.ts +8 -0
  467. package/src/sequence/analytics.ts +5 -5
  468. package/src/sequence/engine.ts +1 -1
  469. package/src/skills/catalog-files.ts +2 -8
  470. package/src/skills/include-graph.ts +5 -5
  471. package/src/skills/remote-skill-policy.ts +5 -5
  472. package/src/skills/skill-file-provider.ts +1 -1
  473. package/src/skills/skill-file-types.ts +13 -0
  474. package/src/skills/skillssh-audit-types.ts +28 -0
  475. package/src/skills/skillssh-registry.ts +8 -21
  476. package/src/telemetry/types.ts +2 -0
  477. package/src/telemetry/usage-telemetry-reporter.test.ts +21 -0
  478. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  479. package/src/tools/app-control/skill-proxy-bridge.ts +28 -0
  480. package/src/tools/apps/executors.ts +56 -69
  481. package/src/tools/browser/__tests__/browser-status.test.ts +21 -18
  482. package/src/tools/browser/browser-execution.ts +2 -2
  483. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +55 -4
  484. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +12 -6
  485. package/src/tools/browser/cdp-client/factory.ts +23 -24
  486. package/src/tools/browser/cdp-client/index.ts +1 -14
  487. package/src/tools/computer-use/definitions.ts +42 -20
  488. package/src/tools/executor.ts +2 -0
  489. package/src/tools/host-filesystem/edit.ts +26 -0
  490. package/src/tools/host-filesystem/read.ts +26 -0
  491. package/src/tools/host-filesystem/transfer.ts +31 -1
  492. package/src/tools/host-filesystem/write.ts +26 -0
  493. package/src/tools/host-terminal/host-shell.ts +58 -0
  494. package/src/tools/schedule/create.ts +6 -0
  495. package/src/tools/schedule/list.ts +2 -0
  496. package/src/tools/schedule/update.ts +10 -0
  497. package/src/tools/shared/filesystem/file-ops-service.ts +2 -0
  498. package/src/tools/shared/filesystem/path-policy.ts +25 -1
  499. package/src/tools/skills/load.ts +0 -32
  500. package/src/tools/tool-approval-handler.ts +1 -5
  501. package/src/tools/types.ts +4 -0
  502. package/src/usage/pricing.ts +1 -1
  503. package/src/workspace/hatched-date.ts +86 -0
  504. package/src/workspace/migrations/003-seed-device-id.ts +1 -1
  505. package/src/workspace/migrations/006-services-config.ts +8 -5
  506. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +3 -9
  507. package/src/workspace/migrations/021-move-signals-to-workspace.ts +4 -10
  508. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +4 -10
  509. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +4 -11
  510. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +3 -10
  511. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +3 -2
  512. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +2 -1
  513. package/src/workspace/migrations/059-move-pid-to-workspace.ts +3 -8
  514. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +3 -8
  515. package/src/workspace/migrations/AGENTS.md +1 -1
  516. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
  517. package/src/workspace/migrations/utils.ts +21 -0
  518. package/src/__tests__/host-browser-e2e-cloud.test.ts +0 -443
  519. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +0 -226
  520. package/src/__tests__/host-browser-ws-events-e2e.test.ts +0 -427
  521. package/src/__tests__/twilio-rest.test.ts +0 -34
  522. package/src/backup/__tests__/backup-key.test.ts +0 -152
  523. package/src/backup/__tests__/backup-worker.test.ts +0 -782
  524. package/src/backup/__tests__/offsite-writer.test.ts +0 -641
  525. package/src/backup/__tests__/stream-crypt.test.ts +0 -228
  526. package/src/backup/backup-key.ts +0 -137
  527. package/src/backup/backup-worker.ts +0 -472
  528. package/src/backup/offsite-writer.ts +0 -222
  529. package/src/backup/stream-crypt.ts +0 -263
  530. package/src/daemon/message-types/pairing.ts +0 -58
  531. package/src/outbound-proxy/config.ts +0 -20
  532. package/src/outbound-proxy/health.ts +0 -18
  533. package/src/outbound-proxy/types.ts +0 -150
  534. package/src/runtime/capability-tokens.ts +0 -190
  535. package/src/signals/mcp-reload.ts +0 -18
@@ -30,6 +30,16 @@ type IpcResponse = {
30
30
  id: string;
31
31
  result?: unknown;
32
32
  error?: string;
33
+ /** HTTP-style status code mirrored from `RouteError.statusCode`. */
34
+ statusCode?: number;
35
+ /** Machine-readable error code (e.g. "UNPROCESSABLE_ENTITY"). */
36
+ errorCode?: string;
37
+ /**
38
+ * Structured error payload mirroring `RouteError.details` — present only
39
+ * when the originating error carried a `details` field. Mirrors the HTTP
40
+ * adapter's `error.details` envelope.
41
+ */
42
+ errorDetails?: unknown;
33
43
  };
34
44
 
35
45
  // ---------------------------------------------------------------------------
@@ -43,6 +53,15 @@ export interface CliIpcCallResult<T = unknown> {
43
53
  ok: boolean;
44
54
  result?: T;
45
55
  error?: string;
56
+ /** HTTP-style status code surfaced from a daemon-side `RouteError`. */
57
+ statusCode?: number;
58
+ /** Machine-readable error code (e.g. "UNPROCESSABLE_ENTITY"). */
59
+ errorCode?: string;
60
+ /**
61
+ * Structured error payload mirroring `RouteError.details` — present only
62
+ * when the originating daemon-side error carried a `details` field.
63
+ */
64
+ errorDetails?: unknown;
46
65
  }
47
66
 
48
67
  /**
@@ -115,7 +134,19 @@ export async function cliIpcCall<T = unknown>(
115
134
  const msg = JSON.parse(line) as IpcResponse;
116
135
  if (msg.id === reqId) {
117
136
  if (msg.error) {
118
- finish({ ok: false, error: msg.error });
137
+ finish({
138
+ ok: false,
139
+ error: msg.error,
140
+ ...(msg.statusCode !== undefined && {
141
+ statusCode: msg.statusCode,
142
+ }),
143
+ ...(msg.errorCode !== undefined && {
144
+ errorCode: msg.errorCode,
145
+ }),
146
+ ...(msg.errorDetails !== undefined && {
147
+ errorDetails: msg.errorDetails,
148
+ }),
149
+ });
119
150
  } else {
120
151
  finish({ ok: true, result: msg.result as T });
121
152
  }
@@ -14,9 +14,9 @@ export type LiveVoiceMetricsEvent =
14
14
  | "turn_cancelled"
15
15
  | "session_ended";
16
16
 
17
- export type LiveVoiceTurnStatus = "active" | "completed" | "cancelled";
17
+ type LiveVoiceTurnStatus = "active" | "completed" | "cancelled";
18
18
 
19
- export interface LiveVoiceMetricsCollectorOptions {
19
+ interface LiveVoiceMetricsCollectorOptions {
20
20
  sessionId: string;
21
21
  conversationId?: string;
22
22
  clock?: LiveVoiceMetricsClock;
@@ -24,7 +24,7 @@ export interface LiveVoiceMetricsCollectorOptions {
24
24
  recentTurnLimit?: number;
25
25
  }
26
26
 
27
- export interface LiveVoiceSessionMetrics {
27
+ interface LiveVoiceSessionMetrics {
28
28
  sessionId: string;
29
29
  conversationId?: string;
30
30
  startedAtMs: number;
@@ -32,7 +32,7 @@ export interface LiveVoiceSessionMetrics {
32
32
  startToReadyMs: number | null;
33
33
  }
34
34
 
35
- export interface LiveVoiceTurnTimestamps {
35
+ interface LiveVoiceTurnTimestamps {
36
36
  startedAtMs: number;
37
37
  firstAudioAtMs: number | null;
38
38
  firstPartialAtMs: number | null;
@@ -44,7 +44,7 @@ export interface LiveVoiceTurnTimestamps {
44
44
  cancelledAtMs: number | null;
45
45
  }
46
46
 
47
- export interface LiveVoiceTurnDurations {
47
+ interface LiveVoiceTurnDurations {
48
48
  firstAudioToFirstPartialMs: number | null;
49
49
  pttReleaseToFinalTranscriptMs: number | null;
50
50
  finalTranscriptToFirstAssistantDeltaMs: number | null;
@@ -52,7 +52,7 @@ export interface LiveVoiceTurnDurations {
52
52
  totalTurnDurationMs: number | null;
53
53
  }
54
54
 
55
- export interface LiveVoiceTurnMetrics {
55
+ interface LiveVoiceTurnMetrics {
56
56
  turnId: string;
57
57
  status: LiveVoiceTurnStatus;
58
58
  cancellationReason: string | null;
@@ -60,13 +60,13 @@ export interface LiveVoiceTurnMetrics {
60
60
  durations: LiveVoiceTurnDurations;
61
61
  }
62
62
 
63
- export interface LiveVoiceDurationSummary {
63
+ interface LiveVoiceDurationSummary {
64
64
  count: number;
65
65
  p50Ms: number | null;
66
66
  p95Ms: number | null;
67
67
  }
68
68
 
69
- export interface LiveVoiceMetricsSummary {
69
+ interface LiveVoiceMetricsSummary {
70
70
  retainedTurnCount: number;
71
71
  completedTurnCount: number;
72
72
  cancelledTurnCount: number;
@@ -79,14 +79,14 @@ export interface LiveVoiceMetricsSummary {
79
79
  };
80
80
  }
81
81
 
82
- export interface LiveVoiceMetricsSnapshot {
82
+ interface LiveVoiceMetricsSnapshot {
83
83
  session: LiveVoiceSessionMetrics;
84
84
  activeTurn: LiveVoiceTurnMetrics | null;
85
85
  recentTurns: LiveVoiceTurnMetrics[];
86
86
  summary: LiveVoiceMetricsSummary;
87
87
  }
88
88
 
89
- export interface LiveVoiceMetricsAggregateFields {
89
+ interface LiveVoiceMetricsAggregateFields {
90
90
  sttMs: number | null;
91
91
  llmFirstDeltaMs: number | null;
92
92
  ttsFirstAudioMs: number | null;
@@ -0,0 +1,304 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ── Module mocks (must precede imports) ───────────────────────────────────────
4
+
5
+ // Track captured callbacks and deferred code promises for each McpOAuthProvider
6
+ // instance so tests can drive the flow.
7
+ let capturedOnAuthorizationUrl: ((url: string) => void) | undefined;
8
+ let deferredCodeResolve: ((code: string) => void) | undefined;
9
+ let deferredCodeReject: ((err: Error) => void) | undefined;
10
+
11
+ const mockInvalidateCredentials = mock(async () => {});
12
+ const mockStartCallbackServer = mock(async () => {
13
+ const codePromise = new Promise<string>((resolve, reject) => {
14
+ deferredCodeResolve = resolve;
15
+ deferredCodeReject = reject;
16
+ });
17
+ return { codePromise };
18
+ });
19
+ const mockStopCallbackServer = mock(() => {});
20
+
21
+ mock.module("../mcp-oauth-provider.js", () => ({
22
+ McpOAuthProvider: class {
23
+ constructor(
24
+ _serverId: string,
25
+ _serverUrl: string,
26
+ _interactive: boolean,
27
+ _callbackTransport: string,
28
+ options: { onAuthorizationUrl?: (url: string) => void } = {},
29
+ ) {
30
+ capturedOnAuthorizationUrl = options.onAuthorizationUrl;
31
+ }
32
+ invalidateCredentials = mockInvalidateCredentials;
33
+ startCallbackServer = mockStartCallbackServer;
34
+ stopCallbackServer = mockStopCallbackServer;
35
+ },
36
+ }));
37
+
38
+ const mockSetMcpAuthPending = mock(
39
+ (_serverId: string, _authUrl: string, _attemptId: string) => {},
40
+ );
41
+ // Default behavior: pretend the attempt still owns the slot (return true),
42
+ // so completion writes are applied unless a test overrides this.
43
+ const mockSetMcpAuthComplete = mock(
44
+ (_serverId: string, _attemptId: string): boolean => true,
45
+ );
46
+ const mockSetMcpAuthError = mock(
47
+ (_serverId: string, _error: string, _attemptId: string): boolean => true,
48
+ );
49
+
50
+ mock.module("../mcp-auth-state.js", () => ({
51
+ setMcpAuthPending: (...args: unknown[]) =>
52
+ mockSetMcpAuthPending(...(args as [string, string, string])),
53
+ setMcpAuthComplete: (...args: unknown[]) =>
54
+ mockSetMcpAuthComplete(...(args as [string, string])),
55
+ setMcpAuthError: (...args: unknown[]) =>
56
+ mockSetMcpAuthError(...(args as [string, string, string])),
57
+ }));
58
+
59
+ const mockReloadMcpServers = mock(async () => ({
60
+ ok: true,
61
+ reloaded: 0,
62
+ servers: [],
63
+ }));
64
+
65
+ mock.module("../../daemon/mcp-reload-service.js", () => ({
66
+ reloadMcpServers: () => mockReloadMcpServers(),
67
+ }));
68
+
69
+ mock.module("../../config/env-registry.js", () => ({
70
+ getIsContainerized: () => false,
71
+ }));
72
+
73
+ mock.module("../../util/logger.js", () => ({
74
+ getLogger: () =>
75
+ new Proxy({} as Record<string, unknown>, {
76
+ get: () => () => {},
77
+ }),
78
+ }));
79
+
80
+ // Create a fake UnauthorizedError class that the orchestrator's instanceof check
81
+ // will recognize (since we're also mocking the auth module).
82
+ class FakeUnauthorizedError extends Error {
83
+ constructor(message: string) {
84
+ super(message);
85
+ this.name = "UnauthorizedError";
86
+ }
87
+ }
88
+
89
+ mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({
90
+ UnauthorizedError: FakeUnauthorizedError,
91
+ }));
92
+
93
+ const mockFinishAuth = mock(async (_code: string) => {});
94
+ let mockConnectCallCount = 0;
95
+
96
+ mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
97
+ Client: class {
98
+ async connect() {
99
+ mockConnectCallCount++;
100
+ // Every connect fires onAuthorizationUrl and throws UnauthorizedError —
101
+ // the orchestrator never calls connect() a second time after finishAuth.
102
+ if (capturedOnAuthorizationUrl) {
103
+ capturedOnAuthorizationUrl("https://auth.example.com/oauth");
104
+ }
105
+ throw new FakeUnauthorizedError("unauthorized");
106
+ }
107
+ async close() {}
108
+ },
109
+ }));
110
+
111
+ mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({
112
+ SSEClientTransport: class {
113
+ constructor(_url: URL, _opts: unknown) {}
114
+ finishAuth = mockFinishAuth;
115
+ },
116
+ }));
117
+
118
+ mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
119
+ StreamableHTTPClientTransport: class {
120
+ constructor(_url: URL, _opts: unknown) {}
121
+ finishAuth = mockFinishAuth;
122
+ },
123
+ }));
124
+
125
+ // ── Import SUT after mocks ─────────────────────────────────────────────────────
126
+
127
+ const { orchestrateMcpOAuthConnect } =
128
+ await import("../mcp-auth-orchestrator.js");
129
+
130
+ // ── Helpers ────────────────────────────────────────────────────────────────────
131
+
132
+ function resetMocks() {
133
+ capturedOnAuthorizationUrl = undefined;
134
+ deferredCodeResolve = undefined;
135
+ deferredCodeReject = undefined;
136
+ mockInvalidateCredentials.mockClear();
137
+ mockStartCallbackServer.mockClear();
138
+ mockStopCallbackServer.mockClear();
139
+ mockSetMcpAuthPending.mockClear();
140
+ mockSetMcpAuthComplete.mockClear();
141
+ mockSetMcpAuthError.mockClear();
142
+ mockReloadMcpServers.mockClear();
143
+ mockFinishAuth.mockClear();
144
+ // Reset complete/error mocks to default "applied=true" behavior; tests
145
+ // that exercise the superseded branch override these in-test.
146
+ mockSetMcpAuthComplete.mockImplementation(() => true);
147
+ mockSetMcpAuthError.mockImplementation(() => true);
148
+ }
149
+
150
+ // ── Tests ──────────────────────────────────────────────────────────────────────
151
+
152
+ describe("orchestrateMcpOAuthConnect", () => {
153
+ beforeEach(() => {
154
+ resetMocks();
155
+ mockConnectCallCount = 0;
156
+ });
157
+
158
+ afterEach(() => {
159
+ resetMocks();
160
+ mockConnectCallCount = 0;
161
+ });
162
+
163
+ test("happy path — returns auth_url and sets state to pending", async () => {
164
+ const result = await orchestrateMcpOAuthConnect({
165
+ serverId: "test-server",
166
+ transport: { url: "https://example.com", type: "sse" },
167
+ });
168
+
169
+ expect(result.auth_url).toBe("https://auth.example.com/oauth");
170
+ expect(mockSetMcpAuthPending.mock.calls[0]).toEqual([
171
+ "test-server",
172
+ "https://auth.example.com/oauth",
173
+ expect.any(String) as unknown as string, // attemptId UUID
174
+ ]);
175
+ // Sanity-check the attemptId looks UUID-shaped
176
+ expect(mockSetMcpAuthPending.mock.calls[0][2]).toMatch(
177
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
178
+ );
179
+ expect(result).toBeDefined();
180
+ });
181
+
182
+ test("tail completion — codePromise resolves → state goes to complete", async () => {
183
+ await orchestrateMcpOAuthConnect({
184
+ serverId: "test-server",
185
+ transport: { url: "https://example.com", type: "sse" },
186
+ });
187
+
188
+ // Resolve the code to trigger the background tail
189
+ deferredCodeResolve!("auth-code-123");
190
+
191
+ // Wait for fire-and-forget tail to settle
192
+ await new Promise<void>((resolve) => setTimeout(resolve, 20));
193
+
194
+ expect(mockFinishAuth).toHaveBeenCalledWith("auth-code-123");
195
+ // The orchestrator calls connect() exactly once (the initial attempt that triggers
196
+ // UnauthorizedError). It does NOT reconnect after finishAuth to avoid the
197
+ // "already started" error thrown by SSE/StreamableHTTP transports.
198
+ expect(mockConnectCallCount).toBe(1);
199
+ expect(mockSetMcpAuthComplete).toHaveBeenCalledWith(
200
+ "test-server",
201
+ expect.any(String) as unknown as string,
202
+ );
203
+ // Daemon-side reload should be triggered after a successful completion.
204
+ expect(mockReloadMcpServers).toHaveBeenCalled();
205
+ });
206
+
207
+ test("transport.finishAuth rejects → state goes to error", async () => {
208
+ mockFinishAuth.mockImplementationOnce(async () => {
209
+ throw new Error("exchange failed");
210
+ });
211
+
212
+ await orchestrateMcpOAuthConnect({
213
+ serverId: "test-server",
214
+ transport: { url: "https://example.com", type: "sse" },
215
+ });
216
+
217
+ deferredCodeResolve!("auth-code-456");
218
+
219
+ await new Promise<void>((resolve) => setTimeout(resolve, 20));
220
+
221
+ expect(mockSetMcpAuthError).toHaveBeenCalledWith(
222
+ "test-server",
223
+ "exchange failed",
224
+ expect.any(String) as unknown as string,
225
+ );
226
+ expect(mockSetMcpAuthComplete).not.toHaveBeenCalled();
227
+ });
228
+
229
+ test("codePromise rejects (timeout/user deny) → state goes to error", async () => {
230
+ await orchestrateMcpOAuthConnect({
231
+ serverId: "test-server",
232
+ transport: { url: "https://example.com", type: "sse" },
233
+ });
234
+
235
+ deferredCodeReject!(new Error("MCP OAuth callback timed out"));
236
+
237
+ await new Promise<void>((resolve) => setTimeout(resolve, 20));
238
+
239
+ expect(mockSetMcpAuthError).toHaveBeenCalledWith(
240
+ "test-server",
241
+ "MCP OAuth callback timed out",
242
+ expect.any(String) as unknown as string,
243
+ );
244
+ expect(mockSetMcpAuthComplete).not.toHaveBeenCalled();
245
+ });
246
+
247
+ test("re-start for same serverId while previous is pending → new state overwrites with a fresh attemptId", async () => {
248
+ await orchestrateMcpOAuthConnect({
249
+ serverId: "srv",
250
+ transport: { url: "https://example.com", type: "sse" },
251
+ });
252
+
253
+ await orchestrateMcpOAuthConnect({
254
+ serverId: "srv",
255
+ transport: { url: "https://example.com", type: "sse" },
256
+ });
257
+
258
+ expect(mockSetMcpAuthPending.mock.calls).toHaveLength(2);
259
+ expect(mockSetMcpAuthPending.mock.calls[0][0]).toBe("srv");
260
+ expect(mockSetMcpAuthPending.mock.calls[1][0]).toBe("srv");
261
+ // Each attempt gets a distinct UUID so superseded tails can be detected.
262
+ const firstAttemptId = mockSetMcpAuthPending.mock.calls[0][2];
263
+ const secondAttemptId = mockSetMcpAuthPending.mock.calls[1][2];
264
+ expect(firstAttemptId).not.toBe(secondAttemptId);
265
+ });
266
+
267
+ test("supersede — when setMcpAuthComplete returns false, daemon-side reload is NOT triggered", async () => {
268
+ // Simulate a newer attempt having taken the slot: completion writes
269
+ // for the older attempt should be skipped, and reload should not fire.
270
+ mockSetMcpAuthComplete.mockImplementation(() => false);
271
+
272
+ await orchestrateMcpOAuthConnect({
273
+ serverId: "test-server",
274
+ transport: { url: "https://example.com", type: "sse" },
275
+ });
276
+
277
+ deferredCodeResolve!("auth-code-superseded");
278
+ await new Promise<void>((resolve) => setTimeout(resolve, 20));
279
+
280
+ // The set was attempted but returned false, so no reload should happen
281
+ expect(mockSetMcpAuthComplete).toHaveBeenCalled();
282
+ expect(mockReloadMcpServers).not.toHaveBeenCalled();
283
+ });
284
+
285
+ test("reload failure after completion is logged but does not corrupt success state", async () => {
286
+ mockReloadMcpServers.mockImplementation(async () => {
287
+ throw new Error("reload boom");
288
+ });
289
+
290
+ await orchestrateMcpOAuthConnect({
291
+ serverId: "test-server",
292
+ transport: { url: "https://example.com", type: "sse" },
293
+ });
294
+
295
+ deferredCodeResolve!("auth-code-789");
296
+ await new Promise<void>((resolve) => setTimeout(resolve, 20));
297
+
298
+ // Completion was applied even though reload threw — the polling CLI
299
+ // still observes status=complete.
300
+ expect(mockSetMcpAuthComplete).toHaveBeenCalled();
301
+ expect(mockSetMcpAuthError).not.toHaveBeenCalled();
302
+ expect(mockReloadMcpServers).toHaveBeenCalled();
303
+ });
304
+ });
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Daemon-side orchestrator for MCP OAuth flows.
3
+ *
4
+ * Runs the entire OAuth exchange (callback registration, auth URL capture,
5
+ * code exchange, token persistence) inside the daemon heap so that
6
+ * registerPendingCallback and consumeCallback always execute in the same
7
+ * process. The CLI receives only the authorization URL via IPC and polls
8
+ * for completion.
9
+ */
10
+
11
+ import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
12
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
13
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
14
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
15
+
16
+ import { getIsContainerized } from "../config/env-registry.js";
17
+ import { reloadMcpServers } from "../daemon/mcp-reload-service.js";
18
+ import { getLogger } from "../util/logger.js";
19
+ import {
20
+ setMcpAuthComplete,
21
+ setMcpAuthError,
22
+ setMcpAuthPending,
23
+ } from "./mcp-auth-state.js";
24
+ import {
25
+ type McpOAuthCallbackTransport,
26
+ McpOAuthProvider,
27
+ } from "./mcp-oauth-provider.js";
28
+
29
+ const log = getLogger("mcp-auth-orchestrator");
30
+
31
+ export interface McpAuthTransportConfig {
32
+ url: string;
33
+ type: "sse" | "streamable-http";
34
+ headers?: Record<string, string>;
35
+ }
36
+
37
+ export interface OrchestrateMcpOAuthConnectResult {
38
+ auth_url: string;
39
+ already_authenticated?: true;
40
+ }
41
+
42
+ /**
43
+ * Start a daemon-owned MCP OAuth flow.
44
+ *
45
+ * Returns immediately with the authorization URL for the CLI to open in
46
+ * the browser. The token exchange runs in the background in the daemon heap
47
+ * and updates the in-memory auth state map on completion.
48
+ */
49
+ export async function orchestrateMcpOAuthConnect(args: {
50
+ serverId: string;
51
+ transport: McpAuthTransportConfig;
52
+ }): Promise<OrchestrateMcpOAuthConnectResult> {
53
+ const { serverId, transport } = args;
54
+
55
+ // Containerized deployments (platform-managed AND self-hosted Docker) must
56
+ // use the gateway transport: the browser is outside the daemon container,
57
+ // so a loopback callback inside the container is unreachable. Bare-metal
58
+ // daemons can use loopback as before.
59
+ const callbackTransport: McpOAuthCallbackTransport = getIsContainerized()
60
+ ? "gateway"
61
+ : "loopback";
62
+
63
+ let capturedAuthUrl: string | undefined;
64
+ const provider = new McpOAuthProvider(
65
+ serverId,
66
+ transport.url,
67
+ /* interactive */ false,
68
+ callbackTransport,
69
+ {
70
+ onAuthorizationUrl: (url) => {
71
+ capturedAuthUrl = url;
72
+ },
73
+ },
74
+ );
75
+
76
+ // Clear stale credentials so the flow starts fresh
77
+ await provider.invalidateCredentials("client");
78
+ await provider.invalidateCredentials("discovery");
79
+
80
+ // Register the pending callback in the daemon heap
81
+ const { codePromise } = await provider.startCallbackServer();
82
+
83
+ // Build the MCP transport and client
84
+ const serverUrl = new URL(transport.url);
85
+ const TransportClass =
86
+ transport.type === "sse"
87
+ ? SSEClientTransport
88
+ : StreamableHTTPClientTransport;
89
+ const mcpTransport = new TransportClass(serverUrl, {
90
+ authProvider: provider,
91
+ requestInit: transport.headers ? { headers: transport.headers } : undefined,
92
+ });
93
+ const client = new Client({ name: "vellum-assistant", version: "1.0.0" });
94
+
95
+ try {
96
+ await client.connect(mcpTransport);
97
+ // No error — server is already authenticated
98
+ provider.stopCallbackServer();
99
+ try {
100
+ await client.close();
101
+ } catch {
102
+ /* ignore */
103
+ }
104
+ return { auth_url: "", already_authenticated: true };
105
+ } catch (err) {
106
+ if (err instanceof UnauthorizedError) {
107
+ // Expected — onAuthorizationUrl has fired, capturedAuthUrl is set
108
+ } else {
109
+ provider.stopCallbackServer();
110
+ try {
111
+ await client.close();
112
+ } catch {
113
+ /* ignore */
114
+ }
115
+ throw err;
116
+ }
117
+ }
118
+
119
+ if (!capturedAuthUrl) {
120
+ provider.stopCallbackServer();
121
+ try {
122
+ await client.close();
123
+ } catch {
124
+ /* ignore */
125
+ }
126
+ throw new Error("No authorization URL captured from OAuth provider");
127
+ }
128
+
129
+ // Per-attempt token. Gates fire-and-forget state writes so that a
130
+ // re-run of `assistant mcp auth <serverId>` before the previous attempt
131
+ // finishes cannot have its slot overwritten by stale completion writes
132
+ // from the older attempt.
133
+ const attemptId = crypto.randomUUID();
134
+ setMcpAuthPending(serverId, capturedAuthUrl, attemptId);
135
+
136
+ // Fire-and-forget background tail — completes the token exchange once
137
+ // the user approves in the browser.
138
+ // Note: we do NOT call client.connect() again here — both SSEClientTransport
139
+ // and StreamableHTTPClientTransport throw "already started" on a second
140
+ // connect() call. The tokens are persisted by saveTokens inside finishAuth,
141
+ // so the daemon can reconnect on the next MCP reload without re-connecting here.
142
+ void (async () => {
143
+ try {
144
+ // Apply an explicit timeout: the loopback callback path has a built-in
145
+ // 2-minute timer, but the gateway transport's deferred promise relies on
146
+ // the caller for time-boxing. Without this race, gateway-mode tails leak
147
+ // forever if the user never completes the OAuth handshake.
148
+ const code = await Promise.race([
149
+ codePromise,
150
+ new Promise<never>((_, reject) => {
151
+ const t = setTimeout(
152
+ () => reject(new Error("OAuth callback timed out")),
153
+ CALLBACK_TIMEOUT_MS,
154
+ );
155
+ // Don't keep the event loop alive solely for this timer
156
+ if (typeof t.unref === "function") t.unref();
157
+ }),
158
+ ]);
159
+ await mcpTransport.finishAuth(code);
160
+ const applied = setMcpAuthComplete(serverId, attemptId);
161
+ if (!applied) {
162
+ log.info(
163
+ { serverId, attemptId },
164
+ "MCP OAuth completion superseded by newer attempt — skipping state write",
165
+ );
166
+ return;
167
+ }
168
+ log.info({ serverId }, "MCP OAuth flow completed");
169
+ // Trigger MCP reload from inside the daemon so the CLI doesn't need
170
+ // to fall back on the deprecated file-based signal mechanism.
171
+ // Best-effort: reload failures are logged but don't poison the
172
+ // success status the polling CLI is about to observe.
173
+ try {
174
+ await reloadMcpServers();
175
+ } catch (reloadErr) {
176
+ log.warn(
177
+ {
178
+ serverId,
179
+ err:
180
+ reloadErr instanceof Error
181
+ ? reloadErr.message
182
+ : String(reloadErr),
183
+ },
184
+ "MCP reload after auth completion failed",
185
+ );
186
+ }
187
+ } catch (err) {
188
+ const message = err instanceof Error ? err.message : String(err);
189
+ const applied = setMcpAuthError(serverId, message, attemptId);
190
+ if (!applied) {
191
+ log.info(
192
+ { serverId, attemptId, error: message },
193
+ "MCP OAuth error superseded by newer attempt — skipping state write",
194
+ );
195
+ } else {
196
+ log.warn({ serverId, error: message }, "MCP OAuth flow failed");
197
+ }
198
+ } finally {
199
+ provider.stopCallbackServer();
200
+ try {
201
+ await client.close();
202
+ } catch {
203
+ /* ignore */
204
+ }
205
+ }
206
+ })();
207
+
208
+ return { auth_url: capturedAuthUrl };
209
+ }
210
+
211
+ // Matches the loopback callback timeout; keep both in lockstep so loopback
212
+ // and gateway transports time-box OAuth identically.
213
+ const CALLBACK_TIMEOUT_MS = 2 * 60 * 1000;