@vellumai/assistant 0.11.3 → 0.11.4-dev.202608190019.b94dbf2

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 (596) hide show
  1. package/AGENTS.md +9 -3
  2. package/ARCHITECTURE.md +32 -10
  3. package/docs/architecture/memory.md +43 -5
  4. package/docs/architecture/security.md +96 -152
  5. package/docs/architecture/turn-actor.md +70 -0
  6. package/docs/browser-use-architecture-phase2.md +128 -56
  7. package/docs/flux-turn-detection-spike.md +248 -0
  8. package/docs/guardian-request-flow.md +35 -0
  9. package/docs/stt-provider-onboarding.md +3 -1
  10. package/knip.json +3 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  24. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  25. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  26. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  27. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  28. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  29. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  30. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  31. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  32. package/openapi.yaml +289 -113
  33. package/package.json +1 -1
  34. package/scripts/voice-ttft-spike.ts +3 -3
  35. package/scripts/write-plugin-api-shim.ts +10 -0
  36. package/src/__tests__/app-compiler.test.ts +38 -3
  37. package/src/__tests__/app-control-flow.test.ts +1 -0
  38. package/src/__tests__/approval-routes-http.test.ts +2 -2
  39. package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
  40. package/src/__tests__/attachments-store.test.ts +21 -12
  41. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  42. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  43. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  44. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  45. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  46. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  47. package/src/__tests__/channel-inbound-disk-pressure.test.ts +4 -4
  48. package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
  49. package/src/__tests__/checker.test.ts +20 -314
  50. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  51. package/src/__tests__/compaction-events.test.ts +8 -10
  52. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  53. package/src/__tests__/config-schema.test.ts +25 -10
  54. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  55. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  56. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  57. package/src/__tests__/conversation-agent-loop.test.ts +32 -21
  58. package/src/__tests__/conversation-attachments.test.ts +0 -1
  59. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
  61. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  62. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  63. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  64. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  65. package/src/__tests__/conversation-load-history-repair.test.ts +209 -0
  66. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  67. package/src/__tests__/conversation-queue.test.ts +154 -4
  68. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  69. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
  71. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  72. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  73. package/src/__tests__/conversation-runtime-assembly.test.ts +187 -102
  74. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  75. package/src/__tests__/conversation-slash-queue.test.ts +3 -0
  76. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  77. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
  78. package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
  79. package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
  80. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
  81. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
  82. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
  83. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
  84. package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
  85. package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
  86. package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
  87. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
  88. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
  89. package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
  90. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  91. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  92. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  93. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  94. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  95. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  96. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  97. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  98. package/src/__tests__/document-sync-tags.test.ts +0 -75
  99. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  100. package/src/__tests__/edit-propagation.test.ts +107 -11
  101. package/src/__tests__/file-ops-service.test.ts +163 -30
  102. package/src/__tests__/filesystem-tools.test.ts +23 -24
  103. package/src/__tests__/gateway-only-guard.test.ts +2 -5
  104. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  105. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  106. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  107. package/src/__tests__/host-file-read-tool.test.ts +16 -19
  108. package/src/__tests__/http-user-message-parity.test.ts +1 -1
  109. package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
  110. package/src/__tests__/injector-chain.test.ts +63 -41
  111. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  112. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  113. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  114. package/src/__tests__/llm-schema.test.ts +5 -2
  115. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
  116. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  117. package/src/__tests__/media-generate-image.test.ts +131 -21
  118. package/src/__tests__/memory-retrieval-hook.test.ts +100 -7
  119. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  120. package/src/__tests__/mtime-cache.test.ts +3 -1
  121. package/src/__tests__/non-member-access-request.test.ts +0 -20
  122. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  123. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  124. package/src/__tests__/plugin-api-store-credential.test.ts +261 -0
  125. package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
  126. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  127. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  128. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  129. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  130. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -4
  131. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  132. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  133. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  134. package/src/__tests__/proxy-approval-callback.test.ts +1 -0
  135. package/src/__tests__/qdrant-manager.test.ts +14 -1
  136. package/src/__tests__/reaction-persistence.test.ts +200 -7
  137. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  138. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  139. package/src/__tests__/run-conversation-turn-persistence.test.ts +460 -105
  140. package/src/__tests__/run-due-schedules.test.ts +21 -0
  141. package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
  142. package/src/__tests__/schedule-routes.test.ts +23 -0
  143. package/src/__tests__/schedule-store.test.ts +17 -0
  144. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  145. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  146. package/src/__tests__/secret-ingress-http.test.ts +1 -1
  147. package/src/__tests__/send-endpoint-busy.test.ts +3 -3
  148. package/src/__tests__/skills.test.ts +32 -0
  149. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  150. package/src/__tests__/starter-task-flow.test.ts +5 -4
  151. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  152. package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
  153. package/src/__tests__/subagent-spawn-and-await.test.ts +18 -17
  154. package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
  155. package/src/__tests__/subagent-tools.test.ts +81 -101
  156. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
  157. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  158. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  159. package/src/__tests__/tool-executor.test.ts +68 -39
  160. package/src/__tests__/turn-events-store.test.ts +43 -0
  161. package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
  162. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  163. package/src/__tests__/ui-visual-surface.test.ts +1 -1
  164. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  165. package/src/__tests__/ui-work-result-surface.test.ts +1 -1
  166. package/src/__tests__/user-plugin-loader.test.ts +3 -1
  167. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  168. package/src/__tests__/visible-app-context.test.ts +16 -9
  169. package/src/__tests__/voice-config-update.test.ts +40 -0
  170. package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
  171. package/src/__tests__/voice-session-bridge.test.ts +85 -29
  172. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  173. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  174. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  175. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  176. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  177. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  178. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  179. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  180. package/src/acp/__tests__/auth-required.test.ts +161 -0
  181. package/src/acp/acp-claude-oauth.ts +19 -2
  182. package/src/acp/agent-process.test.ts +100 -0
  183. package/src/acp/agent-process.ts +29 -26
  184. package/src/acp/auth-required.ts +102 -0
  185. package/src/acp/session-manager.test.ts +119 -0
  186. package/src/acp/session-manager.ts +76 -3
  187. package/src/api/events/acp-auth-required.ts +55 -0
  188. package/src/api/events/host-file.ts +2 -2
  189. package/src/api/index.ts +7 -0
  190. package/src/api/surfaces.ts +12 -3
  191. package/src/apps/app-store.ts +3 -0
  192. package/src/bundler/package-resolver.ts +2 -30
  193. package/src/calls/__tests__/voice-session-bridge.test.ts +194 -11
  194. package/src/calls/__tests__/voice-triage-escalate.test.ts +102 -0
  195. package/src/calls/call-controller.ts +19 -3
  196. package/src/calls/call-setup-flow.ts +0 -1
  197. package/src/calls/media-stream-stt-session.ts +15 -0
  198. package/src/calls/voice-session-bridge.ts +115 -41
  199. package/src/calls/voice-triage-escalate.ts +105 -2
  200. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  201. package/src/channels/config.ts +13 -0
  202. package/src/channels/plugin-channel-declarations.ts +108 -0
  203. package/src/channels/types.ts +30 -0
  204. package/src/cli/AGENTS.md +5 -2
  205. package/src/cli/bundled-modules.ts +29 -0
  206. package/src/cli/commands/credentials.help.ts +2 -2
  207. package/src/cli/commands/db/repair.ts +4 -8
  208. package/src/cli/commands/domain.ts +6 -3
  209. package/src/cli/commands/email.ts +6 -3
  210. package/src/cli/commands/inference-providers.ts +1 -1
  211. package/src/cli/commands/keys.ts +8 -3
  212. package/src/cli/commands/mcp.help.ts +13 -4
  213. package/src/cli/commands/mcp.ts +9 -0
  214. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  215. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  216. package/src/cli/commands/memory/index.help.ts +84 -28
  217. package/src/cli/commands/memory/index.ts +2 -0
  218. package/src/cli/commands/memory/memory-v2.ts +58 -54
  219. package/src/cli/commands/memory/memory-v3.ts +64 -0
  220. package/src/cli/commands/memory/memory-validate.ts +18 -0
  221. package/src/cli/commands/plugins.ts +85 -36
  222. package/src/cli/commands/schedules.ts +35 -1
  223. package/src/cli/commands/stt.help.ts +27 -2
  224. package/src/cli/commands/trust.ts +3 -14
  225. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  226. package/src/cli/lib/bundled-marketplace.json +14 -1
  227. package/src/cli/lib/upgrade-plugin.ts +42 -0
  228. package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
  229. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  230. package/src/config/__tests__/default-provider.test.ts +6 -1
  231. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  232. package/src/config/assistant-feature-flags.ts +36 -15
  233. package/src/config/balanced-model-experiment.ts +35 -0
  234. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  235. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  236. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  237. package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
  238. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
  239. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
  240. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  241. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  242. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  243. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  244. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  245. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  246. package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
  247. package/src/config/bundled-skills/subagent/SKILL.md +17 -12
  248. package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
  249. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  250. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  251. package/src/config/call-site-defaults.ts +11 -5
  252. package/src/config/default-profile-catalog.ts +179 -16
  253. package/src/config/default-profile-names.ts +4 -1
  254. package/src/config/default-provider-resolution.ts +4 -0
  255. package/src/config/feature-flag-registry.json +12 -12
  256. package/src/config/llm-context-resolution.ts +11 -3
  257. package/src/config/llm-resolver.ts +28 -1
  258. package/src/config/profile-materialization.ts +70 -22
  259. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  260. package/src/config/schemas/call-site-catalog.ts +4 -4
  261. package/src/config/schemas/live-voice.ts +57 -23
  262. package/src/config/schemas/llm.ts +59 -32
  263. package/src/config/schemas/mcp.ts +23 -0
  264. package/src/config/schemas/plugin-updates.ts +6 -2
  265. package/src/config/schemas/stt.ts +1 -0
  266. package/src/config/skills.ts +9 -2
  267. package/src/context/outbound-sanitize.ts +96 -1
  268. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
  269. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  270. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  271. package/src/daemon/conversation-agent-loop.ts +31 -19
  272. package/src/daemon/conversation-messaging.ts +5 -1
  273. package/src/daemon/conversation-notifiers.ts +20 -10
  274. package/src/daemon/conversation-process.ts +20 -6
  275. package/src/daemon/conversation-runtime-assembly.ts +12 -6
  276. package/src/daemon/conversation-store.ts +14 -4
  277. package/src/daemon/conversation-surfaces.ts +75 -26
  278. package/src/daemon/conversation-tool-setup.ts +3 -6
  279. package/src/daemon/conversation.ts +154 -51
  280. package/src/daemon/doordash-steps.ts +2 -2
  281. package/src/daemon/lifecycle.ts +14 -1
  282. package/src/daemon/mcp-reload-service.ts +36 -6
  283. package/src/daemon/process-message.ts +13 -26
  284. package/src/daemon/providers-setup.ts +6 -3
  285. package/src/daemon/trust-context-types.ts +29 -0
  286. package/src/daemon/trust-context.ts +12 -7
  287. package/src/daemon/wake-conversation-ops.ts +3 -2
  288. package/src/daemon/windows-compiled-entry.ts +4 -0
  289. package/src/documents/document-store.ts +143 -240
  290. package/src/hooks/hook-loader.ts +3 -3
  291. package/src/hooks/registry.ts +103 -45
  292. package/src/hooks/types.ts +5 -0
  293. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  294. package/src/inbound/oauth-callback-url.ts +61 -0
  295. package/src/ipc/gateway-client.test.ts +1 -1
  296. package/src/ipc/gateway-client.ts +14 -21
  297. package/src/ipc/gateway-flag-listener.ts +17 -3
  298. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  299. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  300. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +1050 -0
  301. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  302. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  303. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  304. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  305. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  306. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  307. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  308. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  309. package/src/live-voice/live-voice-archive.ts +2 -0
  310. package/src/live-voice/live-voice-manager.ts +16 -3
  311. package/src/live-voice/live-voice-metrics.ts +57 -32
  312. package/src/live-voice/live-voice-photo.ts +1 -2
  313. package/src/live-voice/live-voice-session.ts +598 -323
  314. package/src/live-voice/progress-narration.ts +277 -0
  315. package/src/live-voice/protocol.ts +21 -1
  316. package/src/live-voice/windows-compiled-live-voice.ts +4 -0
  317. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  318. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  319. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  320. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  321. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  322. package/src/mcp/client.ts +16 -11
  323. package/src/mcp/effective-config.ts +113 -0
  324. package/src/mcp/manager.ts +11 -6
  325. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  326. package/src/mcp/mcp-oauth-provider.ts +205 -240
  327. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  328. package/src/monitoring/control.ts +1 -0
  329. package/src/monitoring/db-integrity-sample.ts +4 -5
  330. package/src/monitoring/plugin-auto-update.ts +128 -24
  331. package/src/notifications/AGENTS.md +2 -0
  332. package/src/notifications/approval-card-data.ts +33 -0
  333. package/src/notifications/signal.ts +1 -0
  334. package/src/permissions/AGENTS.md +16 -0
  335. package/src/permissions/checker.test.ts +75 -122
  336. package/src/permissions/checker.ts +44 -560
  337. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  338. package/src/permissions/confirmation-guardian-request.ts +2 -2
  339. package/src/permissions/prompter.ts +1 -5
  340. package/src/permissions/question-guardian-request.test.ts +14 -6
  341. package/src/permissions/question-guardian-request.ts +1 -2
  342. package/src/persistence/attachments-store.ts +8 -1
  343. package/src/persistence/bookmark-crud.ts +3 -7
  344. package/src/persistence/conversation-attention-store.ts +16 -45
  345. package/src/persistence/conversation-crud.ts +33 -4
  346. package/src/persistence/conversation-lineage.ts +9 -0
  347. package/src/persistence/conversation-queries.ts +182 -52
  348. package/src/persistence/delivery-crud.ts +150 -36
  349. package/src/persistence/embeddings/qdrant-manager.ts +84 -49
  350. package/src/persistence/external-conversation-store.ts +32 -4
  351. package/src/persistence/llm-request-log-store.ts +4 -10
  352. package/src/persistence/llm-usage-store.ts +8 -3
  353. package/src/persistence/message-reads.test.ts +197 -0
  354. package/src/persistence/message-reads.ts +211 -0
  355. package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
  356. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  357. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  358. package/src/persistence/real-user-turn-filter.ts +27 -3
  359. package/src/persistence/schema/documents.ts +4 -4
  360. package/src/persistence/steps.ts +9 -0
  361. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  362. package/src/plugin-api/constants.ts +8 -0
  363. package/src/plugin-api/conversation-turn.ts +195 -5
  364. package/src/plugin-api/index.ts +38 -6
  365. package/src/plugin-api/resolve-credential.ts +3 -2
  366. package/src/plugin-api/store-credential.ts +146 -0
  367. package/src/plugin-api/vision-support.test.ts +1 -1
  368. package/src/plugin-api/webhook-url.ts +13 -11
  369. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  370. package/src/plugins/defaults/main.ts +21 -7
  371. package/src/plugins/defaults/memory/AGENTS.md +15 -3
  372. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  373. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  374. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  375. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  376. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  377. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  378. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  379. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
  380. package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
  381. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  382. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  383. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  384. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +17 -24
  385. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  386. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
  387. package/src/plugins/defaults/memory/indexer.ts +3 -1
  388. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  389. package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
  390. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  391. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  392. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  393. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +147 -0
  394. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  395. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  396. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  397. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  398. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  399. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  400. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +195 -58
  401. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +93 -24
  402. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  403. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  404. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  405. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  406. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  407. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  408. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  409. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -33
  410. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  411. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  412. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
  413. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
  414. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  415. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  416. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  417. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  418. package/src/plugins/defaults/memory/v3/injector.ts +8 -0
  419. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  420. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  421. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
  422. package/src/plugins/defaults/memory/worker-control.ts +1 -0
  423. package/src/plugins/defaults/memory/worker.ts +6 -3
  424. package/src/plugins/defaults/worker-entrypoints.ts +3 -0
  425. package/src/plugins/external-plugin-loader.ts +47 -0
  426. package/src/plugins/mcp-servers.ts +361 -0
  427. package/src/plugins/mtime-cache.ts +37 -46
  428. package/src/plugins/pipeline.ts +14 -7
  429. package/src/plugins/plugin-execution-context.ts +35 -11
  430. package/src/plugins/worker-plugin-surface.ts +33 -0
  431. package/src/prompts/templates/system-sections.ts +0 -7
  432. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  433. package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
  434. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  435. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  436. package/src/providers/__tests__/retry-callsite.test.ts +25 -2
  437. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  438. package/src/providers/anthropic/client.ts +332 -241
  439. package/src/providers/call-site-routing.ts +30 -3
  440. package/src/providers/connection-resolution.ts +194 -11
  441. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +4 -0
  442. package/src/providers/inference/adapter-factory.ts +3 -0
  443. package/src/providers/inference/auth.ts +6 -0
  444. package/src/providers/inference/connection-availability.ts +24 -2
  445. package/src/providers/inference/connections.ts +2 -0
  446. package/src/providers/model-catalog.ts +3 -3
  447. package/src/providers/model-intents.ts +28 -8
  448. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +163 -40
  449. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  450. package/src/providers/openai/chat-completions-provider.ts +133 -36
  451. package/src/providers/openai/codex-models.ts +2 -1
  452. package/src/providers/provider-send-message.ts +32 -3
  453. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  454. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +625 -0
  455. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  456. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  457. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  458. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +679 -0
  459. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  460. package/src/providers/speech-to-text/resolve.ts +25 -2
  461. package/src/routes/control.ts +1 -0
  462. package/src/routes/route-host-client.ts +1 -0
  463. package/src/routes/route-host-protocol.ts +7 -0
  464. package/src/routes/worker.ts +48 -15
  465. package/src/runtime/AGENTS.md +16 -17
  466. package/src/runtime/access-request-helper.ts +9 -12
  467. package/src/runtime/agent-wake.ts +18 -15
  468. package/src/runtime/effective-capabilities.test.ts +43 -0
  469. package/src/runtime/effective-capabilities.ts +27 -0
  470. package/src/runtime/guardian-reply-router.ts +6 -2
  471. package/src/runtime/pre-first-message-gate.ts +4 -0
  472. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  473. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +522 -2
  474. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  475. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  476. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  477. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  478. package/src/runtime/routes/__tests__/plugins-routes.test.ts +132 -35
  479. package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
  480. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  481. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  482. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  483. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +133 -1
  484. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  485. package/src/runtime/routes/channel-route-shared.ts +0 -6
  486. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  487. package/src/runtime/routes/conversation-list-routes.ts +181 -23
  488. package/src/runtime/routes/conversation-management-routes.ts +17 -12
  489. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  490. package/src/runtime/routes/conversation-routes.ts +43 -29
  491. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  492. package/src/runtime/routes/credential-routes.ts +26 -82
  493. package/src/runtime/routes/default-provider-routes.ts +15 -0
  494. package/src/runtime/routes/documents-routes.ts +3 -222
  495. package/src/runtime/routes/inbound-message-handler.ts +21 -45
  496. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  497. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  498. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  499. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  500. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  501. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -79
  502. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  503. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -7
  504. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -8
  505. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -79
  506. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  507. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  508. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  509. package/src/runtime/routes/log-export-routes.ts +3 -0
  510. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  511. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
  512. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
  513. package/src/runtime/routes/playground/inject-failures.ts +2 -2
  514. package/src/runtime/routes/playground/reset-circuit.ts +1 -1
  515. package/src/runtime/routes/plugins-routes.ts +38 -8
  516. package/src/runtime/routes/schedule-routes.ts +94 -7
  517. package/src/runtime/routes/settings-routes.ts +6 -6
  518. package/src/runtime/routes/stt-routes.ts +31 -25
  519. package/src/runtime/routes/surface-action-routes.ts +20 -19
  520. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  521. package/src/runtime/routes/user-route-dispatcher.ts +69 -15
  522. package/src/runtime/routes/user-route-import.ts +108 -0
  523. package/src/runtime/routes/user-route-resolution.ts +18 -1
  524. package/src/runtime/routes/workspace-routes.ts +0 -9
  525. package/src/runtime/routes/workspace-utils.ts +3 -13
  526. package/src/runtime/services/conversation-serializer.ts +7 -2
  527. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
  528. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
  529. package/src/schedule/plugin-schedule-availability.ts +58 -0
  530. package/src/schedule/plugin-schedule-declarations.ts +23 -27
  531. package/src/schedule/plugin-schedule-reconciler.ts +12 -3
  532. package/src/schedule/schedule-store.ts +5 -1
  533. package/src/schedule/scheduler.ts +9 -4
  534. package/src/schedule/worker-control.ts +1 -0
  535. package/src/schedule/worker.ts +6 -0
  536. package/src/security/oauth2.ts +6 -22
  537. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  538. package/src/stt/__tests__/types.test.ts +94 -0
  539. package/src/stt/daemon-batch-transcriber.ts +10 -0
  540. package/src/stt/stt-stream-session.ts +8 -4
  541. package/src/stt/types.ts +103 -0
  542. package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
  543. package/src/subagent/consult-context.ts +11 -11
  544. package/src/subagent/consult-prompt.ts +26 -35
  545. package/src/subagent/manager.ts +21 -40
  546. package/src/subagent/notify.ts +7 -1
  547. package/src/subagent/types.ts +15 -12
  548. package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
  549. package/src/tools/acp/spawn.test.ts +97 -0
  550. package/src/tools/acp/spawn.ts +38 -4
  551. package/src/tools/credentials/ref-parse.ts +35 -0
  552. package/src/tools/credentials/resolve.ts +4 -10
  553. package/src/tools/credentials/store.ts +168 -0
  554. package/src/tools/document/document-tool.ts +12 -3
  555. package/src/tools/executor.ts +53 -15
  556. package/src/tools/filesystem/read.ts +27 -10
  557. package/src/tools/host-filesystem/read.ts +27 -15
  558. package/src/tools/network/url-safety.ts +9 -16
  559. package/src/tools/permission-checker.ts +43 -52
  560. package/src/tools/registry.ts +2 -1
  561. package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
  562. package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
  563. package/src/tools/shared/filesystem/types.ts +5 -5
  564. package/src/tools/skills/scaffold-managed.ts +25 -7
  565. package/src/tools/subagent/spawn.ts +28 -88
  566. package/src/tools/tool-types.ts +26 -13
  567. package/src/tools/types.ts +6 -6
  568. package/src/tools/ui-surface/surface-shape-docs.ts +12 -1
  569. package/src/tools/workflows/run-workflow.ts +1 -2
  570. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  571. package/src/tts/reasoning-tag-filter.ts +89 -0
  572. package/src/util/__tests__/worker-process-command.test.ts +37 -0
  573. package/src/util/logger.ts +16 -0
  574. package/src/util/think-tag-stream.ts +95 -0
  575. package/src/util/worker-process.ts +37 -4
  576. package/src/windows-compiled-cli.ts +32 -0
  577. package/src/windows-compiled-entry.ts +4 -0
  578. package/src/windows-compiled-logger.ts +6 -0
  579. package/src/windows-compiled-worker-entry.ts +29 -0
  580. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  581. package/src/workspace/custom-profile-ensure.ts +4 -24
  582. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  583. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  584. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  585. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  586. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  587. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  588. package/src/workspace/migrations/registry.ts +10 -0
  589. package/src/workspace/provider-commit-message-generator.ts +7 -5
  590. package/src/__tests__/document-workspace-file.test.ts +0 -467
  591. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  592. package/src/live-voice/front-decision.ts +0 -476
  593. package/src/permissions/ipc-risk-types.ts +0 -143
  594. package/src/permissions/risk-types.ts +0 -76
  595. package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
  596. package/src/subagent/consult-transcript.ts +0 -90
