@vellumai/assistant 0.11.3 → 0.11.4-staging.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 (341) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/docs/architecture/memory.md +11 -0
  3. package/docs/architecture/turn-actor.md +70 -0
  4. package/docs/flux-turn-detection-spike.md +243 -0
  5. package/docs/stt-provider-onboarding.md +3 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  8. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  10. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  11. package/openapi.yaml +139 -37
  12. package/package.json +1 -1
  13. package/scripts/voice-ttft-spike.ts +3 -3
  14. package/src/__tests__/app-compiler.test.ts +38 -3
  15. package/src/__tests__/attachments-store.test.ts +21 -12
  16. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  17. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  18. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  19. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  20. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  21. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  22. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  23. package/src/__tests__/config-schema.test.ts +25 -10
  24. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  26. package/src/__tests__/conversation-agent-loop.test.ts +28 -20
  27. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  28. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  29. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  30. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  31. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  32. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  33. package/src/__tests__/conversation-queue.test.ts +177 -4
  34. package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
  35. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  36. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  37. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  38. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  39. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  40. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  41. package/src/__tests__/edit-propagation.test.ts +0 -7
  42. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  43. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  44. package/src/__tests__/injector-chain.test.ts +63 -41
  45. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  46. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  47. package/src/__tests__/llm-schema.test.ts +5 -2
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  49. package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
  50. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  51. package/src/__tests__/mtime-cache.test.ts +1 -1
  52. package/src/__tests__/non-member-access-request.test.ts +0 -20
  53. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
  55. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  56. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  57. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  58. package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
  59. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  60. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  61. package/src/__tests__/skills.test.ts +32 -0
  62. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  63. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  64. package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
  65. package/src/__tests__/turn-events-store.test.ts +43 -0
  66. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  67. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  68. package/src/__tests__/user-plugin-loader.test.ts +1 -1
  69. package/src/__tests__/visible-app-context.test.ts +16 -9
  70. package/src/__tests__/voice-config-update.test.ts +40 -0
  71. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  72. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  73. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  74. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  75. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  76. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  77. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  78. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  79. package/src/acp/__tests__/auth-required.test.ts +161 -0
  80. package/src/acp/acp-claude-oauth.ts +19 -2
  81. package/src/acp/agent-process.test.ts +100 -0
  82. package/src/acp/agent-process.ts +29 -26
  83. package/src/acp/auth-required.ts +102 -0
  84. package/src/acp/session-manager.test.ts +119 -0
  85. package/src/acp/session-manager.ts +68 -2
  86. package/src/api/events/acp-auth-required.ts +55 -0
  87. package/src/api/index.ts +7 -0
  88. package/src/api/surfaces.ts +7 -3
  89. package/src/apps/app-store.ts +3 -0
  90. package/src/bundler/package-resolver.ts +2 -30
  91. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  92. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  93. package/src/calls/call-controller.ts +19 -3
  94. package/src/calls/call-setup-flow.ts +0 -1
  95. package/src/calls/media-stream-stt-session.ts +15 -0
  96. package/src/calls/voice-session-bridge.ts +71 -16
  97. package/src/calls/voice-triage-escalate.ts +104 -2
  98. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  99. package/src/channels/config.ts +13 -0
  100. package/src/channels/plugin-channel-declarations.ts +108 -0
  101. package/src/channels/types.ts +30 -0
  102. package/src/cli/AGENTS.md +5 -2
  103. package/src/cli/commands/credentials.help.ts +2 -2
  104. package/src/cli/commands/inference-providers.ts +1 -1
  105. package/src/cli/commands/mcp.help.ts +13 -4
  106. package/src/cli/commands/mcp.ts +9 -0
  107. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  108. package/src/cli/commands/memory/index.help.ts +43 -1
  109. package/src/cli/commands/memory/memory-v3.ts +64 -0
  110. package/src/cli/commands/stt.help.ts +27 -2
  111. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  112. package/src/cli/lib/bundled-marketplace.json +13 -0
  113. package/src/cli/lib/upgrade-plugin.ts +42 -0
  114. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  115. package/src/config/__tests__/default-provider.test.ts +6 -1
  116. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  117. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  118. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  119. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  120. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  121. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  122. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  123. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  124. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  125. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  126. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  127. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  128. package/src/config/call-site-defaults.ts +4 -5
  129. package/src/config/default-profile-catalog.ts +83 -12
  130. package/src/config/default-profile-names.ts +4 -1
  131. package/src/config/default-provider-resolution.ts +4 -0
  132. package/src/config/llm-context-resolution.ts +11 -3
  133. package/src/config/llm-resolver.ts +28 -1
  134. package/src/config/profile-materialization.ts +70 -22
  135. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  136. package/src/config/schemas/call-site-catalog.ts +4 -4
  137. package/src/config/schemas/live-voice.ts +57 -23
  138. package/src/config/schemas/llm.ts +59 -32
  139. package/src/config/schemas/mcp.ts +23 -0
  140. package/src/config/schemas/plugin-updates.ts +6 -2
  141. package/src/config/schemas/stt.ts +1 -0
  142. package/src/context/outbound-sanitize.ts +96 -1
  143. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  144. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  145. package/src/daemon/conversation-agent-loop.ts +17 -6
  146. package/src/daemon/conversation-messaging.ts +5 -1
  147. package/src/daemon/conversation-notifiers.ts +9 -1
  148. package/src/daemon/conversation-process.ts +36 -6
  149. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  150. package/src/daemon/conversation-surfaces.ts +27 -5
  151. package/src/daemon/conversation-tool-setup.ts +1 -2
  152. package/src/daemon/conversation.ts +48 -0
  153. package/src/daemon/interactive-turn-sender.ts +59 -0
  154. package/src/daemon/mcp-reload-service.ts +36 -6
  155. package/src/daemon/process-message.ts +24 -24
  156. package/src/daemon/providers-setup.ts +6 -3
  157. package/src/daemon/trust-context-types.ts +29 -0
  158. package/src/daemon/wake-conversation-ops.ts +3 -2
  159. package/src/documents/document-store.ts +138 -5
  160. package/src/hooks/hook-loader.ts +3 -3
  161. package/src/hooks/registry.ts +50 -6
  162. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  163. package/src/inbound/oauth-callback-url.ts +61 -0
  164. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  165. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  166. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  167. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  168. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  169. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  170. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  171. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  172. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  173. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  174. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  175. package/src/live-voice/live-voice-archive.ts +2 -0
  176. package/src/live-voice/live-voice-metrics.ts +57 -32
  177. package/src/live-voice/live-voice-photo.ts +1 -2
  178. package/src/live-voice/live-voice-session.ts +535 -314
  179. package/src/live-voice/progress-narration.ts +277 -0
  180. package/src/live-voice/protocol.ts +21 -1
  181. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  182. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  183. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  184. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  185. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  186. package/src/mcp/client.ts +16 -11
  187. package/src/mcp/effective-config.ts +113 -0
  188. package/src/mcp/manager.ts +11 -6
  189. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  190. package/src/mcp/mcp-oauth-provider.ts +205 -240
  191. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  192. package/src/monitoring/plugin-auto-update.ts +128 -24
  193. package/src/notifications/signal.ts +1 -0
  194. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  195. package/src/permissions/confirmation-guardian-request.ts +2 -2
  196. package/src/permissions/question-guardian-request.test.ts +14 -6
  197. package/src/permissions/question-guardian-request.ts +1 -2
  198. package/src/persistence/attachments-store.ts +8 -1
  199. package/src/persistence/bookmark-crud.ts +3 -7
  200. package/src/persistence/conversation-attention-store.ts +16 -45
  201. package/src/persistence/conversation-crud.ts +33 -4
  202. package/src/persistence/conversation-lineage.ts +9 -0
  203. package/src/persistence/conversation-queries.ts +108 -41
  204. package/src/persistence/delivery-crud.ts +38 -29
  205. package/src/persistence/external-conversation-store.ts +32 -4
  206. package/src/persistence/llm-request-log-store.ts +4 -10
  207. package/src/persistence/llm-usage-store.ts +8 -3
  208. package/src/persistence/message-reads.test.ts +197 -0
  209. package/src/persistence/message-reads.ts +211 -0
  210. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  211. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  212. package/src/persistence/real-user-turn-filter.ts +27 -3
  213. package/src/persistence/steps.ts +9 -0
  214. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  215. package/src/plugin-api/conversation-turn.ts +168 -5
  216. package/src/plugin-api/index.ts +21 -5
  217. package/src/plugin-api/vision-support.test.ts +1 -1
  218. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  219. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  220. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  224. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  225. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  226. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  227. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  228. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  229. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  230. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  231. package/src/plugins/defaults/memory/indexer.ts +3 -1
  232. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  233. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  234. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  238. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  239. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  240. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  241. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  242. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  243. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  244. package/src/plugins/defaults/memory/worker.ts +6 -3
  245. package/src/plugins/external-plugin-loader.ts +47 -0
  246. package/src/plugins/mcp-servers.ts +361 -0
  247. package/src/plugins/mtime-cache.ts +23 -49
  248. package/src/plugins/worker-plugin-surface.ts +33 -0
  249. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  250. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  251. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  252. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  253. package/src/providers/call-site-routing.ts +30 -3
  254. package/src/providers/connection-resolution.ts +194 -11
  255. package/src/providers/inference/auth.ts +6 -0
  256. package/src/providers/inference/connection-availability.ts +24 -2
  257. package/src/providers/inference/connections.ts +2 -0
  258. package/src/providers/model-catalog.ts +3 -3
  259. package/src/providers/model-intents.ts +28 -8
  260. package/src/providers/openai/chat-completions-provider.ts +5 -6
  261. package/src/providers/openai/codex-models.ts +2 -1
  262. package/src/providers/provider-send-message.ts +32 -3
  263. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  264. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  265. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  266. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  267. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  268. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  269. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  270. package/src/providers/speech-to-text/resolve.ts +25 -2
  271. package/src/routes/worker.ts +17 -5
  272. package/src/runtime/access-request-helper.ts +9 -12
  273. package/src/runtime/agent-wake.ts +3 -3
  274. package/src/runtime/pre-first-message-gate.ts +4 -0
  275. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  276. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  277. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  278. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  279. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  280. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  281. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  282. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  283. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  284. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  285. package/src/runtime/routes/channel-route-shared.ts +0 -6
  286. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  287. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  288. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  289. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  290. package/src/runtime/routes/default-provider-routes.ts +15 -0
  291. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  292. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  294. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  295. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  296. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  297. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  298. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  299. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  300. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  301. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  302. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  303. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  304. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  305. package/src/runtime/routes/log-export-routes.ts +3 -0
  306. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  307. package/src/runtime/routes/plugins-routes.ts +21 -3
  308. package/src/runtime/routes/stt-routes.ts +31 -25
  309. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  310. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  311. package/src/runtime/routes/user-route-import.ts +108 -0
  312. package/src/schedule/worker.ts +6 -0
  313. package/src/security/oauth2.ts +6 -22
  314. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  315. package/src/stt/__tests__/types.test.ts +94 -0
  316. package/src/stt/daemon-batch-transcriber.ts +10 -0
  317. package/src/stt/stt-stream-session.ts +8 -4
  318. package/src/stt/types.ts +103 -0
  319. package/src/subagent/manager.ts +1 -3
  320. package/src/subagent/types.ts +7 -6
  321. package/src/tools/acp/spawn.test.ts +97 -0
  322. package/src/tools/acp/spawn.ts +32 -0
  323. package/src/tools/document/document-tool.ts +12 -3
  324. package/src/tools/registry.ts +2 -1
  325. package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
  326. package/src/tools/workflows/run-workflow.ts +1 -2
  327. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  328. package/src/tts/reasoning-tag-filter.ts +89 -0
  329. package/src/util/think-tag-stream.ts +95 -0
  330. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  331. package/src/workspace/custom-profile-ensure.ts +4 -24
  332. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  333. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  334. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  335. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  336. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  337. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  338. package/src/workspace/migrations/registry.ts +10 -0
  339. package/src/workspace/provider-commit-message-generator.ts +7 -5
  340. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  341. package/src/live-voice/front-decision.ts +0 -476
