@vellumai/assistant 0.12.0 → 0.12.1-dev.202609141913.5e16607

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 (441) hide show
  1. package/AGENTS.md +5 -1
  2. package/ARCHITECTURE.md +11 -12
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/openapi.yaml +292 -50
  6. package/package.json +1 -1
  7. package/scripts/smoke-desktop-window-theme.ts +186 -0
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  9. package/src/__tests__/app-compiler.test.ts +6 -0
  10. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  12. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  13. package/src/__tests__/computer-use-tools.test.ts +17 -5
  14. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  15. package/src/__tests__/config-schema.test.ts +28 -29
  16. package/src/__tests__/config-watcher.test.ts +23 -8
  17. package/src/__tests__/connection-model-compat.test.ts +2 -2
  18. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  19. package/src/__tests__/conversation-pairing.test.ts +26 -0
  20. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  21. package/src/__tests__/conversation-queue.test.ts +48 -207
  22. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  23. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  24. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  25. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  26. package/src/__tests__/conversation-title-service.test.ts +14 -10
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  28. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  29. package/src/__tests__/credential-record-retire.test.ts +171 -1
  30. package/src/__tests__/credential-routes.test.ts +212 -3
  31. package/src/__tests__/credential-security-invariants.test.ts +4 -2
  32. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  33. package/src/__tests__/document-sync-tags.test.ts +25 -0
  34. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  35. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  36. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  37. package/src/__tests__/git-porcelain.test.ts +47 -0
  38. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  39. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  40. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  41. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  42. package/src/__tests__/host-file-proxy.test.ts +11 -23
  43. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  44. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  45. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  46. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  47. package/src/__tests__/mcp-health-check.test.ts +8 -11
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  49. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  50. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  51. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  52. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  53. package/src/__tests__/model-intents.test.ts +3 -0
  54. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  55. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  56. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  57. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  58. package/src/__tests__/plugin-import-boundary-guard.test.ts +13 -0
  59. package/src/__tests__/resolve-app.test.ts +135 -0
  60. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  61. package/src/__tests__/service-account-token.test.ts +247 -0
  62. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  63. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  64. package/src/__tests__/source-fingerprint.test.ts +184 -0
  65. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  66. package/src/__tests__/subagent-tools.test.ts +3 -2
  67. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  68. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  69. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  70. package/src/__tests__/workspace-git-service.test.ts +26 -0
  71. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  72. package/src/acp/__tests__/agent-process.test.ts +258 -4
  73. package/src/acp/__tests__/auth-required.test.ts +96 -0
  74. package/src/acp/__tests__/client-handler.test.ts +90 -2
  75. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  76. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  77. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  78. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  79. package/src/acp/__tests__/session-manager.test.ts +895 -4
  80. package/src/acp/agent-process.test.ts +4 -1
  81. package/src/acp/agent-process.ts +94 -9
  82. package/src/acp/auth-required.ts +50 -1
  83. package/src/acp/auto-install.test.ts +9 -7
  84. package/src/acp/client-handler.ts +16 -3
  85. package/src/acp/model-config.test.ts +166 -0
  86. package/src/acp/model-config.ts +138 -0
  87. package/src/acp/resolve-agent.test.ts +220 -6
  88. package/src/acp/resolve-agent.ts +88 -23
  89. package/src/acp/session-manager.test.ts +196 -3
  90. package/src/acp/session-manager.ts +441 -18
  91. package/src/acp/types.ts +50 -5
  92. package/src/api/events/acp-session-model-update.test.ts +88 -0
  93. package/src/api/events/acp-session-model-update.ts +46 -0
  94. package/src/api/events/app-files-changed.ts +4 -3
  95. package/src/api/events/notification-intent.ts +2 -0
  96. package/src/api/index.ts +6 -0
  97. package/src/apps/app-store.ts +1 -1
  98. package/src/apps/resolve-app.ts +81 -0
  99. package/src/apps/source-fingerprint.ts +266 -0
  100. package/src/bundler/app-compiler.ts +15 -0
  101. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  102. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  103. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  104. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  105. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  106. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  107. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  108. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  109. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  110. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  111. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  112. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  113. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  114. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  115. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  116. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  117. package/src/calls/barge-in-guard.ts +118 -0
  118. package/src/calls/call-constants.ts +24 -4
  119. package/src/calls/call-controller.ts +499 -199
  120. package/src/calls/call-transport.ts +16 -0
  121. package/src/calls/media-stream-output.ts +32 -0
  122. package/src/calls/media-stream-server.ts +82 -30
  123. package/src/calls/media-stream-stt-session.ts +15 -0
  124. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  125. package/src/calls/resolve-call-tts-provider.ts +33 -2
  126. package/src/calls/tts-call-strategy.ts +9 -5
  127. package/src/calls/types.ts +2 -0
  128. package/src/calls/voice-control-protocol.ts +53 -21
  129. package/src/calls/voice-leg-coordinator.ts +189 -0
  130. package/src/calls/voice-progress-cadence.ts +423 -0
  131. package/src/calls/voice-session-bridge.ts +59 -46
  132. package/src/calls/voice-triage-escalate.ts +175 -56
  133. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  134. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  135. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  136. package/src/cli/commands/apps.help.ts +67 -13
  137. package/src/cli/commands/apps.ts +183 -0
  138. package/src/cli/commands/bash.help.ts +8 -13
  139. package/src/cli/commands/bash.ts +7 -25
  140. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  141. package/src/cli/commands/channels/index.help.ts +70 -2
  142. package/src/cli/commands/channels/index.ts +16 -3
  143. package/src/cli/commands/channels/send.ts +128 -0
  144. package/src/cli/commands/credentials.help.ts +6 -2
  145. package/src/cli/commands/credentials.ts +2 -2
  146. package/src/cli/commands/mcp.help.ts +8 -8
  147. package/src/cli/commands/mcp.ts +1 -1
  148. package/src/cli/commands/roadmap.help.ts +5 -1
  149. package/src/cli/commands/roadmap.ts +1 -1
  150. package/src/cli/lib/bundled-marketplace.json +103 -90
  151. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  152. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  153. package/src/config/__tests__/platform-identity.test.ts +255 -0
  154. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  155. package/src/config/acp-agent-write.ts +43 -0
  156. package/src/config/acp-defaults.test.ts +13 -3
  157. package/src/config/acp-defaults.ts +23 -6
  158. package/src/config/acp-schema.test.ts +81 -0
  159. package/src/config/acp-schema.ts +30 -2
  160. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  161. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  162. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  163. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  164. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  165. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  166. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  167. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  168. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  169. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  170. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  171. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  172. package/src/config/default-profile-catalog.ts +1 -1
  173. package/src/config/env.ts +2 -2
  174. package/src/config/feature-flag-registry.json +24 -12
  175. package/src/config/loader.ts +1 -0
  176. package/src/config/memory-v3-gate.test.ts +31 -0
  177. package/src/config/memory-v3-gate.ts +20 -16
  178. package/src/config/platform-identity.ts +201 -0
  179. package/src/config/platform-rehydration.ts +11 -27
  180. package/src/config/schema.ts +2 -2
  181. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  182. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  183. package/src/config/schemas/calls.ts +1 -28
  184. package/src/config/schemas/live-voice.ts +1 -173
  185. package/src/config/schemas/mcp.ts +1 -1
  186. package/src/config/schemas/memory-retrospective.ts +10 -0
  187. package/src/config/schemas/services.ts +8 -0
  188. package/src/config/schemas/skills.ts +0 -14
  189. package/src/config/schemas/voice.ts +150 -0
  190. package/src/credential-execution/process-manager.test.ts +18 -19
  191. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  192. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  193. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  194. package/src/daemon/app-change-notify.ts +45 -0
  195. package/src/daemon/config-watcher.ts +13 -194
  196. package/src/daemon/conversation-process.ts +35 -64
  197. package/src/daemon/conversation-skill-tools.ts +13 -31
  198. package/src/daemon/conversation-store.ts +1 -1
  199. package/src/daemon/conversation-surfaces.ts +10 -8
  200. package/src/daemon/conversation-tool-setup.ts +37 -43
  201. package/src/daemon/conversation.ts +0 -3
  202. package/src/daemon/host-bash-proxy.ts +8 -12
  203. package/src/daemon/host-browser-proxy.ts +15 -1
  204. package/src/daemon/host-file-proxy.ts +8 -10
  205. package/src/daemon/host-transfer-proxy.ts +14 -18
  206. package/src/daemon/lifecycle.ts +0 -5
  207. package/src/daemon/mcp-reload-service.ts +5 -5
  208. package/src/daemon/message-types/sync.ts +1 -0
  209. package/src/daemon/orphan-reaper.ts +1 -1
  210. package/src/daemon/providers-setup.ts +1 -1
  211. package/src/daemon/shutdown-handlers.ts +1 -9
  212. package/src/daemon/skill-memory-refresh.ts +64 -0
  213. package/src/daemon/tool-side-effects.ts +7 -18
  214. package/src/desktop/__tests__/fake-desktop.ts +119 -0
  215. package/src/desktop/desktop-chrome-policy.test.ts +71 -0
  216. package/src/desktop/desktop-chrome-policy.ts +42 -0
  217. package/src/desktop/desktop-dependencies.test.ts +102 -0
  218. package/src/desktop/desktop-dependencies.ts +306 -0
  219. package/src/desktop/desktop-feature.test.ts +18 -0
  220. package/src/desktop/desktop-feature.ts +19 -0
  221. package/src/desktop/desktop-panel-config.test.ts +135 -0
  222. package/src/desktop/desktop-panel-config.ts +149 -0
  223. package/src/desktop/desktop-session-manager.test.ts +627 -0
  224. package/src/desktop/desktop-session-manager.ts +814 -0
  225. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  226. package/src/desktop/desktop-stream-bridge.ts +214 -0
  227. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  228. package/src/desktop/desktop-wallpaper.ts +113 -0
  229. package/src/desktop/desktop-window-theme.ts +170 -0
  230. package/src/home/__tests__/feed-writer.test.ts +22 -0
  231. package/src/home/feed-writer.ts +11 -2
  232. package/src/hooks/types.ts +33 -0
  233. package/src/i18n/AGENTS.md +22 -0
  234. package/src/i18n/__tests__/i18n.test.ts +108 -0
  235. package/src/i18n/index.ts +16 -0
  236. package/src/i18n/locales.ts +93 -0
  237. package/src/i18n/messages.ts +61 -0
  238. package/src/i18n/title.ts +25 -0
  239. package/src/inbound/platform-callback-registration.ts +4 -5
  240. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  241. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  242. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  243. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  244. package/src/live-voice/live-voice-session.ts +213 -693
  245. package/src/live-voice/protocol.ts +0 -2
  246. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  247. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  248. package/src/mcp/effective-config.ts +3 -3
  249. package/src/mcp/mcp-header-store.ts +29 -45
  250. package/src/mcp/spec-schema.ts +111 -0
  251. package/src/mcp/startup.ts +8 -4
  252. package/src/mcp/workspace-mcp-config.ts +188 -0
  253. package/src/messaging/provider.ts +10 -3
  254. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  255. package/src/messaging/providers/callback-routing.ts +18 -0
  256. package/src/messaging/providers/channel-transport.ts +56 -0
  257. package/src/messaging/providers/discord/transport.ts +17 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  259. package/src/messaging/providers/slack/adapter.ts +7 -44
  260. package/src/messaging/providers/slack/transport.ts +16 -0
  261. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  262. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  263. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  264. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  265. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  266. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  267. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  268. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  269. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  270. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  271. package/src/notifications/adapters/macos.ts +3 -0
  272. package/src/notifications/assistant-reply-producer.ts +8 -3
  273. package/src/notifications/conversation-pairing.ts +36 -16
  274. package/src/notifications/delivered-post-record.ts +11 -1
  275. package/src/notifications/guardian-feed-projection.ts +125 -5
  276. package/src/notifications/home-feed-side-effect.ts +55 -7
  277. package/src/notifications/schedule-result-producer.ts +66 -22
  278. package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
  279. package/src/oauth/byo-connection.test.ts +45 -0
  280. package/src/oauth/byo-connection.ts +36 -6
  281. package/src/oauth/connection-resolver.test.ts +73 -0
  282. package/src/oauth/connection-resolver.ts +95 -2
  283. package/src/oauth/seed-providers.ts +165 -0
  284. package/src/outbound-proxy/certs.ts +3 -5
  285. package/src/permissions/checker.test.ts +11 -6
  286. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  287. package/src/persistence/conversation-crud.ts +71 -63
  288. package/src/persistence/conversation-lineage.ts +12 -4
  289. package/src/persistence/conversation-title-placeholders.ts +23 -0
  290. package/src/persistence/conversation-title-service.ts +9 -26
  291. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  292. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  293. package/src/persistence/message-cursor.ts +78 -0
  294. package/src/persistence/message-reads.ts +30 -0
  295. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  296. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  297. package/src/persistence/schema/memory-core.ts +5 -0
  298. package/src/platform/client.test.ts +4 -2
  299. package/src/platform/client.ts +2 -10
  300. package/src/plugin-api/constants.ts +2 -0
  301. package/src/plugin-api/index.ts +1 -0
  302. package/src/plugin-api/types.ts +2 -0
  303. package/src/plugins/defaults/index.ts +2 -0
  304. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  305. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  306. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  307. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  308. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  309. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  310. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  311. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  312. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  313. package/src/plugins/defaults/memory/graph/capability-seed.ts +4 -6
  314. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  315. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  316. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  317. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  318. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  319. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  320. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  321. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  322. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  323. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  324. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  325. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  326. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  327. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  328. package/src/plugins/defaults/memory/startup.ts +6 -1
  329. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  330. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  331. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +89 -0
  332. package/src/plugins/defaults/memory/worker.ts +6 -0
  333. package/src/plugins/mcp-servers.ts +15 -75
  334. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  335. package/src/providers/gemini/client.ts +9 -23
  336. package/src/providers/gemini-thought-signature.test.ts +63 -0
  337. package/src/providers/gemini-thought-signature.ts +44 -0
  338. package/src/providers/inference/auth.ts +2 -3
  339. package/src/providers/inference/connection-availability.ts +27 -8
  340. package/src/providers/inference/resolve-auth.ts +22 -4
  341. package/src/providers/inference/service-account-token.ts +248 -0
  342. package/src/providers/model-catalog.ts +34 -0
  343. package/src/providers/model-intents.ts +10 -5
  344. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  345. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  346. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  347. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  348. package/src/providers/openai/chat-completions-provider.ts +192 -78
  349. package/src/providers/openai/codex-models.ts +0 -4
  350. package/src/providers/openai/google-thought-signature.ts +320 -0
  351. package/src/runtime/AGENTS.md +2 -0
  352. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  353. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  354. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  355. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  356. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  357. package/src/runtime/auth/same-actor.ts +38 -3
  358. package/src/runtime/channel-send.ts +316 -0
  359. package/src/runtime/http-server.ts +160 -151
  360. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  361. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  362. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  363. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  364. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  365. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  366. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  367. package/src/runtime/routes/acp-routes.test.ts +15 -0
  368. package/src/runtime/routes/acp-routes.ts +72 -9
  369. package/src/runtime/routes/app-management-routes.ts +73 -0
  370. package/src/runtime/routes/app-routes.ts +1 -1
  371. package/src/runtime/routes/channel-send-routes.ts +85 -0
  372. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  373. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  374. package/src/runtime/routes/credential-routes.ts +19 -10
  375. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  376. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  377. package/src/runtime/routes/diagnostics-routes.ts +55 -3
  378. package/src/runtime/routes/index.ts +4 -2
  379. package/src/runtime/routes/log-export-routes.ts +2 -0
  380. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  381. package/src/runtime/routes/migration-routes.ts +6 -20
  382. package/src/runtime/routes/platform-routes.ts +18 -15
  383. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  384. package/src/runtime/services/conversation-serializer.ts +16 -3
  385. package/src/runtime/sync/resource-sync-events.ts +2 -1
  386. package/src/schedule/worker.ts +1 -1
  387. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  388. package/src/security/ces-http-record-backend.ts +120 -0
  389. package/src/security/ces-rpc-record-backend.ts +7 -0
  390. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  391. package/src/skills/workspace-skill-md-signature.ts +50 -0
  392. package/src/subagent/consult-context.ts +12 -25
  393. package/src/tools/acp/list-agents.test.ts +3 -1
  394. package/src/tools/acp/spawn.test.ts +146 -11
  395. package/src/tools/acp/spawn.ts +21 -3
  396. package/src/tools/acp/status.test.ts +94 -0
  397. package/src/tools/acp/status.ts +40 -3
  398. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  399. package/src/tools/capability-offer.test.ts +22 -2
  400. package/src/tools/capability-offer.ts +16 -1
  401. package/src/tools/computer-use/definitions.ts +4 -2
  402. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  403. package/src/tools/credentials/metadata-store.ts +100 -0
  404. package/src/tools/host-filesystem/edit.test.ts +2 -1
  405. package/src/tools/host-filesystem/edit.ts +2 -2
  406. package/src/tools/host-filesystem/read.test.ts +2 -1
  407. package/src/tools/host-filesystem/read.ts +2 -2
  408. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  409. package/src/tools/host-filesystem/transfer.ts +2 -2
  410. package/src/tools/host-filesystem/write.test.ts +2 -1
  411. package/src/tools/host-filesystem/write.ts +2 -2
  412. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  413. package/src/tools/host-terminal/host-shell.ts +4 -20
  414. package/src/tools/policy-context.ts +6 -7
  415. package/src/tools/subagent/spawn.ts +5 -8
  416. package/src/tools/terminal/__tests__/safe-env.test.ts +6 -6
  417. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  418. package/src/tools/terminal/safe-env.ts +23 -3
  419. package/src/tools/terminal/sanitized-bash.ts +105 -0
  420. package/src/tools/watch/watch-retro-report.ts +0 -8
  421. package/src/util/__tests__/language-subtag.test.ts +23 -1
  422. package/src/util/ca-bundle.ts +13 -0
  423. package/src/util/language-subtag.ts +16 -0
  424. package/src/util/platform.ts +8 -0
  425. package/src/workspace/git-porcelain.ts +66 -0
  426. package/src/workspace/git-service.ts +5 -54
  427. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  428. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  429. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  430. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  431. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  432. package/src/workspace/migrations/registry.ts +6 -0
  433. package/src/__tests__/app-source-watcher.test.ts +0 -218
  434. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  435. package/src/__tests__/speaker-identification.test.ts +0 -56
  436. package/src/calls/speaker-identification.ts +0 -233
  437. package/src/daemon/app-source-watcher.ts +0 -218
  438. package/src/messaging/providers/whatsapp/client.ts +0 -29
  439. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  440. package/src/util/fs-watcher-error.ts +0 -36
  441. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -37,6 +37,7 @@ export const acpSpawnInputSchema = z.looseObject({
37
37
  agent: nullAsOmitted(z.string()),
38
38
  task: nullAsOmitted(z.string()),
39
39
  cwd: nullAsOmitted(z.string()),
40
+ model: nullAsOmitted(z.string()),
40
41
  });