@@ -0,0 +1,215 @@
1
+ /**
2
+ * The schedule list and detail routes say why a plugin-sourced schedule is
3
+ * off, so a client never has to show an unexplained disabled row.
4
+ *
5
+ * The reason is derived from the row plus the plugin's files, so the fixtures
6
+ * here write real plugin directories under the per-test-process workspace
7
+ * plugins dir and converge them against the real schedule store.
8
+ */
9
+
10
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { beforeEach, describe, expect, test } from "bun:test";
13
+
14
+ import { setOverridesForTesting } from "../../../__tests__/feature-flag-test-helpers.js";
15
+ import { getDb } from "../../../persistence/db-connection.js";
16
+ import { initializeDb } from "../../../persistence/db-init.js";
17
+ import {
18
+ createSchedule,
19
+ setUserEnabled,
20
+ upsertDeclaredSchedule,
21
+ } from "../../../schedule/schedule-store.js";
22
+ import { getWorkspacePluginsDir } from "../../../util/platform.js";
23
+ import { ROUTES } from "../schedule-routes.js";
24
+
25
+ await initializeDb();
26
+
27
+ const pluginsDir = getWorkspacePluginsDir();
28
+
29
+ function routeHandler(operationId: string) {
30
+ const route = ROUTES.find((r) => r.operationId === operationId);
31
+ if (!route) {
32
+ throw new Error(`${operationId} route not found`);
33
+ }
34
+ return route.handler;
35
+ }
36
+
37
+ const listHandler = routeHandler("listSchedules");
38
+ const getHandler = routeHandler("getSchedule");
39
+
40
+ interface SerializedSchedule {
41
+ id: string;
42
+ sourceKey: string | null;
43
+ enabled: boolean;
44
+ disarmReason: string | null;
45
+ }
46
+
47
+ async function listed(id: string): Promise<SerializedSchedule> {
48
+ const { schedules } = (await listHandler({ queryParams: {} })) as {
49
+ schedules: SerializedSchedule[];
50
+ };
51
+ const found = schedules.find((s) => s.id === id);
52
+ if (!found) {
53
+ throw new Error(`schedule ${id} missing from the list response`);
54
+ }
55
+ return found;
56
+ }
57
+
58
+ async function fetched(id: string): Promise<SerializedSchedule> {
59
+ const { schedule } = (await getHandler({ pathParams: { id } })) as {
60
+ schedule: SerializedSchedule;
61
+ };
62
+ return schedule;
63
+ }
64
+
65
+ /** Write a plugin dir holding one `schedules/<name>/` declaration. */
66
+ function writePlugin(
67
+ pluginName: string,
68
+ options: { declaration?: string; disabled?: boolean } = {},
69
+ ): void {
70
+ const dir = join(pluginsDir, pluginName);
71
+ mkdirSync(dir, { recursive: true });
72
+ writeFileSync(
73
+ join(dir, "package.json"),
74
+ JSON.stringify({ name: pluginName, version: "1.0.0" }),
75
+ );
76
+ if (options.declaration) {
77
+ const declarationDir = join(dir, "schedules", options.declaration);
78
+ mkdirSync(declarationDir, { recursive: true });
79
+ writeFileSync(
80
+ join(declarationDir, "config.json"),
81
+ JSON.stringify({ expression: "* * * * *" }),
82
+ );
83
+ writeFileSync(join(declarationDir, "index.md"), "Summarize the day.\n");
84
+ }
85
+ if (options.disabled) {
86
+ writeFileSync(join(dir, ".disabled"), "");
87
+ }
88
+ }
89
+
90
+ /** A declared row for `plugin:<pluginName>/digest`, armed or not. */
91
+ async function declaredRow(
92
+ pluginName: string,
93
+ enabled: boolean,
94
+ ): Promise<string> {
95
+ const job = await upsertDeclaredSchedule(`plugin:${pluginName}/digest`, {
96
+ name: `${pluginName}-digest`,
97
+ description: "Daily digest",
98
+ syntax: "cron",
99
+ expression: "* * * * *",
100
+ message: "Summarize the day.",
101
+ mode: "execute",
102
+ enabled,
103
+ definitionHash: `hash-${pluginName}`,
104
+ });
105
+ return job.id;
106
+ }
107
+
108
+ /** Stamp the engine's exhaust latch onto a row: off, spent, and last run. */
109
+ function latchAsCompleted(id: string): void {
110
+ getDb().run(
111
+ `UPDATE cron_jobs SET enabled = 0, next_run_at = 0, last_run_at = ${Date.now()} WHERE id = '${id}'`,
112
+ );
113
+ }
114
+
115
+ beforeEach(() => {
116
+ getDb().run("DELETE FROM cron_runs");
117
+ getDb().run("DELETE FROM cron_jobs");
118
+ rmSync(pluginsDir, { recursive: true, force: true });
119
+ // The feature ships off, so every case states the flag it runs under rather
120
+ // than inheriting the registry default.
121
+ setOverridesForTesting({ "plugin-schedules": true });
122
+ });
123
+
124
+ describe("schedule serialization: disarmReason", () => {
125
+ test("a user's own override outranks whatever the plugin's files say", async () => {
126
+ writePlugin("news", { declaration: "digest" });
127
+ const id = await declaredRow("news", true);
128
+ await setUserEnabled(id, false);
129
+
130
+ expect((await listed(id)).disarmReason).toBe("user_disabled");
131
+ expect((await fetched(id)).disarmReason).toBe("user_disabled");
132
+ });
133
+
134
+ test("a plugin that is gone from disk reads as removed", async () => {
135
+ const id = await declaredRow("news", false);
136
+
137
+ expect((await listed(id)).disarmReason).toBe("plugin_removed");
138
+ expect((await fetched(id)).disarmReason).toBe("plugin_removed");
139
+ });
140
+
141
+ test("a plugin carrying the .disabled sentinel reads as disabled", async () => {
142
+ writePlugin("news", { declaration: "digest", disabled: true });
143
+ const id = await declaredRow("news", false);
144
+
145
+ expect((await listed(id)).disarmReason).toBe("plugin_disabled");
146
+ expect((await fetched(id)).disarmReason).toBe("plugin_disabled");
147
+ });
148
+
149
+ test("a declaration dropped from a plugin that is still installed", async () => {
150
+ writePlugin("news");
151
+ const id = await declaredRow("news", false);
152
+
153
+ expect((await listed(id)).disarmReason).toBe("declaration_removed");
154
+ expect((await fetched(id)).disarmReason).toBe("declaration_removed");
155
+ });
156
+
157
+ test("a declaration still on disk means the plugin's files turned it off", async () => {
158
+ writePlugin("news", { declaration: "digest" });
159
+ const id = await declaredRow("news", false);
160
+
161
+ expect((await listed(id)).disarmReason).toBe("declaration_disabled");
162
+ expect((await fetched(id)).disarmReason).toBe("declaration_disabled");
163
+ });
164
+
165
+ test("an armed plugin row has no reason to give", async () => {
166
+ writePlugin("news", { declaration: "digest" });
167
+ const id = await declaredRow("news", true);
168
+
169
+ const row = await listed(id);
170
+ expect(row.enabled).toBe(true);
171
+ expect(row.disarmReason).toBeNull();
172
+ expect((await fetched(id)).disarmReason).toBeNull();
173
+ });
174
+
175
+ test("a run-to-completion row reads as finished, not paused", async () => {
176
+ writePlugin("news", { declaration: "digest" });
177
+ const id = await declaredRow("news", true);
178
+ latchAsCompleted(id);
179
+
180
+ const row = await listed(id);
181
+ expect(row.enabled).toBe(false);
182
+ expect(row.disarmReason).toBeNull();
183
+ expect((await fetched(id)).disarmReason).toBeNull();
184
+ });
185
+
186
+ test("the kill switch being off is not a per-row cause", async () => {
187
+ writePlugin("news", { declaration: "digest" });
188
+ const id = await declaredRow("news", false);
189
+ setOverridesForTesting({ "plugin-schedules": false });
190
+
191
+ const row = await listed(id);
192
+ expect(row.enabled).toBe(false);
193
+ expect(row.disarmReason).toBeNull();
194
+ expect((await fetched(id)).disarmReason).toBeNull();
195
+ });
196
+
197
+ test("a user-created schedule never carries one, off or on", async () => {
198
+ const job = await createSchedule({
199
+ name: "wt-imperative",
200
+ description: "d",
201
+ message: "do the thing",
202
+ mode: "execute",
203
+ enabled: false,
204
+ timezone: null,
205
+ expression: "* * * * *",
206
+ syntax: "cron",
207
+ });
208
+
209
+ const row = await listed(job.id);
210
+ expect(row.sourceKey).toBeNull();
211
+ expect(row.enabled).toBe(false);
212
+ expect(row.disarmReason).toBeNull();
213
+ expect((await fetched(job.id)).disarmReason).toBeNull();
214
+ });
215
+ });
@@ -288,6 +288,31 @@ describe("stt-routes", () => {
288
288
  expect(err.message).toContain("not available");
289
289
  });