@@ -10,6 +10,11 @@
10
10
  * stamp survives restarts;
11
11
  * - only plugins that can actually move reach the daemon — up-to-date and
12
12
  * disabled installs cost it nothing;
13
+ * - only curated marketplace installs are swept: a plugin installed straight
14
+ * from a GitHub URL, or one whose recorded source disagrees with the
15
+ * catalog entry claiming its name, is left for a human to upgrade, and
16
+ * every request the daemon does get carries `marketplaceOnly` so it
17
+ * enforces that boundary against its own inspection;
13
18
  * - one plugin's refusal does not stop the sweep, an unreachable daemon
14
19
  * abandons it without stamping (so it retries), and a timed-out upgrade
15
20
  * abandons it *with* a stamp (so the retry cannot race the handler that
@@ -56,6 +61,25 @@ afterAll(() => {
56
61
  let inspectStatus = new Map<string, string>();
57
62
  let inspectThrowsFor = new Set<string>();
58
63
 
64
+ /** GitHub coordinates, as both the provenance sidecar and the catalog carry them. */
65
+ interface Coordinates {
66
+ /** `owner/repo`. */
67
+ readonly repo: string;
68
+ /** Repo-relative plugin root; absent = repo root. */
69
+ readonly path?: string;
70
+ }
71
+
72
+ /**
73
+ * Provenance `inspectPlugin` reports per plugin name: where the installed copy
74
+ * says it came from, and where the catalog entry claiming that name points.
75
+ * Absent for a plugin that declares neither, which is what an older install
76
+ * with no sidecar looks like.
77
+ */
78
+ let inspectProvenance = new Map<
79
+ string,
80
+ { readonly source?: Coordinates; readonly marketplace?: Coordinates }
81
+ >();
82
+
59
83
  type UpgradeReply = {
60
84
  ok: boolean;
61
85
  result?: { outcome?: string; toCommit?: string };
@@ -68,6 +92,8 @@ let upgradeReply: (name: string) => UpgradeReply = () => ({
68
92
  result: { outcome: "upgraded", toCommit: "abc1234" },
69
93
  });
70
94
  const upgradeCalls: Array<{ name: string; strategy: unknown }> = [];
95
+ /** Full request bodies, for the assertions that care about more than strategy. */
96
+ const upgradeBodies: Array<Record<string, unknown>> = [];
71
97
 
72
98
  const realInspect = await import("../../cli/lib/inspect-plugin.js");
73
99
  const realIpcClient = await import("../../ipc/cli-client.js");
@@ -78,7 +104,20 @@ mock.module("../../cli/lib/inspect-plugin.js", () => ({
78
104
  if (inspectThrowsFor.has(name)) {
79
105
  throw new Error("marketplace unreachable");
80
106
  }
81
- return { name, status: inspectStatus.get(name) ?? "up-to-date" };
107
+ const provenance = inspectProvenance.get(name);
108
+ const source = provenance?.source;
109
+ const marketplace = provenance?.marketplace;
110
+ const [owner = "", repo = ""] = source?.repo.split("/") ?? [];
111
+ return {
112
+ name,
113
+ status: inspectStatus.get(name) ?? "up-to-date",
114
+ local: source
115
+ ? { source: { kind: "github", owner, repo, path: source.path } }
116
+ : null,
117
+ remote: marketplace
118
+ ? { repo: marketplace.repo, path: marketplace.path ?? "" }
119
+ : null,
120
+ };
82
121
  },
83
122
  }));
84
123
 
@@ -86,12 +125,16 @@ mock.module("../../ipc/cli-client.js", () => ({
86
125
  ...realIpcClient,
87
126
  cliIpcCall: async (
88
127
  _method: string,
89
- params: { pathParams: { name: string }; body: { strategy: unknown } },
128
+ params: {
129
+ pathParams: { name: string };
130
+ body: Record<string, unknown>;
131
+ },
90
132
  ) => {
91
133
  upgradeCalls.push({
92
134
  name: params.pathParams.name,
93
135
  strategy: params.body.strategy,
94
136
  });
137
+ upgradeBodies.push(params.body);
95
138
  return upgradeReply(params.pathParams.name);
96
139
  },
97
140
  }));
@@ -110,7 +153,14 @@ function writeConfig(pluginUpdates: Record<string, unknown>): void {
110
153
  /** Materialize an installed plugin and declare what inspect says about it. */
111
154
  function installPlugin(
112
155
  name: string,
113
- opts: { status?: string; disabled?: boolean } = {},
156
+ opts: {
157
+ status?: string;
158
+ disabled?: boolean;
159
+ /** Coordinates the install's provenance sidecar records. */
160
+ source?: Coordinates;
161
+ /** Coordinates the catalog entry claiming this name points at. */
162
+ marketplace?: Coordinates;
163
+ } = {},
114
164
  ): void {
115
165
  const dir = join(PLUGINS_DIR, name);
116
166
  mkdirSync(dir, { recursive: true });
@@ -122,6 +172,12 @@ function installPlugin(
122
172
  writeFileSync(join(dir, ".disabled"), "");
123
173
  }
124
174
  inspectStatus.set(name, opts.status ?? "update-available");
175
+ if (opts.source || opts.marketplace) {
176
+ inspectProvenance.set(name, {
177
+ source: opts.source,
178
+ marketplace: opts.marketplace,
179
+ });
180
+ }
125
181
  }
126
182
 
127
183
  /** Backdate the stamp so the sweep is (or is not) due again. */
@@ -134,11 +190,13 @@ function stampAgedBy(ms: number): void {
134
190
  beforeEach(() => {
135
191
  inspectStatus = new Map();
136
192
  inspectThrowsFor = new Set();
193
+ inspectProvenance = new Map();
137
194
  upgradeReply = () => ({
138
195
  ok: true,
139
196
  result: { outcome: "upgraded", toCommit: "abc1234" },
140
197
  });
141
198
  upgradeCalls.length = 0;
199
+ upgradeBodies.length = 0;
142
200
  rmSync(STAMP, { force: true });
143
201
  rmSync(PLUGINS_DIR, { recursive: true, force: true });
144
202
  mkdirSync(PLUGINS_DIR, { recursive: true });
@@ -203,6 +261,111 @@ describe("plugin auto-update sweep", () => {
203
261
  expect(upgradeCalls.map((c) => c.name)).toEqual(["beta"]);
204
262
  });
205
263
 
264
+ test("a plugin installed straight from a GitHub URL is never swept", async () => {
265
+ writeConfig({ mode: "auto" });
266
+ // No catalog entry claims the name, so its only upgrade target is whatever
267
+ // the recorded upstream ref points at right now. Nobody reviewed that.
268
+ installPlugin("alpha", { status: "not-in-marketplace" });
269
+ installPlugin("beta", { status: "update-available" });
270
+
271
+ const result = await runPluginAutoUpdateSweepIfDue();
272
+
273
+ expect(upgradeCalls.map((c) => c.name)).toEqual(["beta"]);
274
+ expect(result.skippedUntrusted).toEqual(["alpha"]);
275
+ expect(result.failed).toEqual([]);
276
+ });
277
+
278
+ test("every request asks the daemon to enforce the curated boundary itself", async () => {
279
+ writeConfig({ mode: "auto" });
280
+ installPlugin("alpha", { status: "update-available" });
281
+ installPlugin("beta", { status: "unknown-provenance" });
282
+
283
+ await runPluginAutoUpdateSweepIfDue();
284
+
285
+ // The daemon re-inspects before it moves anything, so a catalog entry that
286
+ // disappears after the inspection above would flip it to the direct path.
287
+ // `marketplaceOnly` makes the daemon refuse instead of following the
288
+ // install's mutable ref.
289
+ expect(upgradeBodies).toEqual([
290
+ { strategy: "theirs", marketplaceOnly: true },
291
+ { strategy: "theirs", marketplaceOnly: true },
292
+ ]);
293
+ });
294
+
295
+ test("a whole workspace of untrusted installs stamps instead of retrying every minute", async () => {
296
+ writeConfig({ mode: "auto" });
297
+ installPlugin("alpha", { status: "not-in-marketplace" });
298
+
299
+ const result = await runPluginAutoUpdateSweepIfDue();
300
+
301
+ expect(result.skipped).toBe("no-candidates");
302
+ expect(result.skippedUntrusted).toEqual(["alpha"]);
303
+ expect(upgradeCalls).toEqual([]);
304
+ expect(await runPluginAutoUpdateSweepIfDue()).toMatchObject({
305
+ skipped: "not-due",
306
+ });
307
+ });
308
+
309
+ test("an install whose source disagrees with the catalog is left alone", async () => {
310
+ writeConfig({ mode: "auto" });
311
+ // A direct install sitting on a curated plugin's name: only the name lines
312
+ // up, so the sweep must not swap it for the catalog's code.
313
+ installPlugin("alpha", {
314
+ status: "update-available",
315
+ source: { repo: "someone-else/alpha" },
316
+ marketplace: { repo: "vellum-ai/alpha" },
317
+ });
318
+
319
+ const result = await runPluginAutoUpdateSweepIfDue();
320
+
321
+ expect(upgradeCalls).toEqual([]);
322
+ expect(result.skippedUntrusted).toEqual(["alpha"]);
323
+ });
324
+
325
+ test("an install from the catalog's own repo is upgraded", async () => {
326
+ writeConfig({ mode: "auto" });
327
+ installPlugin("alpha", {
328
+ status: "update-available",
329
+ source: { repo: "vellum-ai/plugins", path: "alpha" },
330
+ marketplace: { repo: "vellum-ai/plugins", path: "alpha" },
331
+ });
332
+
333
+ const result = await runPluginAutoUpdateSweepIfDue();
334
+
335
+ expect(upgradeCalls.map((c) => c.name)).toEqual(["alpha"]);
336
+ expect(result.skippedUntrusted).toEqual([]);
337
+ });
338
+
339
+ test("a matching repo with a different plugin root is left alone", async () => {
340
+ writeConfig({ mode: "auto" });
341
+ // Same monorepo, different directory: not the plugin the catalog pins.
342
+ installPlugin("alpha", {
343
+ status: "update-available",
344
+ source: { repo: "vellum-ai/plugins", path: "experimental/alpha" },
345
+ marketplace: { repo: "vellum-ai/plugins", path: "alpha" },
346
+ });
347
+
348
+ const result = await runPluginAutoUpdateSweepIfDue();
349
+
350
+ expect(upgradeCalls).toEqual([]);
351
+ expect(result.skippedUntrusted).toEqual(["alpha"]);
352
+ });
353
+
354
+ test("an install with no recorded source is still re-pinned to the curated commit", async () => {
355
+ writeConfig({ mode: "auto" });
356
+ // An older or manually-copied copy: nothing about it contradicts the
357
+ // catalog, and the upgrade is what gives it provenance.
358
+ installPlugin("alpha", {
359
+ status: "unknown-provenance",
360
+ marketplace: { repo: "vellum-ai/alpha" },
361
+ });
362
+
363
+ const result = await runPluginAutoUpdateSweepIfDue();
364
+
365
+ expect(upgradeCalls.map((c) => c.name)).toEqual(["alpha"]);
366
+ expect(result.upgraded).toEqual(["alpha"]);
367
+ });
368
+
206
369
  test("a disabled plugin is left alone", async () => {
207
370
  writeConfig({ mode: "auto" });
208
371
  installPlugin("alpha");
@@ -8,6 +8,22 @@
8
8
  * (default hourly), inspects which ones can actually move, and upgrades those
9
9
  * with the configured merge strategy (default `theirs`).
10
10
  *
11
+ * Only plugins that come from the curated marketplace are swept. The catalog
12
+ * pins every entry to an immutable commit that a curator reviewed, so an
13
+ * unattended upgrade can only land code that passed that review. A plugin
14
+ * installed straight from a GitHub URL has no such gate: it tracks a mutable
15
+ * ref (a branch, a tag, or the repo's default branch), so upgrading it means
16
+ * fetching and executing whatever upstream pushed since. That is a decision
17
+ * for a human at a terminal, not for an hourly background sweep, so untrusted
18
+ * installs are filtered out here and left for `assistant plugins upgrade`.
19
+ *
20
+ * The filter is applied twice, in both processes. Inspecting here keeps the
21
+ * daemon from being asked about plugins the sweep would never accept, but the
22
+ * daemon re-inspects before it moves anything, so this side's verdict cannot be
23
+ * the one that matters: a catalog entry that disappears in between would flip
24
+ * the daemon to the direct path. Every request therefore carries
25
+ * `marketplaceOnly`, which the daemon enforces against its own inspection.
26
+ *
11
27
  * The monitor drives it — not the daemon — for the same reason it drives the
12
28
  * plugin source watch and crash recovery: the work is periodic, network-bound,
13
29
  * and must not compete with the daemon's turn loop. Drift detection happens
@@ -30,7 +46,10 @@
30
46
  import { statSync, writeFileSync } from "node:fs";
31
47
  import { join } from "node:path";
32
48
 
33
- import { inspectPlugin } from "../cli/lib/inspect-plugin.js";
49
+ import {
50
+ inspectPlugin,
51
+ type PluginInspection,
52
+ } from "../cli/lib/inspect-plugin.js";
34
53
  import { listInstalledPlugins } from "../cli/lib/list-installed-plugins.js";
35
54
  import { getConfigReadOnly } from "../config/loader.js";
36
55
  import type { PluginUpdatesConfig } from "../config/schemas/plugin-updates.js";
@@ -67,22 +86,20 @@ const STAMP_FILENAME = "plugin-auto-update-last-run-at";
67
86
  /**
68
87
  * Inspection verdicts worth asking the daemon about.
69
88
  *
70
- * `update-available` is the obvious one. `unknown-provenance` (an older or
71
- * manually-copied install with no recorded commit) is included because an
72
- * upgrade re-pins it, which is how it stops being unknown. So is
73
- * `not-in-marketplace`: a plugin installed straight from a GitHub URL has no
74
- * catalog entry to compare against, but it still tracks a ref upstream may
75
- * have moved — only the daemon's upgrade can resolve that, and it no-ops when
76
- * the ref still points at the installed commit.
89
+ * Both resolve to a curated marketplace pin, which is the only revision this
90
+ * sweep is willing to move an install to. `update-available` is the obvious
91
+ * one. `unknown-provenance` (an older or manually-copied install with no
92
+ * recorded commit) is included because an upgrade re-pins it to that same
93
+ * curated commit, which is how it stops being unknown.
77
94
  *
78
- * Everything else is skipped: `up-to-date` has nowhere to move, and
79
- * `remote-unavailable` means the catalog could not be read, so an upgrade
80
- * would fail on the same outage a moment later.
95
+ * Everything else is skipped: `up-to-date` has nowhere to move,
96
+ * `remote-unavailable` means the catalog could not be read so an upgrade would
97
+ * fail on the same outage a moment later, and `not-in-marketplace` is the
98
+ * untrusted direct install whose only upgrade target is a mutable upstream ref.
81
99
  */
82
100
  const UPGRADABLE_STATUSES: ReadonlySet<string> = new Set([
83
101
  "update-available",
84
102
  "unknown-provenance",
85
- "not-in-marketplace",
86
103
  ]);
87
104
 
88
105
  /** Outcome the daemon reports for one plugin; mirrors `PluginUpgradeResult`. */
@@ -113,31 +130,83 @@ function stampSweep(): void {
113
130
  }
114
131
  }
115
132
 
133
+ /**
134
+ * Whether an installed copy actually tracks the curated source the sweep would
135
+ * upgrade it to.
136
+ *
137
+ * The provenance sidecar records the owner/repo/path the install was
138
+ * materialized from. When that names a different repository than the catalog
139
+ * entry claiming the plugin's name, the install is a direct (untrusted) one
140
+ * sitting on a curated name, and only its name lines up with the catalog. The
141
+ * sweep leaves it alone: the user picked that source, and swapping it for
142
+ * someone else's code is a call for a human running `assistant plugins
143
+ * upgrade`, not for an unattended hourly pass.
144
+ *
145
+ * An install with no recorded source at all (an older or manually-copied copy)
146
+ * is not disqualified. Nothing about it contradicts the catalog, and re-pinning
147
+ * it to the curated commit is exactly how it gains provenance.
148
+ */
149
+ function tracksCuratedSource(inspection: PluginInspection): boolean {
150
+ const source = inspection.local?.source;
151
+ if (!source) {
152
+ return true;
153
+ }
154
+ const remote = inspection.remote;
155
+ if (!remote) {
156
+ return false;
157
+ }
158
+ return (
159
+ `${source.owner}/${source.repo}`.toLowerCase() ===
160
+ remote.repo.toLowerCase() && (source.path ?? "") === remote.path
161
+ );
162
+ }
163
+
164
+ /** What one pass of inspection concluded about the installed plugins. */
165
+ interface UpgradeCandidates {
166
+ /** Names the daemon should be asked to upgrade. */
167
+ readonly candidates: readonly string[];
168
+ /** Names left alone because they do not come from the curated marketplace. */
169
+ readonly untrusted: readonly string[];
170
+ }
171
+
116
172
  /**
117
173
  * Installed plugins this sweep should ask the daemon to upgrade.
118
174
  *
119
175
  * Only user-installed plugins are listed (defaults ship with the assistant and
120
176
  * have no upstream to advance to). A disabled plugin is deliberately left
121
- * alone — the user switched it off, and upgrading it would re-materialize code
177
+ * alone: the user switched it off, and upgrading it would re-materialize code
122
178
  * and re-declare schedules for something that isn't running. What survives
123
179
  * that is inspected, so the daemon is only asked about plugins that can
124
- * actually move (see {@link UPGRADABLE_STATUSES}).
180
+ * actually move (see {@link UPGRADABLE_STATUSES}) and whose move lands on a
181
+ * curated pin (see {@link tracksCuratedSource}).
125
182
  */
126
- async function listUpgradableNames(): Promise<string[]> {
183
+ async function listUpgradableNames(): Promise<UpgradeCandidates> {
127
184
  const installed = listInstalledPlugins()
128
185
  .map((plugin) => plugin.name)
129
186
  .filter((name) => !isPluginDisabled(name));
130
187
 
131
188
  const candidates: string[] = [];
189
+ const untrusted: string[] = [];
132
190
  for (const name of installed) {
133
191
  try {
134
192
  const inspection = await inspectPlugin(
135
193
  { name },
136
194
  { fetch: globalThis.fetch.bind(globalThis) },
137
195
  );
138
- if (UPGRADABLE_STATUSES.has(inspection.status)) {
139
- candidates.push(name);
196
+ if (inspection.status === "not-in-marketplace") {
197
+ // No catalog entry claims the name, so the only thing to upgrade to is
198
+ // whatever the recorded upstream ref points at right now.
199
+ untrusted.push(name);
200
+ continue;
201
+ }
202
+ if (!UPGRADABLE_STATUSES.has(inspection.status)) {
203
+ continue;
204
+ }
205
+ if (!tracksCuratedSource(inspection)) {
206
+ untrusted.push(name);
207
+ continue;
140
208
  }
209
+ candidates.push(name);
141
210
  } catch (err) {
142
211
  // An install whose drift cannot be classified (source unreachable,
143
212
  // rate-limited) is skipped rather than handed to the daemon, which
@@ -145,10 +214,19 @@ async function listUpgradableNames(): Promise<string[]> {
145
214
  log.debug({ err, name }, "Plugin auto-update could not inspect plugin");
146
215
  }
147
216
  }
148
- return candidates;
217
+ return { candidates, untrusted };
149
218
  }
150
219
 
151
- /** Ask the daemon to upgrade one plugin. */
220
+ /**
221
+ * Ask the daemon to upgrade one plugin.
222
+ *
223
+ * `marketplaceOnly` restates this sweep's boundary as something the daemon
224
+ * enforces rather than something it trusts the caller to have checked. The
225
+ * daemon re-inspects before it moves anything, so a catalog entry that
226
+ * disappears between the inspection above and the daemon's own would otherwise
227
+ * turn a curated upgrade into a direct one against the install's mutable ref.
228
+ * With the flag set the daemon refuses instead.
229
+ */
152
230
  function requestUpgrade(
153
231
  name: string,
154
232
  strategy: PluginUpdatesConfig["strategy"],
@@ -157,7 +235,7 @@ function requestUpgrade(
157
235
  UPGRADE_IPC_METHOD,
158
236
  // The target revision is never caller-supplied — the daemon resolves it
159
237
  // from the plugin's own source, exactly as an interactive upgrade does.
160
- { pathParams: { name }, body: { strategy } },
238
+ { pathParams: { name }, body: { strategy, marketplaceOnly: true } },
161
239
  { timeoutMs: UPGRADE_TIMEOUT_MS },
162
240
  );
163
241
  }
@@ -174,6 +252,12 @@ export interface PluginAutoUpdateSweepResult {
174
252
  readonly upgraded: readonly string[];
175
253
  readonly unchanged: readonly string[];
176
254
  readonly failed: readonly string[];
255
+ /**
256
+ * Installs the sweep refused to touch because they do not come from the
257
+ * curated marketplace. They are not failures: a human can still upgrade them
258
+ * with `assistant plugins upgrade`.
259
+ */
260
+ readonly skippedUntrusted: readonly string[];
177
261
  /** True when the daemon could not be reached, so the sweep stays due. */
178
262
  readonly daemonUnreachable: boolean;
179
263
  }
@@ -182,6 +266,7 @@ const NOTHING: Omit<PluginAutoUpdateSweepResult, "skipped"> = {
182
266
  upgraded: [],
183
267
  unchanged: [],
184
268
  failed: [],
269
+ skippedUntrusted: [],
185
270
  daemonUnreachable: false,
186
271
  };
187
272
 
@@ -214,18 +299,29 @@ export async function runPluginAutoUpdateSweepIfDue(): Promise<PluginAutoUpdateS
214
299
  return { skipped: "not-due", ...NOTHING };
215
300
  }
216
301
 
217
- let names: string[];
302
+ let inspected: UpgradeCandidates;
218
303
  try {
219
- names = await listUpgradableNames();
304
+ inspected = await listUpgradableNames();
220
305
  } catch (err) {
221
306
  log.warn({ err }, "Plugin auto-update could not list installed plugins");
222
307
  return { skipped: "no-candidates", ...NOTHING };
223
308
  }
309
+ const { candidates: names, untrusted } = inspected;
310
+ if (untrusted.length > 0) {
311
+ log.info(
312
+ { plugins: untrusted },
313
+ "Plugin auto-update skipped plugins that are not from the curated marketplace",
314
+ );
315
+ }
224
316
  if (names.length === 0) {
225
317
  // Stamp anyway: a workspace with nothing to move is a completed sweep, and
226
318
  // re-inspecting every plugin a minute later would be pure churn.
227
319
  stampSweep();
228
- return { skipped: "no-candidates", ...NOTHING };
320
+ return {
321
+ skipped: "no-candidates",
322
+ ...NOTHING,
323
+ skippedUntrusted: untrusted,
324
+ };
229
325
  }
230
326
 
231
327
  const upgraded: string[] = [];
@@ -302,13 +398,21 @@ export async function runPluginAutoUpdateSweepIfDue(): Promise<PluginAutoUpdateS
302
398
  upgraded: upgraded.length,
303
399
  unchanged: unchanged.length,
304
400
  failed: failed.length,
401
+ skippedUntrusted: untrusted.length,
305
402
  strategy: config.strategy,
306
403
  },
307
404
  "Plugin auto-update sweep complete",
308
405
  );
309
406
  }
310
407
 
311
- return { skipped: null, upgraded, unchanged, failed, daemonUnreachable };
408
+ return {
409
+ skipped: null,
410
+ upgraded,
411
+ unchanged,
412
+ failed,
413
+ skippedUntrusted: untrusted,
414
+ daemonUnreachable,
415
+ };
312
416
  }
313
417
 
314
418
  /** Handle for the running auto-update loop. */
@@ -26,6 +26,7 @@ export const NOTIFICATION_SOURCE_CHANNELS = [
26
26
  { id: "platform", description: "Platform-managed channel" },
27
27
  { id: "a2a", description: "Agent-to-agent protocol channel" },
28
28
  { id: "discord", description: "Discord channel" },
29
+ { id: "plugin", description: "Channel brought by an installed plugin" },
29
30
  { id: "scheduler", description: "Scheduled task runner (reminders, cron)" },
30
31
  { id: "watcher", description: "File/event watcher subsystem" },
31
32
  ] as const;
@@ -19,17 +19,19 @@ mock.module("../util/logger.js", () => ({
19
19
  }));
20
20
 
21
21
  mock.module("../daemon/conversation-registry.js", () => ({
22
- findConversation: () => ({
23
- assistantId: "self",
24
- trustContext: {
25
- sourceChannel: "telegram",
26
- requesterExternalUserId: "tg-user-1",
27
- requesterChatId: "tg-chat-1",
28
- guardianExternalUserId: "tg-guardian-1",
29
- guardianPrincipalId: "principal-1",
30
- },
31
- hasPendingConfirmation: () => confirmationPending,
32
- }),
22
+ findConversation: () =>
23
+ asConversation({
24
+ assistantId: "self",
25
+ trustContext: {
26
+ trustClass: "trusted_contact",
27
+ sourceChannel: "telegram",
28
+ requesterExternalUserId: "tg-user-1",
29
+ requesterChatId: "tg-chat-1",
30
+ guardianExternalUserId: "tg-guardian-1",
31
+ guardianPrincipalId: "principal-1",
32
+ } satisfies TrustContext,
33
+ hasPendingConfirmation: () => confirmationPending,
34
+ }),
33
35
  }));
34
36
 
35
37
  mock.module("../channels/gateway-guardian-requests.js", () => ({
@@ -50,7 +52,9 @@ mock.module("../runtime/confirmation-request-guardian-bridge.js", () => ({
50
52
  },
51
53
  }));
52
54
 
55
+ import { asConversation } from "../__tests__/helpers/mock-conversation.js";
53
56
  import type { AssistantEvent } from "../api/index.js";
57
+ import type { TrustContext } from "../daemon/trust-context-types.js";
54
58
  import { createGuardianRequestForConfirmation } from "./confirmation-guardian-request.js";
55
59
 
56
60
  const MSG = {
@@ -66,8 +66,8 @@ export async function createGuardianRequestForConfirmation(
66
66
 
67
67
  const conversation = findConversation(conversationId);
68
68
  const trustContext = opts?.preferTurnSnapshot
69
- ? (conversation?.currentTurnTrustContext ?? conversation?.trustContext)
70
- : conversation?.trustContext;
69
+ ? conversation?.getTurnOrRestingTrust()
70
+ : conversation?.getTrustContext();
71
71
  const sourceChannel = trustContext?.sourceChannel ?? "vellum";
72
72
  const inputRecord = msg.input as Record<string, unknown>;
73
73
  const activityRaw =
@@ -7,7 +7,10 @@
7
7
 
8
8
  import { beforeEach, describe, expect, mock, test } from "bun:test";
9
9
 
10
+ import { mockTrustContext } from "../__tests__/helpers/mock-actor-context.js";
11
+ import { asConversation } from "../__tests__/helpers/mock-conversation.js";
10
12
  import type { QuestionRequestEvent } from "../api/events/question-request.js";
13
+ import type { TrustContext } from "../daemon/trust-context-types.js";
11
14
 
12
15
  const createGuardianRequestMock = mock(
13
16
  (params: Record<string, unknown>): Promise<Record<string, unknown>> =>
@@ -21,13 +24,18 @@ const withdrawCardsMock = mock((_params: Record<string, unknown>) =>
21
24
  Promise.resolve(),
22
25
  );
23
26
 
24
- let trustContext: Record<string, unknown> | undefined;
27
+ let trustContext: TrustContext | undefined;
25
28
  let pendingInteraction: { kind: string } | undefined;
26
29
  let rowAfterExpire: Record<string, unknown> | null = null;
27
30
 
28
31
  mock.module("../daemon/conversation-registry.js", () => ({
29
32
  findConversation: (_id: string) =>
30
- trustContext ? { trustContext, assistantId: "self" } : undefined,
33
+ trustContext
34
+ ? asConversation({
35
+ trustContext,
36
+ assistantId: "self",
37
+ })
38
+ : undefined,
31
39
  }));
32
40
  mock.module("../channels/gateway-guardian-requests.js", () => ({
33
41
  createGuardianRequest: (params: Record<string, unknown>) =>
@@ -77,9 +85,9 @@ function makeEvent(
77
85
  }
78
86
 
79
87
  function guardianTrustContext(
80
- overrides: Record<string, unknown> = {},
81
- ): Record<string, unknown> {
82
- return {
88
+ overrides: Partial<TrustContext> = {},
89
+ ): TrustContext {
90
+ return mockTrustContext({
83
91
  trustClass: "guardian",
84
92
  sourceChannel: "telegram",
85
93
  guardianPrincipalId: "prin-guardian",
@@ -87,7 +95,7 @@ function guardianTrustContext(
87
95
  requesterExternalUserId: "tg-guardian",
88
96
  requesterChatId: "chat-1",
89
97
  ...overrides,
90
- };
98
+ });
91
99
  }
92
100
 
93
101
  beforeEach(() => {
@@ -107,8 +107,7 @@ export async function createGuardianRequestForQuestion(
107
107
  const conversation = findConversation(conversationId);
108
108
  // The prompter runs inside the emitting turn — bind to the turn's trust
109
109
  // snapshot so a concurrent context mutation can't repoint the request.
110
- const trustContext =
111
- conversation?.currentTurnTrustContext ?? conversation?.trustContext;
110
+ const trustContext = conversation?.getTurnOrRestingTrust();
112
111
  if (!conversation || !trustContext) {
113
112
  return;
114
113
  }
@@ -137,6 +137,9 @@ function getAttachmentRow(attachmentId: string): AttachmentRow | null {
137
137
  function getMessageConversationContext(
138
138
  messageId: string,
139
139
  ): { conversationId: string; conversationCreatedAt: number } | null {
140
+ // Any-state read, deliberately: the id names a message the caller is
141
+ // already linking an attachment to, and a rendered row may still be
142
+ // streaming. Existence and ownership are the question, not completeness.
140
143
  return (
141
144
  rawGet<{ conversationId: string; conversationCreatedAt: number }>(
142
145
  "attachments:getMessageConversationContext",
@@ -474,6 +477,11 @@ const ALLOWED_MIME_TYPES = new Set([
474
477
  // Source code
475
478
  "text/javascript",
476
479
  "text/typescript",
480
+ // Shell scripts (browsers and OS file pickers report these for .sh files)
481
+ "application/x-sh",
482
+ "application/x-shellscript",
483
+ "text/x-sh",
484
+ "text/x-shellscript",
477
485
  // Archives
478
486
  "application/zip",
479
487
  "application/gzip",
@@ -501,7 +509,6 @@ const ALLOWED_MIME_TYPES = new Set([
501
509
  */
502
510
  const DANGEROUS_EXTENSIONS = new Set([
503
511
  "exe",
504
- "sh",
505
512
  "bat",
506
513
  "cmd",
507
514
  "com",