41
42
 
42
43
  /**
@@ -66,6 +67,7 @@ export async function executeAcpSpawn(
66
67
  }
67
68
  const agent = parsedInput.data.agent || "claude";
68
69
  const task = parsedInput.data.task;
70
+ const model = parsedInput.data.model;
69
71
 
70
72
  if (!task) {
71
73
  return { content: '"task" is required.', isError: true };
@@ -124,14 +126,20 @@ export async function executeAcpSpawn(
124
126
  // Recheck: the auto-install and the agent-env preparation above are both
125
127
  // awaits, and this is the point a long-lived subprocess starts.
126
128
  throwIfCancelled(context);
127
- const { acpSessionId, protocolSessionId } = await manager.spawn(
129
+ const {
130
+ acpSessionId,
131
+ protocolSessionId,
132
+ requestedModel,
133
+ effectiveModel,
134
+ modelWarning,
135
+ } = await manager.spawn(
128
136
  agent,
129
137
  agentConfig,
130
138
  task,
131
139
  cwd,
132
140
  context.conversationId,
133
141
  sendToClient,
134
- context.toolUseId,
142
+ { parentToolUseId: context.toolUseId, model },
135
143
  // The manager rechecks after its own protocol handshake and session
136
144
  // creation, so a turn stopped in that window tears the child process
137
145
  // down instead of handing it the task.
@@ -150,16 +158,26 @@ export async function executeAcpSpawn(
150
158
  const installNote = autoInstalledPackage
151
159
  ? ` Installed ${autoInstalledPackage} automatically.`
152
160
  : "";
161
+ // Relayed verbatim: the warning already distinguishes an adapter with no
162
+ // selector from one that refused the value.
163
+ const modelNote = modelWarning
164
+ ? ` The requested model was not applied: ${modelWarning}`
165
+ : "";
166
+ const effectiveModelNote = effectiveModel
167
+ ? ` The top-level ACP session reports "${effectiveModel}" as its effective model.`
168
+ : " The top-level ACP session did not report an effective model.";
153
169
  const payload = JSON.stringify({
154
170
  acpSessionId,
155
171
  protocolSessionId,
156
172
  agent,
157
173
  cwd,
174
+ requestedModel: requestedModel ?? null,
175
+ effectiveModel: effectiveModel ?? null,
158
176
  status: "running",
159
177
  message:
160
178
  `ACP agent "${agent}" spawned (session: ${protocolSessionId}). ` +
161
179
  `Results stream back via SSE. You will be notified when it completes.` +
162
- `${installNote}${resumeHint}`,
180
+ `${installNote}${modelNote}${effectiveModelNote}${resumeHint}`,
163
181
  });
164
182
 
165
183
  return { content: payload, isError: false };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Tests for the `acp_status` tool's projection of session state.
3
+ *
4
+ * The state the manager tracks feeds three surfaces with different budgets:
5
+ * the HTTP route, the SSE picker, and this tool, whose output is spent on
6
+ * LLM context. Only the fields an agent can act on belong here.
7
+ */
8
+
9
+ import { describe, expect, mock, test } from "bun:test";
10
+
11
+ import type { AcpSessionState } from "../../acp/types.js";
12
+ import type { ToolContext } from "../types.js";
13
+
14
+ const RUNNING_STATE: AcpSessionState = {
15
+ id: "acp-1",
16
+ agentId: "claude",
17
+ acpSessionId: "proto-1",
18
+ parentConversationId: "conv-1",
19
+ status: "running",
20
+ startedAt: 1234,
21
+ task: "refactor the parser",
22
+ model: "claude-opus-4-5",
23
+ availableModels: [
24
+ { value: "sonnet", label: "Sonnet" },
25
+ { value: "opus", label: "Opus", description: "Most capable" },
26
+ ],
27
+ };
28
+
29
+ let statusResult: AcpSessionState | AcpSessionState[] = RUNNING_STATE;
30
+
31
+ const realAcpModule = await import("../../acp/index.js");
32
+ mock.module("../../acp/index.js", () => ({
33
+ ...realAcpModule,
34
+ getAcpSessionManager: () => ({ getStatus: () => statusResult }),
35
+ }));
36
+
37
+ const { executeAcpStatus } = await import("./status.js");
38
+
39
+ const context = {
40
+ conversationId: "conv-1",
41
+ workingDir: "/tmp",
42
+ } as unknown as ToolContext;
43
+
44
+ describe("executeAcpStatus", () => {
45
+ test("a single session reports its model but not the picker", async () => {
46
+ statusResult = RUNNING_STATE;
47
+
48
+ const result = await executeAcpStatus({ acp_session_id: "acp-1" }, context);
49
+
50
+ expect(result.isError).toBe(false);
51
+ const payload = JSON.parse(result.content);
52
+ expect(payload).toEqual({
53
+ id: "acp-1",
54
+ agentId: "claude",
55
+ acpSessionId: "proto-1",
56
+ parentConversationId: "conv-1",
57
+ status: "running",
58
+ startedAt: 1234,
59
+ task: "refactor the parser",
60
+ model: "claude-opus-4-5",
61
+ });
62
+ expect(result.content).not.toContain("Most capable");
63
+ });
64
+
65
+ test("the listing drops the picker from every entry", async () => {
66
+ statusResult = [
67
+ RUNNING_STATE,
68
+ { ...RUNNING_STATE, id: "acp-2", model: "sonnet" },
69
+ ];
70
+
71
+ const result = await executeAcpStatus({}, context);
72
+
73
+ const payload = JSON.parse(result.content) as Array<
74
+ Record<string, unknown>
75
+ >;
76
+ expect(payload).toHaveLength(2);
77
+ expect(payload.map((entry) => entry.model)).toEqual([
78
+ "claude-opus-4-5",
79
+ "sonnet",
80
+ ]);
81
+ for (const entry of payload) {
82
+ expect(entry).not.toHaveProperty("availableModels");
83
+ }
84
+ });
85
+
86
+ test("an empty listing says so rather than rendering an empty array", async () => {
87
+ statusResult = [];
88
+
89
+ const result = await executeAcpStatus({}, context);
90
+
91
+ expect(result.content).toBe("No ACP sessions found.");
92
+ expect(result.isError).toBe(false);
93
+ });
94
+ });
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  import { getAcpSessionManager } from "../../acp/index.js";
4
+ import type { AcpSessionState } from "../../acp/types.js";
4
5
  import {
5
6
  invalidToolInputResult,
6
7
  nullAsOmitted,
@@ -16,6 +17,40 @@ export const acpStatusInputSchema = z.looseObject({
16
17
  acp_session_id: nullAsOmitted(z.string()),
17
18
  });
18
19
 
20
+ /**
21
+ * The session fields worth spending LLM context on. An allowlist rather than
22
+ * an omission: `availableModels` is a picker for the human surfaces (HTTP and
23
+ * SSE) that says nothing the agent can act on, and a field added to
24
+ * `AcpSessionState` for those surfaces should not reach this one by default.
25
+ * `model` stays: which model a session is running on is answerable.
26
+ */
27
+ function projectSession(state: AcpSessionState) {
28
+ return {
29
+ id: state.id,
30
+ agentId: state.agentId,
31
+ acpSessionId: state.acpSessionId,
32
+ parentConversationId: state.parentConversationId,
33
+ status: state.status,
34
+ startedAt: state.startedAt,
35
+ completedAt: state.completedAt,
36
+ error: state.error,
37
+ stopReason: state.stopReason,
38
+ task: state.task,
39
+ parentToolUseId: state.parentToolUseId,
40
+ authErrorCode: state.authErrorCode,
41
+ authErrorCredential: state.authErrorCredential,
42
+ latestUsage: state.latestUsage,
43
+ model: state.model,
44
+ };
45
+ }
46
+
47
+ /** Projects either shape `getStatus` answers with. */
48
+ function projectStatus(status: AcpSessionState | AcpSessionState[]): unknown {
49
+ return Array.isArray(status)
50
+ ? status.map(projectSession)
51
+ : projectSession(status);
52
+ }
53
+
19
54
  export async function executeAcpStatus(
20
55
  input: Record<string, unknown>,
21
56
  _context: ToolContext,
@@ -29,9 +64,8 @@ export async function executeAcpStatus(
29
64
 
30
65
  try {
31
66
  if (acpSessionId) {
32
- const state = manager.getStatus(acpSessionId);
33
67
  return {
34
- content: JSON.stringify(state),
68
+ content: JSON.stringify(projectStatus(manager.getStatus(acpSessionId))),
35
69
  isError: false,
36
70
  };
37
71
  }
@@ -42,7 +76,10 @@ export async function executeAcpStatus(
42
76
  return { content: "No ACP sessions found.", isError: false };
43
77
  }
44
78
 
45
- return { content: JSON.stringify(allStates), isError: false };
79
+ return {
80
+ content: JSON.stringify(projectStatus(allStates)),
81
+ isError: false,
82
+ };
46
83
  } catch (err) {
47
84
  const msg = err instanceof Error ? err.message : String(err);
48
85
  return { content: msg, isError: true };
@@ -5,6 +5,7 @@
5
5
  * the proxy resolver, which forwards the call to the connected client.
6
6
  */
7
7
 
8
+ import { formatDesktopAppRequired } from "../capability-offer.js";
8
9
  import { throwIfCancelled } from "../shared/abort.js";
9
10
  import type { ToolContext, ToolExecutionResult } from "../types.js";
10
11
 
@@ -32,7 +33,7 @@ export function forwardAppControlProxyTool(
32
33
  }
33
34
  if (!context.proxyToolResolver) {
34
35
  return Promise.resolve({
35
- content: `Cannot execute ${toolName}: no proxy resolver available. This tool requires a connected client.`,
36
+ content: formatDesktopAppRequired("apps"),
36
37
  isError: true,
37
38
  });
38
39
  }
@@ -4,11 +4,26 @@ import {
4
4
  appendLoggedInBrowserOffer,
5
5
  CHROME_WEB_STORE_INSTALL_URL,
6
6
  DESKTOP_APP_DOWNLOAD_URL,
7
+ formatDesktopAppRequired,
7
8
  formatLoggedInBrowserOffer,
8
9
  isPhoneSurface,
9
10
  shouldOfferLoggedInBrowser,
10
11
  } from "./capability-offer.js";
11
12
 
13
+ describe("formatDesktopAppRequired", () => {
14
+ test("offers a direct desktop-app download and retry step", () => {
15
+ const message = formatDesktopAppRequired("shell");
16
+
17
+ expect(message).toContain(
18
+ "The Vellum desktop app is needed to run commands on your computer",
19
+ );
20
+ expect(message).toContain(DESKTOP_APP_DOWNLOAD_URL);
21
+ expect(message).toContain("Open it, then retry.");
22
+ expect(message).not.toContain("client");
23
+ expect(message).not.toContain("daemon");
24
+ });
25
+ });
26
+
12
27
  describe("isPhoneSurface", () => {
13
28
  test("is true for iOS transport and mobile client OS", () => {
14
29
  expect(isPhoneSurface({ transportInterface: "ios" })).toBe(true);
@@ -49,7 +64,9 @@ describe("shouldOfferLoggedInBrowser", () => {
49
64
  expect(shouldOfferLoggedInBrowser('Invalid browser_mode "bogus"')).toBe(
50
65
  false,
51
66
  );
52
- expect(shouldOfferLoggedInBrowser("url must use http or https")).toBe(false);
67
+ expect(shouldOfferLoggedInBrowser("url must use http or https")).toBe(
68
+ false,
69
+ );
53
70
  });
54
71
  });
55
72
 
@@ -71,6 +88,7 @@ describe("formatLoggedInBrowserOffer", () => {
71
88
  });
72
89
  expect(offer).toContain("no in-app browser");
73
90
  expect(offer).toContain("Do not describe a browser panel");
91
+ expect(offer).toContain("Mac or Windows PC");
74
92
  expect(offer).toContain(DESKTOP_APP_DOWNLOAD_URL);
75
93
  expect(offer).toContain(CHROME_WEB_STORE_INSTALL_URL);
76
94
  });
@@ -78,7 +96,9 @@ describe("formatLoggedInBrowserOffer", () => {
78
96
 
79
97
  describe("appendLoggedInBrowserOffer", () => {
80
98
  test("appends the offer once", () => {
81
- const once = appendLoggedInBrowserOffer("Error: Navigation failed: timeout");
99
+ const once = appendLoggedInBrowserOffer(
100
+ "Error: Navigation failed: timeout",
101
+ );
82
102
  expect(once).toContain("Error: Navigation failed: timeout");
83
103
  expect(once).toContain(DESKTOP_APP_DOWNLOAD_URL);
84
104
  const twice = appendLoggedInBrowserOffer(once);
@@ -7,7 +7,22 @@ export const DESKTOP_APP_DOWNLOAD_URL = "https://www.vellum.ai/downloads";
7
7
  export const CHROME_WEB_STORE_INSTALL_URL =
8
8
  "https://chromewebstore.google.com/detail/vellum-assistant-browser/hphbdmpffeigpcdjkckleobjmhhokpne";
9
9
 
10
- export const DESKTOP_APP_INSTALL_HINT = `Install the Vellum desktop app: ${DESKTOP_APP_DOWNLOAD_URL}`;
10
+ export const DESKTOP_APP_INSTALL_HINT = `Download the Vellum desktop app: ${DESKTOP_APP_DOWNLOAD_URL}. Open it, then retry.`;
11
+
12
+ export type DesktopAppCapability = "files" | "screen" | "shell" | "apps";
13
+
14
+ const DESKTOP_APP_CAPABILITY_ACTION: Record<DesktopAppCapability, string> = {
15
+ files: "access files on your computer",
16
+ screen: "view or control your screen",
17
+ shell: "run commands on your computer",
18
+ apps: "control apps on your computer",
19
+ };
20
+
21
+ export function formatDesktopAppRequired(
22
+ capability: DesktopAppCapability,
23
+ ): string {
24
+ return `Error: The Vellum desktop app is needed to ${DESKTOP_APP_CAPABILITY_ACTION[capability]}. ${DESKTOP_APP_INSTALL_HINT}`;
25
+ }
11
26
 
12
27
  /** User-facing install step for status userActions and command-failure hints. */
13
28
  export const CHROME_EXTENSION_INSTALL_HINT = `Install the Vellum Assistant Chrome extension from the Chrome Web Store: ${CHROME_WEB_STORE_INSTALL_URL}`;
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { RiskLevel } from "../../permissions/types.js";
11
+ import { formatDesktopAppRequired } from "../capability-offer.js";
11
12
  import type {
12
13
  ToolContext,
13
14
  ToolDefinition,
@@ -31,7 +32,7 @@ function proxyExecute(toolName: string) {
31
32
  ): Promise<ToolExecutionResult> => {
32
33
  if (!context.proxyToolResolver) {
33
34
  return {
34
- content: `No proxy resolver configured for proxy tool "${toolName}". This tool requires a connected desktop client.`,
35
+ content: formatDesktopAppRequired("screen"),
35
36
  isError: true,
36
37
  };
37
38
  }
@@ -456,7 +457,8 @@ export const computerUseObserveTool = {
456
457
  type: "integer",
457
458
  minimum: 1,
458
459
  maximum: 4294967295,
459
- description: "macOS only: capture this native CGWindowID instead of the desktop, including only its accessibility tree. Obtain a current native window ID first; do not guess or use a browser tab ID. Applies to this observation only, not subsequent actions. Requires a desktop helper with window-capture support. Screenshot coordinates are window-relative; use accessibility element IDs for later actions, not desktop scaling.",
460
+ description:
461
+ "macOS only: capture this native CGWindowID instead of the desktop, including only its accessibility tree. Obtain a current native window ID first; do not guess or use a browser tab ID. Applies to this observation only, not subsequent actions. Requires a desktop helper with window-capture support. Screenshot coordinates are window-relative; use accessibility element IDs for later actions, not desktop scaling.",
460
462
  },
461
463
  target_client_id: {
462
464
  type: "string",
@@ -5,6 +5,7 @@
5
5
  * the proxy resolver, which forwards the call to the connected desktop client.
6
6
  */
7
7
 
8
+ import { formatDesktopAppRequired } from "../capability-offer.js";
8
9
  import { throwIfCancelled } from "../shared/abort.js";
9
10
  import type { ToolContext, ToolExecutionResult } from "../types.js";
10
11
 
@@ -45,7 +46,7 @@ export function forwardComputerUseProxyTool(
45
46
  }
46
47
  if (!context.proxyToolResolver) {
47
48
  return Promise.resolve({
48
- content: `Cannot execute ${toolName}: no proxy resolver available. This tool requires a connected desktop client.`,
49
+ content: formatDesktopAppRequired("screen"),
49
50
  isError: true,
50
51
  });
51
52
  }
@@ -20,6 +20,7 @@ import {
20
20
  } from "@vellumai/credential-storage";
21
21
  import type { CredentialRecord } from "@vellumai/service-contracts/credential-rpc";
22
22
 
23
+ import { createCesHttpRecordBackendIfConfigured } from "../../security/ces-http-record-backend.js";
23
24
  import type { CredentialRecordBackend } from "../../security/ces-rpc-record-backend.js";
24
25
  import { getLogger } from "../../util/logger.js";
25
26
  import type { CredentialInjectionTemplate } from "./policy-types.js";
@@ -55,6 +56,11 @@ const CES_MEMORY_PATH = "ces-in-memory-credential-metadata";
55
56
  let _store: StaticCredentialMetadataStore | undefined;
56
57
  let _overridePath: string | null = null;
57
58
  let _recordBackend: CredentialRecordBackend | undefined;
59
+ /**
60
+ * Test override for the CES HTTP listing fallback. `undefined` uses the
61
+ * production factory. `null` disables HTTP fallback.
62
+ */
63
+ let _httpRecordBackendOverride: CredentialRecordBackend | null | undefined;
58
64
  let _cesRecordsLoaded = false;
59
65
  let _cesLoad: Promise<void> | undefined;
60
66
  let _cesLoadGeneration = 0;
@@ -255,6 +261,93 @@ export function listCredentialMetadata(): CredentialMetadata[] {
255
261
  return getStore().list() as CredentialMetadata[];
256
262
  }
257
263
 
264
+ export type CredentialRecordListResult = {
265
+ records: CredentialMetadata[];
266
+ unreachable: boolean;
267
+ };
268
+
269
+ type ListedCredentialRecords = Array<{
270
+ account: string;
271
+ record: CredentialRecord;
272
+ }>;
273
+
274
+ function backendIsAvailable(backend: CredentialRecordBackend): boolean {
275
+ try {
276
+ return backend.isAvailable();
277
+ } catch {
278
+ return false;
279
+ }
280
+ }
281
+
282
+ async function listFromRecordBackend(
283
+ backend: CredentialRecordBackend,
284
+ ): Promise<ListedCredentialRecords | null> {
285
+ if (!backendIsAvailable(backend)) {
286
+ return null;
287
+ }
288
+ return backend.list();
289
+ }
290
+
291
+ function resolveHttpRecordBackend(): CredentialRecordBackend | null {
292
+ if (_httpRecordBackendOverride !== undefined) {
293
+ return _httpRecordBackendOverride;
294
+ }
295
+ return createCesHttpRecordBackendIfConfigured();
296
+ }
297
+
298
+ async function listFromHttpFallback(): Promise<ListedCredentialRecords | null> {
299
+ const httpBackend = resolveHttpRecordBackend();
300
+ if (!httpBackend) {
301
+ return null;
302
+ }
303
+ return listFromRecordBackend(httpBackend);
304
+ }
305
+
306
+ /**
307
+ * Live catalog from the credential record backend. Does not read the
308
+ * daemon in-process cache, so an outage cannot be mistaken for an empty
309
+ * vault. `unreachable` is true when the backend is missing, down, or
310
+ * the list RPC fails, and CES HTTP failover is also unavailable.
311
+ *
312
+ * When the attached backend is CES RPC (or none is attached), listing
313
+ * fails over to CES HTTP in containerized deployments so a down RPC
314
+ * socket does not hide a healthy vault sidecar. A successful empty
315
+ * catalog (`[]`) does not fall through to HTTP.
316
+ *
317
+ * The file-backed test store is the catalog when a test override path is set.
318
+ */
319
+ export async function listCredentialRecordsLive(): Promise<CredentialRecordListResult> {
320
+ if (_overridePath) {
321
+ return {
322
+ records: getStore().list() as CredentialMetadata[],
323
+ unreachable: false,
324
+ };
325
+ }
326
+ if (_recordBackend) {
327
+ const remote = await listFromRecordBackend(_recordBackend);
328
+ if (remote !== null) {
329
+ return {
330
+ records: remote.map((entry) => entry.record as CredentialMetadata),
331
+ unreachable: false,
332
+ };
333
+ }
334
+ if (_recordBackend.name !== "ces-rpc") {
335
+ return { records: [], unreachable: true };
336
+ }
337
+ }
338
+ const httpRemote = await listFromHttpFallback();
339
+ if (httpRemote !== null) {
340
+ log.warn(
341
+ "CES RPC credential record list unavailable. Failing over to CES HTTP.",
342
+ );
343
+ return {
344
+ records: httpRemote.map((entry) => entry.record as CredentialMetadata),
345
+ unreachable: false,
346
+ };
347
+ }
348
+ return { records: [], unreachable: true };
349
+ }
350
+
258
351
  /**
259
352
  * Delete metadata for a credential.
260
353
  */
@@ -287,3 +380,10 @@ export function _setMetadataPath(path: string | null): void {
287
380
  }
288
381
  }
289
382
  }
383
+
384
+ /** @internal Test-only: override the CES HTTP listing fallback. */
385
+ export function _setHttpRecordBackendForTests(
386
+ backend: CredentialRecordBackend | null | undefined,
387
+ ): void {
388
+ _httpRecordBackendOverride = backend;
389
+ }
@@ -71,8 +71,9 @@ describe("host_file_edit cross-client guards", () => {
71
71
  );
72
72
  expect(result.isError).toBe(true);
73
73
  expect(result.content).toContain(
74
- "no client with host_file capability is connected",
74
+ "The Vellum desktop app is needed to access files on your computer",
75
75
  );
76
+ expect(result.content).toContain("https://www.vellum.ai/downloads");
76
77
  });
77
78
 
78
79
  test("returns 'specified client disconnected' error when targetClientId set but proxy unavailable on web", async () => {
@@ -4,7 +4,7 @@ import { supportsHostProxy } from "../../channels/types.js";
4
4
  import { HostFileProxy } from "../../daemon/host-file-proxy.js";
5
5
  import { RiskLevel } from "../../permissions/types.js";
6
6
  import { assistantEventHub } from "../../runtime/assistant-event-hub.js";
7
- import { desktopClientName } from "../client-os.js";
7
+ import { formatDesktopAppRequired } from "../capability-offer.js";
8
8
  import { FileSystemOps } from "../shared/filesystem/file-ops-service.js";
9
9
  import { formatEditDiff } from "../shared/filesystem/format-diff.js";
10
10
  import { hostPolicy } from "../shared/filesystem/path-policy.js";
@@ -115,7 +115,7 @@ export const hostFileEditTool = {
115
115
  !HostFileProxy.instance.isAvailable()
116
116
  ) {
117
117
  return {
118
- content: `Error: no client with host_file capability is connected. Connect a ${desktopClientName(context)} client to use host_file from a non-desktop interface.`,
118
+ content: formatDesktopAppRequired("files"),
119
119
  isError: true,
120
120
  };
121
121
  }
@@ -67,8 +67,9 @@ describe("host_file_read cross-client guards", () => {
67
67
  );
68
68
  expect(result.isError).toBe(true);
69
69
  expect(result.content).toContain(
70
- "no client with host_file capability is connected",
70
+ "The Vellum desktop app is needed to access files on your computer",
71
71
  );
72
+ expect(result.content).toContain("https://www.vellum.ai/downloads");
72
73
  });
73
74
 
74
75
  test("returns 'specified client disconnected' error when targetClientId set but proxy unavailable on web", async () => {
@@ -6,7 +6,7 @@ import { supportsHostProxy } from "../../channels/types.js";
6
6
  import { HostFileProxy } from "../../daemon/host-file-proxy.js";
7
7
  import { RiskLevel } from "../../permissions/types.js";
8
8
  import { assistantEventHub } from "../../runtime/assistant-event-hub.js";
9
- import { desktopClientName } from "../client-os.js";
9
+ import { formatDesktopAppRequired } from "../capability-offer.js";
10
10
  import {
11
11
  AUDIO_EXTENSIONS,
12
12
  readAudioFile,
@@ -121,7 +121,7 @@ export const hostFileReadTool = {
121
121
  !HostFileProxy.instance.isAvailable()
122
122
  ) {
123
123
  return {
124
- content: `Error: no client with host_file capability is connected. Connect a ${desktopClientName(context)} client to use host_file from a non-desktop interface.`,
124
+ content: formatDesktopAppRequired("files"),
125
125
  isError: true,
126
126
  };
127
127
  }
@@ -335,8 +335,9 @@ describe("host_file_transfer cross-client guards", () => {
335
335
 
336
336
  expect(result.isError).toBe(true);
337
337
  expect(result.content).toContain(
338
- "no client with host_file capability is connected",
338
+ "The Vellum desktop app is needed to access files on your computer",
339
339
  );
340
+ expect(result.content).toContain("https://www.vellum.ai/downloads");
340
341
  expect(toSandboxCalls.length).toBe(0);
341
342
  });
342
343
 
@@ -6,7 +6,7 @@ import { supportsHostProxy } from "../../channels/types.js";
6
6
  import { HostTransferProxy } from "../../daemon/host-transfer-proxy.js";
7
7
  import { RiskLevel } from "../../permissions/types.js";
8
8
  import { assistantEventHub } from "../../runtime/assistant-event-hub.js";
9
- import { desktopClientName } from "../client-os.js";
9
+ import { formatDesktopAppRequired } from "../capability-offer.js";
10
10
  import { declareDaemonActivityField } from "../schema-transforms.js";
11
11
  import { sandboxPolicy } from "../shared/filesystem/path-policy.js";
12
12
  import type {
@@ -126,7 +126,7 @@ export const hostFileTransferTool = {
126
126
  !HostTransferProxy.instance.isAvailable()
127
127
  ) {
128
128
  return {
129
- content: `Error: no client with host_file capability is connected. Connect a ${desktopClientName(context)} client to use host_file from a non-desktop interface.`,
129
+ content: formatDesktopAppRequired("files"),
130
130
  isError: true,
131
131
  };
132
132
  }
@@ -67,8 +67,9 @@ describe("host_file_write cross-client guards", () => {
67
67
  );
68
68
  expect(result.isError).toBe(true);
69
69
  expect(result.content).toContain(
70
- "no client with host_file capability is connected",
70
+ "The Vellum desktop app is needed to access files on your computer",
71
71
  );
72
+ expect(result.content).toContain("https://www.vellum.ai/downloads");
72
73
  });
73
74
 
74
75
  test("returns 'specified client disconnected' error when targetClientId set but proxy unavailable on web", async () => {
@@ -4,7 +4,7 @@ import { supportsHostProxy } from "../../channels/types.js";
4
4
  import { HostFileProxy } from "../../daemon/host-file-proxy.js";
5
5
  import { RiskLevel } from "../../permissions/types.js";
6
6
  import { assistantEventHub } from "../../runtime/assistant-event-hub.js";
7
- import { desktopClientName } from "../client-os.js";
7
+ import { formatDesktopAppRequired } from "../capability-offer.js";
8
8
  import { FileSystemOps } from "../shared/filesystem/file-ops-service.js";
9
9
  import { formatWriteSummary } from "../shared/filesystem/format-diff.js";
10
10
  import { hostPolicy } from "../shared/filesystem/path-policy.js";
@@ -90,7 +90,7 @@ export const hostFileWriteTool = {
90
90
  !HostFileProxy.instance.isAvailable()
91
91
  ) {
92
92
  return {
93
- content: `Error: no client with host_file capability is connected. Connect a ${desktopClientName(context)} client to use host_file from a non-desktop interface.`,
93
+ content: formatDesktopAppRequired("files"),
94
94
  isError: true,
95
95
  };
96
96
  }
@@ -170,6 +170,22 @@ afterEach(() => {
170
170
  mockProxyAvailable = false;
171
171
  });
172
172
 
173
+ describe("host_bash unavailable desktop-app recovery", () => {
174
+ test("offers the desktop app download when a web turn has no host connection", async () => {
175
+ const result = await hostShellTool.execute(
176
+ { command: "pwd" },
177
+ makeContext({ transportInterface: "web" }),
178
+ );
179
+
180
+ expect(result.isError).toBe(true);
181
+ expect(result.content).toContain(
182
+ "The Vellum desktop app is needed to run commands on your computer",
183
+ );
184
+ expect(result.content).toContain("https://www.vellum.ai/downloads");
185
+ expect(result.content).toContain("Open it, then retry.");
186
+ });
187
+ });
188
+
173
189
  // ---------------------------------------------------------------------------
174
190
  // Proxy path — background: true
175
191
  // ---------------------------------------------------------------------------