290
290
 
291
+ test("surfaces a typed resolver error verbatim instead of the generic copy", async () => {
292
+ // A streaming-only provider IS configured, so the "nothing configured"
293
+ // and "not available" strings would both send the caller looking for a
294
+ // problem that does not exist.
295
+ const reason =
296
+ 'Deepgram Flux is streaming-only. Batch transcription requires the deepgram provider: set services.stt.provider to "deepgram".';
297
+ mockResolveError = new SttError("provider-error", reason, {
298
+ userFacing: true,
299
+ });
300
+
301
+ const { handler } = getRoute("stt/transcribe");
302
+ const err = await expectRouteError(
303
+ () =>
304
+ handler(
305
+ makeArgs({
306
+ audioBase64: toBase64("audio-data"),
307
+ mimeType: "audio/wav",
308
+ }),
309
+ ),
310
+ 503,
311
+ "SERVICE_UNAVAILABLE",
312
+ );
313
+ expect(err.message).toBe(reason);
314
+ });
315
+
291
316
  // -- Timeout --------------------------------------------------------------
292
317
 
293
318
  test("throws 504 when transcription times out", async () => {
@@ -26,6 +26,10 @@ interface StubConversation {
26
26
  handleSurfaceUndoCalled?: boolean;
27
27
  handleSurfaceUndoThrows?: Error;
28
28
  trustContext?: { trustClass: string; sourceChannel: string };
29
+ /** Drives `isProcessing()`; the route must not stamp trust either way. */
30
+ processing?: boolean;
31
+ /** Trust the route threaded into `handleSurfaceAction`. */
32
+ lastRequesterTrustContext?: { trustClass: string; sourceChannel: string };
29
33
  surfaceActionCalls: Array<{
30
34
  surfaceId: string;
31
35
  actionId: string;
@@ -160,7 +164,9 @@ function makeStub(id: string): StubConversation {
160
164
  actionId: string,
161
165
  data: unknown,
162
166
  sourceActorPrincipalId?: string,
167
+ requesterTrustContext?: { trustClass: string; sourceChannel: string },
163
168
  ) => {
169
+ stub.lastRequesterTrustContext = requesterTrustContext;
164
170
  stub.surfaceActionCalls.push({
165
171
  surfaceId,
166
172
  actionId,
@@ -181,6 +187,7 @@ function makeStub(id: string): StubConversation {
181
187
  setTrustContext: (ctx: { trustClass: string; sourceChannel: string }) => {
182
188
  stub.trustContext = ctx;
183
189
  },
190
+ isProcessing: () => stub.processing === true,
184
191
  });
185
192
  return stub;
186
193
  }
@@ -487,8 +494,8 @@ describe("triggerSurfaceAction trust context", () => {
487
494
  headers: { "x-vellum-actor-principal-id": GUARDIAN_PRINCIPAL },
488
495
  });
489
496
 
490
- expect(live.trustContext?.trustClass).toBe("guardian");
491
- expect(live.trustContext?.sourceChannel).toBe("vellum");
497
+ expect(live.lastRequesterTrustContext?.trustClass).toBe("guardian");
498
+ expect(live.lastRequesterTrustContext?.sourceChannel).toBe("vellum");
492
499
  // First-pass resolve already granted guardian, so the drift helper is skipped.
493
500
  expect(reResolveCalls).toEqual([]);
494
501
  });
@@ -506,7 +513,7 @@ describe("triggerSurfaceAction trust context", () => {
506
513
  });
507
514
 
508
515
  expect(reResolveCalls).toEqual([VELLUM_PRINCIPAL_OLD]);
509
- expect(live.trustContext?.trustClass).toBe("unknown");
516
+ expect(live.lastRequesterTrustContext?.trustClass).toBe("unknown");
510
517
  });
511
518
 
512
519
  test("reset drift: helper returns guardian → route adopts it", async () => {
@@ -522,7 +529,7 @@ describe("triggerSurfaceAction trust context", () => {
522
529
  });
523
530
 
524
531
  expect(reResolveCalls).toEqual([VELLUM_PRINCIPAL_OLD]);
525
- expect(live.trustContext?.trustClass).toBe("guardian");
532
+ expect(live.lastRequesterTrustContext?.trustClass).toBe("guardian");
526
533
  });
527
534
 
528
535
  test("dev-bypass resolves the real guardian principal from the gateway, helper not called", async () => {
@@ -539,7 +546,7 @@ describe("triggerSurfaceAction trust context", () => {
539
546
 
540
547
  // The synthetic dev-bypass principal is translated to the real guardian,
541
548
  // yielding a guardian trust context without consulting the drift helper.
542
- expect(live.trustContext?.trustClass).toBe("guardian");
549
+ expect(live.lastRequesterTrustContext?.trustClass).toBe("guardian");
543
550
  expect(reResolveCalls).toEqual([]);
544
551
  });
545
552
 
@@ -559,7 +566,49 @@ describe("triggerSurfaceAction trust context", () => {
559
566
  headers: { "x-vellum-actor-principal-id": "dev-bypass" },
560
567
  });
561
568
 
562
- expect(live.trustContext?.trustClass).toBe("unknown");
569
+ expect(live.lastRequesterTrustContext?.trustClass).toBe("unknown");
570
+ });
571
+
572
+ test("idle conversation: the requester is threaded to the action and the slot is left to the dispatch", async () => {
573
+ mockGuardianList = [guardianDelivery(GUARDIAN_PRINCIPAL)];
574
+ const live = makeStub("conv-idle");
575
+ live.processing = false;
576
+ memoryBySurface = live;
577
+
578
+ const handler = findHandler("triggerSurfaceAction");
579
+ await handler({
580
+ body: { surfaceId: "surf-idle", actionId: "act-idle" },
581
+ headers: { "x-vellum-actor-principal-id": GUARDIAN_PRINCIPAL },
582
+ });
583
+
584
+ expect(live.lastRequesterTrustContext?.trustClass).toBe("guardian");
585
+ // The route resolves; `handleSurfaceAction` stamps, and only where it
586
+ // commits to running the turn. Stamping here would repoint the actor of a
587
+ // turn that starts between this call and the dispatch.
588
+ expect(live.trustContext).toBeUndefined();
589
+ });
590
+
591
+ test("busy conversation: the running turn's actor is left alone and the click still travels as its own actor", async () => {
592
+ mockGuardianList = [guardianDelivery(GUARDIAN_PRINCIPAL)];
593
+ const live = makeStub("conv-busy");
594
+ // A turn is already running under a different actor.
595
+ const runningTurnActor = {
596
+ trustClass: "untrusted",
597
+ sourceChannel: "sms",
598
+ };
599
+ live.trustContext = runningTurnActor;
600
+ live.processing = true;
601
+ memoryBySurface = live;
602
+
603
+ const handler = findHandler("triggerSurfaceAction");
604
+ await handler({
605
+ body: { surfaceId: "surf-busy", actionId: "act-busy" },
606
+ headers: { "x-vellum-actor-principal-id": GUARDIAN_PRINCIPAL },
607
+ });
608
+
609
+ // Identical object, so a re-write of an equal value still fails this.
610
+ expect(live.trustContext).toBe(runningTurnActor);
611
+ expect(live.lastRequesterTrustContext?.trustClass).toBe("guardian");
563
612
  });
564
613
  });
565
614
 
@@ -3,7 +3,10 @@ import { join } from "node:path";
3
3
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
4
4
 
5
5
  import type { RouteInvokeParams } from "../../../routes/route-host-protocol.js";
6
- import { getWorkspaceRoutesDir } from "../../../util/platform.js";
6
+ import {
7
+ getWorkspacePluginsDir,
8
+ getWorkspaceRoutesDir,
9
+ } from "../../../util/platform.js";
7
10
 
8
11
  // The dispatcher constructs the route host client inline and reads the enabled
9
12
  // flag from config, so both are mocked here (there is no injection seam).
@@ -58,6 +61,16 @@ function writeHandler(name: string, content: string): void {
58
61
  writeFileSync(join(getWorkspaceRoutesDir(), name), content);
59
62
  }
60
63
 
64
+ function writePluginHandler(
65
+ pluginName: string,
66
+ name: string,
67
+ content: string,
68
+ ): void {
69
+ const dir = join(getWorkspacePluginsDir(), pluginName, "routes");
70
+ mkdirSync(dir, { recursive: true });
71
+ writeFileSync(join(dir, name), content);
72
+ }
73
+
61
74
  function jsonResponse(status: number, value: unknown): RouteInvokeResponse {
62
75
  return {
63
76
  status,
@@ -75,6 +88,7 @@ beforeEach(() => {
75
88
 
76
89
  afterEach(() => {
77
90
  rmSync(getWorkspaceRoutesDir(), { recursive: true, force: true });
91
+ rmSync(getWorkspacePluginsDir(), { recursive: true, force: true });
78
92
  });
79
93
 
80
94
  describe("UserRouteDispatcher — route host delegation", () => {
@@ -101,6 +115,43 @@ describe("UserRouteDispatcher — route host delegation", () => {
101
115
  expect(invokeCalls[0].params.url).toContain("/v1/x/foo");
102
116
  });
103
117
 
118
+ test("names the owning plugin so the host executes in its context", async () => {
119
+ // The host marks this plugin as in context for the handler, so
120
+ // plugin-scoped host APIs behave the same in the host and in-thread.
121
+ writePluginHandler(
122
+ "my-plugin",
123
+ "status.ts",
124
+ `export function GET() { return new Response(); }`,
125
+ );
126
+ hostEnabled = true;
127
+ const dispatcher = makeDispatcher();
128
+
129
+ await dispatcher.dispatch(
130
+ "plugins/my-plugin/status",
131
+ new Request("http://localhost/v1/x/plugins/my-plugin/status", {
132
+ method: "GET",
133
+ }),
134
+ );
135
+
136
+ expect(invokeCalls[0].params.pluginName).toBe("my-plugin");
137
+ });
138
+
139
+ test("leaves a workspace route unowned", async () => {
140
+ writeHandler(
141
+ "plain.ts",
142
+ `export function GET() { return new Response(); }`,
143
+ );
144
+ hostEnabled = true;
145
+ const dispatcher = makeDispatcher();
146
+
147
+ await dispatcher.dispatch(
148
+ "plain",
149
+ new Request("http://localhost/v1/x/plain", { method: "GET" }),
150
+ );
151
+
152
+ expect(invokeCalls[0].params.pluginName).toBeUndefined();
153
+ });
154
+
104
155
  test("runs in-band when disabled (host never called)", async () => {
105
156
  writeHandler(
106
157
  "bar.ts",
@@ -1,5 +1,5 @@
1
1
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { dirname, join } from "node:path";
3
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
4
 
5
5
  // ---------------------------------------------------------------------------
@@ -56,6 +56,15 @@ function writePluginHandler(
56
56
  return fullPath;
57
57
  }
58
58
 
59
+ /**
60
+ * Absolute specifier for the plugin execution context, so a handler written to
61
+ * a temp routes directory imports the same module instance this process holds.
62
+ */
63
+ const PLUGIN_CONTEXT_MODULE = new URL(
64
+ "../../../plugins/plugin-execution-context.ts",
65
+ import.meta.url,
66
+ ).href;
67
+
59
68
  async function readErrorBody(
60
69
  response: Response,
61
70
  ): Promise<{ error: { code: string; message: string } }> {
@@ -466,6 +475,71 @@ describe("plugin routes", () => {
466
475
  expect(body.plugin).toBe(true);
467
476
  });
468
477
 
478
+ test("executes the handler as the plugin that owns the namespace", async () => {
479
+ // Plugin-scoped host APIs (resolveCredential, indexDocument) read the
480
+ // execution context to decide what the caller may touch. A route handler
481
+ // that ran outside it would fall through to their unscoped branch and
482
+ // reach every plugin's credentials.
483
+ writePluginHandler(
484
+ "my-plugin",
485
+ "whoami.ts",
486
+ `import { getCurrentPluginName } from ${JSON.stringify(PLUGIN_CONTEXT_MODULE)};
487
+ export function GET(request) {
488
+ return Response.json({ plugin: getCurrentPluginName() ?? null });
489
+ }`,
490
+ );
491
+
492
+ const dispatcher = makeDispatcher();
493
+ const res = await dispatcher.dispatch(
494
+ "plugins/my-plugin/whoami",
495
+ makeRequest("GET"),
496
+ );
497
+ expect(res.status).toBe(200);
498
+ const body = await res.json();
499
+ expect(body.plugin).toBe("my-plugin");
500
+ });
501
+
502
+ test("evaluates the route module in the plugin's context too", async () => {
503
+ // A module can reach a scoped host API at import time, and the import
504
+ // happens once per mtime. A load left outside the context would take the
505
+ // unscoped branch and never be re-run scoped, so the context has to cover
506
+ // module evaluation and not just the handler call.
507
+ writePluginHandler(
508
+ "my-plugin",
509
+ "at-import.ts",
510
+ `import { getCurrentPluginName } from ${JSON.stringify(PLUGIN_CONTEXT_MODULE)};
511
+ const atImport = getCurrentPluginName() ?? null;
512
+ export function GET(request) {
513
+ return Response.json({ atImport });
514
+ }`,
515
+ );
516
+
517
+ const dispatcher = makeDispatcher();
518
+ const res = await dispatcher.dispatch(
519
+ "plugins/my-plugin/at-import",
520
+ makeRequest("GET"),
521
+ );
522
+ expect(res.status).toBe(200);
523
+ const body = await res.json();
524
+ expect(body.atImport).toBe("my-plugin");
525
+ });
526
+
527
+ test("leaves a workspace route outside any plugin context", async () => {
528
+ writeHandler(
529
+ "whoami.ts",
530
+ `import { getCurrentPluginName } from ${JSON.stringify(PLUGIN_CONTEXT_MODULE)};
531
+ export function GET(request) {
532
+ return Response.json({ plugin: getCurrentPluginName() ?? null });
533
+ }`,
534
+ );
535
+
536
+ const dispatcher = makeDispatcher();
537
+ const res = await dispatcher.dispatch("whoami", makeRequest("GET"));
538
+ expect(res.status).toBe(200);
539
+ const body = await res.json();
540
+ expect(body.plugin).toBeNull();
541
+ });
542
+
469
543
  test("resolves nested paths and the index (namespace root)", async () => {
470
544
  writePluginHandler(
471
545
  "my-plugin",
@@ -574,4 +648,62 @@ describe("plugin routes", () => {
574
648
  );
575
649
  expect(other.status).toBe(404);
576
650
  });
651
+
652
+ test("a new route export is not bound to a stale helper after upgrade", async () => {
653
+ // Reproduces the browser plugin 500: `/frame` loaded `src/http.ts`
654
+ // first, then an upgrade added `requireNumber` to http.ts and a new
655
+ // `/input` that imports it. Cache-busting only the entry file rebound
656
+ // input.ts to the cached http.ts, which had no such export.
657
+ const plugin = "stale-helper";
658
+ const helperPath = join(getWorkspacePluginsDir(), plugin, "src", "http.ts");
659
+ mkdirSync(dirname(helperPath), { recursive: true });
660
+ writeFileSync(helperPath, `export function label() { return "v1"; }\n`);
661
+ writePluginHandler(
662
+ plugin,
663
+ "frame.ts",
664
+ `import { label } from "../src/http.js";
665
+ export function GET() { return Response.json({ label: label() }); }`,
666
+ );
667
+ writePluginHandler(
668
+ plugin,
669
+ "input.ts",
670
+ `import { label } from "../src/http.js";
671
+ export function POST() { return Response.json({ label: label() }); }`,
672
+ );
673
+
674
+ const dispatcher = makeDispatcher();
675
+ const frame1 = await dispatcher.dispatch(
676
+ `plugins/${plugin}/frame`,
677
+ makeRequest("GET"),
678
+ );
679
+ expect(frame1.status).toBe(200);
680
+ expect((await frame1.json()).label).toBe("v1");
681
+
682
+ await new Promise((resolve) => setTimeout(resolve, 50));
683
+ writeFileSync(
684
+ helperPath,
685
+ `export function label() { return "v2"; }
686
+ export function extra() { return true; }\n`,
687
+ );
688
+ writePluginHandler(
689
+ plugin,
690
+ "input.ts",
691
+ `import { extra } from "../src/http.js";
692
+ export function POST() { return Response.json({ extra: extra() }); }`,
693
+ );
694
+
695
+ const input = await dispatcher.dispatch(
696
+ `plugins/${plugin}/input`,
697
+ makeRequest("POST"),
698
+ );
699
+ expect(input.status).toBe(200);
700
+ expect((await input.json()).extra).toBe(true);
701
+
702
+ const frame2 = await dispatcher.dispatch(
703
+ `plugins/${plugin}/frame`,
704
+ makeRequest("GET"),
705
+ );
706
+ expect(frame2.status).toBe(200);
707
+ expect((await frame2.json()).label).toBe("v2");
708
+ });
577
709
  });
@@ -4,11 +4,11 @@
4
4
  * GET /v1/channels/available — return the channels this assistant can
5
5
  * surface to clients (Contacts / GuardianChannels views, etc.) along with
6
6
  * their display metadata (label, subtitle, icon, verification capability,
7
- * setup-message copy). Today this is a fixed base list plus `email` when
8
- * an inbox is registered; eventually the list will be driven by
9
- * plugins/skills the assistant has loaded. Clients should treat the
10
- * response as authoritative and stop carrying their own per-channel
11
- * switches.
7
+ * setup-message copy). A fixed base list, plus `email` when an inbox is
8
+ * registered, plus the channels installed plugins bring by declaring
9
+ * ingress. One list, because a plugin channel is a channel; each row's
10
+ * `source` says who contributes it. Clients should treat the response as
11
+ * authoritative and stop carrying their own per-channel switches.
12
12
  *
13
13
  * Distinct from `/v1/channels/readiness` (which answers "is this channel
14
14
  * configured and working?"). Availability answers "could this channel be
@@ -18,8 +18,9 @@
18
18
  import { z } from "zod";
19
19
 
20
20
  import { isA2AEnabled } from "../../a2a/feature-gate.js";
21
+ import { discoverPluginChannels } from "../../channels/plugin-channel-declarations.js";
21
22
  import {
22
- CHANNEL_IDS,
23
+ type AvailableChannel,
23
24
  CHANNEL_METADATA,
24
25
  type ChannelId,
25
26
  type ChannelInfo,
@@ -73,7 +74,7 @@ async function hasRegisteredInbox(): Promise<boolean> {
73
74
 
74
75
  async function handleGetChannelAvailability(
75
76
  _args: RouteHandlerArgs,
76
- ): Promise<{ channels: ChannelInfo[] }> {
77
+ ): Promise<{ channels: AvailableChannel[] }> {
77
78
  const ids: ChannelId[] = [...BASE_AVAILABLE_CHANNELS];
78
79
  if (await hasRegisteredInbox()) {
79
80
  ids.push("email");
@@ -86,14 +87,26 @@ async function handleGetChannelAvailability(
86
87
  // contains channels that BASE_AVAILABLE_CHANNELS / the email branch
87
88
  // explicitly chose, so the lookup is always defined — filter to satisfy
88
89
  // the type system without a non-null assertion.
89
- const channels = ids
90
+ const builtIn: AvailableChannel[] = ids
90
91
  .map((id) => CHANNEL_METADATA[id])
91
- .filter((info): info is ChannelInfo => info !== undefined);
92
- return { channels };
92
+ .filter((info): info is ChannelInfo => info !== undefined)
93
+ .map((info) => ({ ...info, source: "default" as const }));
94
+ // One list, because a plugin channel is a channel: what differs is who
95
+ // contributes it, which is what `source` says. Built-ins lead, so a client
96
+ // rendering in order gets a stable list that plugin installs append to.
97
+ return { channels: [...builtIn, ...(await discoverPluginChannels())] };
93
98
  }
94
99
 
95
100
  const channelInfoSchema = z.object({
96
- id: z.enum(CHANNEL_IDS),
101
+ // A string rather than the channel enum: a plugin channel's id is its
102
+ // plugin name, and `source` is what says which kind a row is.
103
+ id: z.string(),
104
+ source: z
105
+ .string()
106
+ .describe(
107
+ "`default` for a channel the assistant ships, `plugin:<name>` for one " +
108
+ "an installed plugin brings",
109
+ ),
97
110
  label: z.string(),
98
111
  subtitle: z.string(),
99
112
  icon: z.string(),
@@ -117,14 +130,19 @@ export const ROUTES: RouteDefinition[] = [
117
130
  description:
118
131
  "Return the channels this assistant can surface to clients, with " +
119
132
  "display metadata (label, icon, verification capability, setup " +
120
- "copy). Today this is a fixed base list plus `email` when an inbox " +
121
- "is registered; will become plugin/skill-driven in future.",
133
+ "copy). A fixed base list plus `email` when an inbox is registered, " +
134
+ "plus the channels installed plugins bring by declaring ingress " +
135
+ "routes. Each carries a `source` saying which of those it is.",
122
136
  tags: ["channels"],
123
137
  handler: handleGetChannelAvailability,
124
138
  responseBody: z.object({
125
139
  channels: z
126
140
  .array(channelInfoSchema)
127
- .describe("Available channels in display order"),
141
+ .describe(
142
+ "Available channels in display order: the ones the assistant " +
143
+ "ships, then the ones installed plugins bring by declaring " +
144
+ "ingress routes. `source` distinguishes them.",
145
+ ),
128
146
  }),
129
147
  },
130
148
  ];