@vellumai/assistant 0.12.1 → 0.12.2-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 (577) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  6. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  7. package/openapi.yaml +360 -51
  8. package/package.json +3 -1
  9. package/scripts/smoke-desktop-window-theme.ts +186 -0
  10. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  11. package/src/__tests__/anthropic-provider.test.ts +0 -63
  12. package/src/__tests__/app-compiler.test.ts +6 -0
  13. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  14. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  15. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  16. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  17. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  18. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  19. package/src/__tests__/computer-use-tools.test.ts +19 -7
  20. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  21. package/src/__tests__/config-schema.test.ts +28 -29
  22. package/src/__tests__/config-watcher.test.ts +23 -8
  23. package/src/__tests__/connection-model-compat.test.ts +2 -2
  24. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-pairing.test.ts +26 -0
  26. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  27. package/src/__tests__/conversation-queue.test.ts +48 -207
  28. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  30. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  31. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  32. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  35. package/src/__tests__/conversation-title-service.test.ts +14 -10
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  37. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  38. package/src/__tests__/credential-record-retire.test.ts +171 -1
  39. package/src/__tests__/credential-routes.test.ts +250 -3
  40. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  41. package/src/__tests__/cu-unified-flow.test.ts +14 -4
  42. package/src/__tests__/document-sync-tags.test.ts +25 -0
  43. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  45. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  46. package/src/__tests__/git-porcelain.test.ts +47 -0
  47. package/src/__tests__/heartbeat-service.test.ts +56 -40
  48. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  49. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  50. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  51. package/src/__tests__/host-cu-proxy.test.ts +596 -0
  52. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  53. package/src/__tests__/host-file-proxy.test.ts +11 -23
  54. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  55. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  56. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  57. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  58. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  59. package/src/__tests__/mcp-health-check.test.ts +8 -11
  60. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  61. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  62. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  63. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  64. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  65. package/src/__tests__/model-intents.test.ts +3 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  67. package/src/__tests__/oauth-commands-routes.test.ts +168 -0
  68. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  69. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  70. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  71. package/src/__tests__/openai-provider.test.ts +1 -0
  72. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  73. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  74. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  75. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  76. package/src/__tests__/qdrant-manager.test.ts +132 -0
  77. package/src/__tests__/resolve-app.test.ts +135 -0
  78. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  79. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  80. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  81. package/src/__tests__/service-account-token.test.ts +247 -0
  82. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  83. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +9 -4
  85. package/src/__tests__/source-fingerprint.test.ts +184 -0
  86. package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
  87. package/src/__tests__/subagent-tools.test.ts +3 -2
  88. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  89. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  90. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  91. package/src/__tests__/unicode.test.ts +13 -0
  92. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  93. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  94. package/src/__tests__/workspace-git-service.test.ts +26 -0
  95. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  97. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  98. package/src/acp/__tests__/agent-process.test.ts +258 -4
  99. package/src/acp/__tests__/auth-required.test.ts +96 -0
  100. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  101. package/src/acp/__tests__/client-handler.test.ts +90 -2
  102. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  103. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  104. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  105. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  106. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  107. package/src/acp/__tests__/session-manager.test.ts +895 -4
  108. package/src/acp/acp-claude-oauth.ts +328 -14
  109. package/src/acp/acp-credentials.ts +19 -0
  110. package/src/acp/agent-process.test.ts +4 -1
  111. package/src/acp/agent-process.ts +96 -10
  112. package/src/acp/auth-required.ts +50 -1
  113. package/src/acp/auto-install.test.ts +9 -7
  114. package/src/acp/claude-token-refresh.ts +150 -0
  115. package/src/acp/client-handler.ts +16 -3
  116. package/src/acp/model-config.test.ts +166 -0
  117. package/src/acp/model-config.ts +138 -0
  118. package/src/acp/prepare-agent-env.ts +21 -6
  119. package/src/acp/resolve-agent.test.ts +220 -6
  120. package/src/acp/resolve-agent.ts +88 -23
  121. package/src/acp/session-manager.test.ts +196 -3
  122. package/src/acp/session-manager.ts +441 -18
  123. package/src/acp/types.ts +50 -5
  124. package/src/agent/loop.ts +1 -0
  125. package/src/api/events/acp-session-model-update.test.ts +88 -0
  126. package/src/api/events/acp-session-model-update.ts +46 -0
  127. package/src/api/events/app-files-changed.ts +4 -3
  128. package/src/api/events/notification-intent.ts +2 -0
  129. package/src/api/events/user-message-echo.ts +7 -1
  130. package/src/api/index.ts +6 -0
  131. package/src/api/responses/conversation-message.ts +5 -0
  132. package/src/apps/app-store.ts +1 -1
  133. package/src/apps/resolve-app.ts +81 -0
  134. package/src/apps/source-fingerprint.ts +266 -0
  135. package/src/avatar/identity-avatar.ts +7 -2
  136. package/src/bundler/app-compiler.ts +15 -0
  137. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  138. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  139. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  140. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  141. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  142. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  143. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  144. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  145. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  146. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  147. package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
  148. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  149. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  150. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  151. package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
  152. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  153. package/src/calls/barge-in-guard.ts +118 -0
  154. package/src/calls/call-constants.ts +24 -4
  155. package/src/calls/call-controller.ts +499 -199
  156. package/src/calls/call-transport.ts +16 -0
  157. package/src/calls/media-stream-output.ts +32 -0
  158. package/src/calls/media-stream-server.ts +82 -30
  159. package/src/calls/media-stream-stt-session.ts +15 -0
  160. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  161. package/src/calls/resolve-call-tts-provider.ts +33 -2
  162. package/src/calls/tts-call-strategy.ts +9 -5
  163. package/src/calls/types.ts +2 -0
  164. package/src/calls/voice-control-protocol.ts +155 -21
  165. package/src/calls/voice-leg-coordinator.ts +189 -0
  166. package/src/calls/voice-progress-cadence.ts +423 -0
  167. package/src/calls/voice-session-bridge.ts +92 -72
  168. package/src/calls/voice-triage-escalate.ts +178 -59
  169. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  170. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  171. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  172. package/src/cli/commands/apps.help.ts +67 -13
  173. package/src/cli/commands/apps.ts +183 -0
  174. package/src/cli/commands/bash.help.ts +8 -13
  175. package/src/cli/commands/bash.ts +7 -25
  176. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  177. package/src/cli/commands/channels/index.help.ts +70 -2
  178. package/src/cli/commands/channels/index.ts +16 -3
  179. package/src/cli/commands/channels/send.ts +128 -0
  180. package/src/cli/commands/credentials.help.ts +6 -2
  181. package/src/cli/commands/credentials.ts +2 -2
  182. package/src/cli/commands/mcp.help.ts +8 -8
  183. package/src/cli/commands/mcp.ts +1 -1
  184. package/src/cli/commands/oauth/status.ts +5 -0
  185. package/src/cli/commands/roadmap.help.ts +5 -1
  186. package/src/cli/commands/roadmap.ts +1 -1
  187. package/src/cli/lib/bundled-marketplace.json +103 -90
  188. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  189. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  190. package/src/config/__tests__/platform-identity.test.ts +255 -0
  191. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  192. package/src/config/acp-agent-write.ts +43 -0
  193. package/src/config/acp-defaults.test.ts +13 -3
  194. package/src/config/acp-defaults.ts +23 -6
  195. package/src/config/acp-schema.test.ts +81 -0
  196. package/src/config/acp-schema.ts +30 -2
  197. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  198. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  199. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  200. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  201. package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
  202. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  203. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  204. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  205. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  206. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  207. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  208. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  209. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  210. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  211. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  212. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  213. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  214. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  215. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  216. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  217. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  218. package/src/config/default-profile-catalog.ts +1 -1
  219. package/src/config/env.ts +2 -2
  220. package/src/config/feature-flag-registry.json +33 -13
  221. package/src/config/loader.ts +2 -0
  222. package/src/config/memory-v3-gate.test.ts +31 -0
  223. package/src/config/memory-v3-gate.ts +20 -16
  224. package/src/config/platform-identity.ts +201 -0
  225. package/src/config/platform-rehydration.ts +11 -27
  226. package/src/config/schema.ts +2 -2
  227. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  228. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  229. package/src/config/schemas/calls.ts +1 -28
  230. package/src/config/schemas/live-voice.ts +1 -173
  231. package/src/config/schemas/mcp.ts +1 -1
  232. package/src/config/schemas/memory-retrospective.ts +10 -0
  233. package/src/config/schemas/services.ts +18 -0
  234. package/src/config/schemas/skills.ts +0 -14
  235. package/src/config/schemas/voice.ts +150 -0
  236. package/src/credential-execution/process-manager.test.ts +18 -19
  237. package/src/credential-health/credential-health-service.ts +11 -17
  238. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  239. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  240. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  241. package/src/daemon/app-change-notify.ts +45 -0
  242. package/src/daemon/config-watcher.ts +13 -194
  243. package/src/daemon/conversation-error.ts +3 -2
  244. package/src/daemon/conversation-media-retry.ts +2 -1
  245. package/src/daemon/conversation-process.ts +35 -64
  246. package/src/daemon/conversation-runtime-assembly.ts +20 -7
  247. package/src/daemon/conversation-skill-tools.ts +13 -31
  248. package/src/daemon/conversation-store.ts +1 -1
  249. package/src/daemon/conversation-surfaces.ts +21 -10
  250. package/src/daemon/conversation-tool-setup.ts +45 -59
  251. package/src/daemon/conversation.ts +0 -3
  252. package/src/daemon/dictation-profile-store.ts +2 -1
  253. package/src/daemon/handlers/shared.ts +2 -1
  254. package/src/daemon/handlers/skills.ts +6 -3
  255. package/src/daemon/host-bash-proxy.ts +8 -12
  256. package/src/daemon/host-browser-proxy.ts +15 -1
  257. package/src/daemon/host-cu-proxy.ts +224 -19
  258. package/src/daemon/host-file-proxy.ts +8 -10
  259. package/src/daemon/host-proxy-preactivation.ts +2 -1
  260. package/src/daemon/host-transfer-proxy.ts +14 -18
  261. package/src/daemon/lifecycle.ts +0 -5
  262. package/src/daemon/mcp-reload-service.ts +5 -5
  263. package/src/daemon/message-types/sync.ts +1 -0
  264. package/src/daemon/orphan-reaper.ts +1 -1
  265. package/src/daemon/providers-setup.ts +1 -1
  266. package/src/daemon/reaction-history-render.ts +2 -1
  267. package/src/daemon/shutdown-handlers.ts +1 -9
  268. package/src/daemon/skill-memory-refresh.ts +64 -0
  269. package/src/daemon/tool-side-effects.ts +7 -18
  270. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  271. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  272. package/src/desktop/desktop-chrome-policy.ts +47 -0
  273. package/src/desktop/desktop-chrome-session.ts +37 -0
  274. package/src/desktop/desktop-dependencies.test.ts +102 -0
  275. package/src/desktop/desktop-dependencies.ts +308 -0
  276. package/src/desktop/desktop-feature.test.ts +18 -0
  277. package/src/desktop/desktop-feature.ts +19 -0
  278. package/src/desktop/desktop-panel-config.test.ts +135 -0
  279. package/src/desktop/desktop-panel-config.ts +149 -0
  280. package/src/desktop/desktop-session-manager.test.ts +730 -0
  281. package/src/desktop/desktop-session-manager.ts +850 -0
  282. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  283. package/src/desktop/desktop-stream-bridge.ts +214 -0
  284. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  285. package/src/desktop/desktop-wallpaper.ts +113 -0
  286. package/src/desktop/desktop-window-drag.ts +177 -0
  287. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  288. package/src/desktop/desktop-window-manager-config.ts +238 -0
  289. package/src/desktop/desktop-window-theme.ts +170 -0
  290. package/src/heartbeat/heartbeat-service.ts +21 -3
  291. package/src/home/__tests__/feed-writer.test.ts +22 -0
  292. package/src/home/feed-writer.ts +11 -2
  293. package/src/hooks/types.ts +33 -0
  294. package/src/i18n/AGENTS.md +22 -0
  295. package/src/i18n/__tests__/i18n.test.ts +108 -0
  296. package/src/i18n/index.ts +16 -0
  297. package/src/i18n/locales.ts +93 -0
  298. package/src/i18n/messages.ts +61 -0
  299. package/src/i18n/title.ts +25 -0
  300. package/src/inbound/platform-callback-registration.ts +4 -5
  301. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  302. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  303. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  304. package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
  305. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  306. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  307. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  308. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  309. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  310. package/src/live-voice/live-voice-photo.ts +25 -6
  311. package/src/live-voice/live-voice-session.ts +297 -699
  312. package/src/live-voice/protocol.ts +60 -2
  313. package/src/live-voice/session-controls.ts +111 -0
  314. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  315. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  316. package/src/mcp/effective-config.ts +3 -3
  317. package/src/mcp/mcp-header-store.ts +29 -45
  318. package/src/mcp/spec-schema.ts +111 -0
  319. package/src/mcp/startup.ts +8 -4
  320. package/src/mcp/workspace-mcp-config.ts +188 -0
  321. package/src/messaging/provider.ts +10 -3
  322. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  323. package/src/messaging/providers/callback-routing.ts +18 -0
  324. package/src/messaging/providers/channel-transport.ts +56 -0
  325. package/src/messaging/providers/discord/transport.ts +17 -0
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  327. package/src/messaging/providers/slack/adapter.ts +7 -44
  328. package/src/messaging/providers/slack/transport.ts +16 -0
  329. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  330. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  331. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  332. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  333. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  334. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  335. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  336. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  337. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  338. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  339. package/src/notifications/adapters/macos.ts +3 -0
  340. package/src/notifications/assistant-reply-producer.ts +8 -3
  341. package/src/notifications/conversation-pairing.ts +36 -16
  342. package/src/notifications/delivered-post-record.ts +11 -1
  343. package/src/notifications/guardian-feed-projection.ts +125 -5
  344. package/src/notifications/home-feed-side-effect.ts +55 -7
  345. package/src/notifications/schedule-result-producer.ts +66 -22
  346. package/src/oauth/AGENTS.md +2 -0
  347. package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
  348. package/src/oauth/byo-connection.test.ts +105 -0
  349. package/src/oauth/byo-connection.ts +36 -6
  350. package/src/oauth/connection-resolver.test.ts +100 -0
  351. package/src/oauth/connection-resolver.ts +120 -3
  352. package/src/oauth/provider-serializer.ts +9 -0
  353. package/src/oauth/scope-utils.ts +24 -0
  354. package/src/oauth/seed-providers.ts +314 -27
  355. package/src/outbound-proxy/certs.ts +3 -5
  356. package/src/permissions/checker.test.ts +11 -6
  357. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  358. package/src/persistence/conversation-crud.ts +71 -63
  359. package/src/persistence/conversation-lineage.ts +12 -4
  360. package/src/persistence/conversation-queries.ts +2 -2
  361. package/src/persistence/conversation-title-placeholders.ts +23 -0
  362. package/src/persistence/conversation-title-service.ts +9 -26
  363. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  364. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  365. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  366. package/src/persistence/message-cursor.ts +78 -0
  367. package/src/persistence/message-reads.ts +30 -0
  368. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  369. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  370. package/src/persistence/schema/memory-core.ts +5 -0
  371. package/src/platform/client.test.ts +4 -2
  372. package/src/platform/client.ts +2 -10
  373. package/src/plugin-api/constants.ts +2 -0
  374. package/src/plugin-api/index.ts +1 -0
  375. package/src/plugin-api/types.ts +2 -0
  376. package/src/plugins/defaults/index.ts +2 -0
  377. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  378. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  379. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  380. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  381. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  382. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  383. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  384. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  385. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  386. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  387. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  388. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  389. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  390. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  391. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  392. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  393. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  394. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  395. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  396. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  397. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  398. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  399. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  400. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  401. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  402. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  403. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  404. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  405. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  406. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  407. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  408. package/src/plugins/defaults/memory/startup.ts +28 -2
  409. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  410. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  411. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  412. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  413. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  414. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  415. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  416. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  417. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  418. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  419. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  420. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  421. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  422. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  423. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  424. package/src/plugins/defaults/memory/worker.ts +6 -0
  425. package/src/plugins/mcp-servers.ts +15 -75
  426. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  427. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  428. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  429. package/src/providers/anthropic/client.ts +0 -47
  430. package/src/providers/gemini/client.ts +9 -23
  431. package/src/providers/gemini-thought-signature.test.ts +63 -0
  432. package/src/providers/gemini-thought-signature.ts +44 -0
  433. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  434. package/src/providers/inference/auth.ts +2 -3
  435. package/src/providers/inference/connection-availability.ts +27 -8
  436. package/src/providers/inference/endpoint-probe.ts +10 -1
  437. package/src/providers/inference/resolve-auth.ts +22 -4
  438. package/src/providers/inference/service-account-token.ts +248 -0
  439. package/src/providers/model-catalog.ts +34 -0
  440. package/src/providers/model-intents.ts +10 -5
  441. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  442. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  443. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  444. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  445. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  446. package/src/providers/openai/chat-completions-provider.ts +212 -79
  447. package/src/providers/openai/codex-models.ts +0 -4
  448. package/src/providers/openai/google-thought-signature.ts +320 -0
  449. package/src/providers/opencode/client.test.ts +73 -4
  450. package/src/providers/opencode/client.ts +60 -3
  451. package/src/providers/openrouter/client.ts +20 -0
  452. package/src/providers/outbound-request-sanitize.ts +96 -0
  453. package/src/providers/retry.ts +30 -19
  454. package/src/providers/unparseable-tool-args.ts +5 -1
  455. package/src/runtime/AGENTS.md +3 -1
  456. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  457. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  458. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  459. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  460. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  461. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  462. package/src/runtime/agent-wake.ts +2 -1
  463. package/src/runtime/assistant-event-hub.ts +21 -3
  464. package/src/runtime/auth/same-actor.ts +38 -3
  465. package/src/runtime/channel-send.ts +316 -0
  466. package/src/runtime/host-observe.ts +11 -1
  467. package/src/runtime/http-server.ts +160 -151
  468. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  469. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  470. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
  471. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  472. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  473. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  474. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  475. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  476. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  477. package/src/runtime/routes/acp-routes.test.ts +15 -0
  478. package/src/runtime/routes/acp-routes.ts +72 -9
  479. package/src/runtime/routes/app-management-routes.ts +90 -2
  480. package/src/runtime/routes/app-routes.ts +1 -1
  481. package/src/runtime/routes/channel-send-routes.ts +85 -0
  482. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  483. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  484. package/src/runtime/routes/conversation-routes.ts +16 -3
  485. package/src/runtime/routes/credential-routes.ts +20 -14
  486. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  487. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  488. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  489. package/src/runtime/routes/events-routes.ts +4 -0
  490. package/src/runtime/routes/host-cu-routes.ts +6 -0
  491. package/src/runtime/routes/index.ts +4 -2
  492. package/src/runtime/routes/log-export-routes.ts +2 -0
  493. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  494. package/src/runtime/routes/migration-routes.ts +6 -20
  495. package/src/runtime/routes/oauth-commands-routes.ts +50 -0
  496. package/src/runtime/routes/oauth-providers.ts +7 -0
  497. package/src/runtime/routes/platform-routes.ts +18 -15
  498. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  499. package/src/runtime/routes/workspace-routes.ts +46 -63
  500. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  501. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  502. package/src/runtime/routes/workspace-utils.ts +17 -0
  503. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  504. package/src/runtime/services/conversation-serializer.ts +16 -3
  505. package/src/runtime/sync/resource-sync-events.ts +2 -1
  506. package/src/schedule/run-script.ts +2 -1
  507. package/src/schedule/schedule-store.ts +9 -2
  508. package/src/schedule/worker.ts +1 -1
  509. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  510. package/src/security/ces-http-record-backend.ts +120 -0
  511. package/src/security/ces-rpc-record-backend.ts +7 -0
  512. package/src/security/untrusted-content.ts +3 -1
  513. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  514. package/src/skills/workspace-skill-md-signature.ts +50 -0
  515. package/src/subagent/consult-context.ts +12 -25
  516. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  517. package/src/tools/acp/list-agents.test.ts +3 -1
  518. package/src/tools/acp/spawn.test.ts +146 -11
  519. package/src/tools/acp/spawn.ts +21 -3
  520. package/src/tools/acp/status.test.ts +94 -0
  521. package/src/tools/acp/status.ts +40 -3
  522. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  523. package/src/tools/browser/browser-execution.ts +1 -1
  524. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  525. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  526. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  527. package/src/tools/browser/network-recorder.ts +2 -1
  528. package/src/tools/capability-offer.test.ts +22 -2
  529. package/src/tools/capability-offer.ts +16 -1
  530. package/src/tools/computer-use/definitions.ts +121 -2
  531. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  532. package/src/tools/credentials/metadata-store.ts +100 -0
  533. package/src/tools/filesystem/search.ts +2 -1
  534. package/src/tools/host-filesystem/edit.test.ts +2 -1
  535. package/src/tools/host-filesystem/edit.ts +2 -2
  536. package/src/tools/host-filesystem/read.test.ts +2 -1
  537. package/src/tools/host-filesystem/read.ts +2 -2
  538. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  539. package/src/tools/host-filesystem/transfer.ts +2 -2
  540. package/src/tools/host-filesystem/write.test.ts +2 -1
  541. package/src/tools/host-filesystem/write.ts +2 -2
  542. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  543. package/src/tools/host-terminal/host-shell.ts +4 -20
  544. package/src/tools/network/web-search.ts +6 -1
  545. package/src/tools/policy-context.ts +6 -7
  546. package/src/tools/skills/scaffold-managed.ts +61 -3
  547. package/src/tools/subagent/spawn.ts +5 -8
  548. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  549. package/src/tools/terminal/sanitized-bash.ts +105 -0
  550. package/src/tools/ui-surface/channel-variants.ts +10 -59
  551. package/src/tools/watch/watch-retro-report.ts +0 -8
  552. package/src/types/host-capabilities.ts +5 -3
  553. package/src/util/__tests__/language-subtag.test.ts +23 -1
  554. package/src/util/ca-bundle.ts +13 -0
  555. package/src/util/language-subtag.ts +16 -0
  556. package/src/util/platform.ts +8 -0
  557. package/src/util/unicode.ts +15 -1
  558. package/src/watch/watch-retro.ts +6 -8
  559. package/src/watch/watch-timeline.ts +2 -2
  560. package/src/workflows/run-manager.ts +2 -1
  561. package/src/workspace/git-porcelain.ts +66 -0
  562. package/src/workspace/git-service.ts +6 -55
  563. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  564. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  565. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  566. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  567. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  568. package/src/workspace/migrations/registry.ts +6 -0
  569. package/src/__tests__/app-source-watcher.test.ts +0 -218
  570. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  571. package/src/__tests__/speaker-identification.test.ts +0 -56
  572. package/src/calls/speaker-identification.ts +0 -233
  573. package/src/daemon/app-source-watcher.ts +0 -218
  574. package/src/messaging/providers/whatsapp/client.ts +0 -29
  575. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  576. package/src/util/fs-watcher-error.ts +0 -36
  577. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -32,6 +32,7 @@ import { getAttentionStateByConversationIds } from "../persistence/conversation-
32
32
  import {
33
33
  getAssistantMessageIdsInTurn,
34
34
  getMessageById,
35
+ getMessagesAfter,
35
36
  type MessageRow,
36
37
  } from "../persistence/conversation-crud.js";
37
38
  import { stringifyMessageContent } from "../persistence/message-content.js";
@@ -39,7 +40,6 @@ import {
39
40
  isPrivateAssistantText,
40
41
  projectPersistedAssistantContent,
41
42
  } from "../persistence/user-facing-content.js";
42
- import type { ContentBlock } from "../providers/types.js";
43
43
  import { emitNotificationSignal } from "./emit-signal.js";
44
44
  import { hasNotifiedSourceContextSince } from "./events-store.js";
45
45
  import {
@@ -79,28 +79,72 @@ export interface ScheduleResultNotificationParams {
79
79
  }
80
80
 
81
81
  /**
82
- * Whether a tool call in the run's turn delivered the result somewhere the
83
- * user will see it, outside the notification pipeline.
82
+ * Whether the run delivered its result through the messaging tool, and the
83
+ * call succeeded.
84
84
  *
85
- * The schedule skill prescribes two such routes for rich content the
86
- * messaging tool for email, and the Slack Web API's `chat.postMessage` through
87
- * bash and neither writes a `notification_events` row, so the pipeline probe
88
- * cannot see them. Without this check a well-authored Slack digest would post
85
+ * The messaging tool is the route the schedule skill prescribes for rich
86
+ * content, and it writes no `notification_events` row, so the pipeline probe
87
+ * cannot see it. Without this check a well-authored Slack digest would post
89
88
  * its summary and then get a second notification whose body is "Posted the
90
- * digest to #general." This is a recognized-routes list, not a general "did
91
- * the run do anything?" heuristic: a route that is not here gets the fallback,
92
- * which is the safe failure.
89
+ * digest to #general."
90
+ *
91
+ * Both conditions are load-bearing. The route has to be recognized, because
92
+ * this is a known-routes list rather than a general "did the run do anything?"
93
+ * heuristic. And the call has to have succeeded: a `tool_use` block records
94
+ * only that the model called the tool, so a send the channel refused leaves
95
+ * the same block as one that landed, and suppressing on it costs the user both
96
+ * the digest and the run's explanation of the failure. The executor's verdict
97
+ * is on the `tool_result` the call produced, so success is read from there,
98
+ * and a call with no result counts as undelivered. Either condition unmet gets
99
+ * the fallback, which is the safe failure.
100
+ *
101
+ * The result read starts at the run's first row, so it covers the results
102
+ * interleaved through the turn. Rows from a later turn in a reused
103
+ * conversation can come back too and are harmless, because only this run's
104
+ * own call ids are matched.
105
+ *
106
+ * A Slack Web API post through bash is deliberately not a route, because its
107
+ * success proves nothing about the post. Slack refuses a call with HTTP 200
108
+ * and `ok: false` in the body, and the authenticated-request command fails
109
+ * only on a non-2xx status, so a post Slack refused still leaves a successful
110
+ * tool result. A run that posts that way gets the fallback too: at worst a
111
+ * duplicate, never a silence.
93
112
  */
94
- function isDirectDelivery(block: ContentBlock): boolean {
95
- if (block.type !== "tool_use") {
96
- return false;
113
+ function deliveredThroughMessagingTool(
114
+ conversationId: string,
115
+ runRows: readonly MessageRow[],
116
+ ): boolean {
117
+ const callIds = new Set<string>();
118
+ for (const row of runRows) {
119
+ for (const block of row.content) {
120
+ if (block.type === "tool_use" && block.name === "messaging_send") {
121
+ callIds.add(block.id);
122
+ }
123
+ }
97
124
  }
98
- if (block.name === "messaging_send") {
99
- return true;
125
+ if (callIds.size === 0) {
126
+ return false;
100
127
  }
101
- if (block.name === "bash") {
102
- const command = (block.input as { command?: unknown } | undefined)?.command;
103
- return typeof command === "string" && command.includes("chat.postMessage");
128
+ const [firstRunRow] = runRows;
129
+ for (const row of getMessagesAfter(conversationId, {
130
+ id: firstRunRow.id,
131
+ createdAt: firstRunRow.createdAt,
132
+ })) {
133
+ if (row.role !== "user") {
134
+ continue;
135
+ }
136
+ for (const block of row.content) {
137
+ // guard:allow-tool-result-only: the local executor's verdict on a
138
+ // delivery it ran. A server-side `web_search_tool_result` carries no
139
+ // `is_error` and never delivers anything.
140
+ if (
141
+ block.type === "tool_result" &&
142
+ block.is_error !== true &&
143
+ callIds.has(block.tool_use_id)
144
+ ) {
145
+ return true;
146
+ }
147
+ }
104
148
  }
105
149
  return false;
106
150
  }
@@ -237,11 +281,11 @@ export async function emitScheduleResultNotification(
237
281
  return;
238
282
  }
239
283
 
240
- // The run delivered around the pipeline — an email through the messaging
241
- // tool, a Slack post through the Web API. The user has the result; a
242
- // notification reading "posted it" on top would be the duplicate.
284
+ // The run delivered around the pipeline through the messaging tool, and
285
+ // the call succeeded. The user has the result; a notification reading
286
+ // "posted it" on top would be the duplicate.
243
287
  const runRows = collectRunRows(latestRow, conversationId, runStartedAt);
244
- if (runRows.some((row) => row.content.some(isDirectDelivery))) {
288
+ if (deliveredThroughMessagingTool(conversationId, runRows)) {
245
289
  return;
246
290
  }
247
291
 
@@ -8,6 +8,8 @@ When introducing a new built-in OAuth integration (one that appears in `seed-pro
8
8
 
9
9
  Add an entry to `PROVIDER_SEED_DATA`. Required fields: `provider`, `authorizeUrl`, `tokenExchangeUrl`, `defaultScopes`, `displayLabel`, `description`, `dashboardUrl`, `clientIdPlaceholder`, `logoUrl`, and `injectionTemplates`. Optional: `availableScopes` — either a structured array of `{scope, description?}` objects or a URL string pointing to the provider's scope documentation. See existing entries for the full shape. The `provider` key must be snake_case and is used as the canonical identifier everywhere else.
10
10
 
11
+ `injectionTemplates` is also a security boundary, not only an injection rule. `assistant oauth request` and `assistant channels request` accept an absolute URL only when its host matches one of the entry's `hostPattern` values (`assertOAuthRequestUrlAllowed` in `../runtime/routes/oauth-commands-routes.ts`), so that list is the full set of hosts the credential may be sent to. A pattern matches exactly unless it starts with `*.`; reserve the wildcard for per-tenant hosts that cannot be enumerated (`*.salesforce.com`), and name a fixed host the provider documents (`files.slack.com`) by name. The guard sees only the URL the caller passes: the connection follows a redirect itself, and a cross-origin hop carries no `Authorization`, so a redirect target never receives the credential and never needs listing.
12
+
11
13
  If the provider will support managed mode, set `managedServiceConfigKey` to a slug matching the key you will add to `ServicesSchema` (e.g. `"acme-oauth"`).
12
14
 
13
15
  ### 2. _(managed only)_ Add a service schema — `../config/schemas/services.ts`
@@ -60,6 +60,26 @@ describe("PROVIDER_SEED_DATA managed mode wiring", () => {
60
60
  );
61
61
  }
62
62
 
63
+ // `folders:read` is documented too, but the app's scope picker only
64
+ // offers `folder_metadata:read` under Folders, so Figma answers
65
+ // "Invalid scopes for app" whenever it is requested.
66
+ expect(figma.defaultScopes).not.toContain("folders:read");
67
+ if (Array.isArray(availableScopes)) {
68
+ expect(availableScopes.map(({ scope }) => scope)).toContain(
69
+ "folders:read",
70
+ );
71
+ }
72
+
73
+ // `file_code_connect:write` is only offered to apps owned by an
74
+ // Organization-plan team. Managed apps in other environments are not,
75
+ // so requesting it fails the whole authorization there.
76
+ expect(figma.defaultScopes).not.toContain("file_code_connect:write");
77
+ if (Array.isArray(availableScopes)) {
78
+ expect(availableScopes.map(({ scope }) => scope)).toContain(
79
+ "file_code_connect:write",
80
+ );
81
+ }
82
+
63
83
  // GET /v1/me backs both the ping and the identity label.
64
84
  expect(figma.defaultScopes).toContain("current_user:read");
65
85
  });
@@ -127,6 +147,135 @@ describe("PROVIDER_SEED_DATA managed mode wiring", () => {
127
147
  expect(figma.featureFlag).toBe("figma-oauth");
128
148
  });
129
149
 
150
+ test("shopify keeps its per-tenant URL templates", () => {
151
+ // Shopify has no global OAuth host: every endpoint lives on the
152
+ // merchant's own domain. Hardcoding any host here would send every
153
+ // merchant's authorization to the wrong store, so the {tenant_host}
154
+ // placeholder the platform substitutes must survive.
155
+ const shopify = PROVIDER_SEED_DATA.shopify;
156
+ expect(shopify).toBeDefined();
157
+ for (const url of [
158
+ shopify.authorizeUrl,
159
+ shopify.tokenExchangeUrl,
160
+ shopify.refreshUrl,
161
+ shopify.baseUrl,
162
+ shopify.identityUrl,
163
+ ]) {
164
+ expect(url).toContain("{tenant_host}");
165
+ }
166
+ });
167
+
168
+ test("shopify requests comma-separated scopes", () => {
169
+ // Shopify parses `scope` as a comma-separated list; the OAuth2 default
170
+ // space separator is read as one malformed scope and the whole
171
+ // authorization request is rejected.
172
+ expect(PROVIDER_SEED_DATA.shopify.scopeSeparator).toBe(",");
173
+ });
174
+
175
+ test("shopify does not request approval-gated scopes", () => {
176
+ // Shopify rejects the entire authorization request when the app cannot
177
+ // grant a requested scope -- the same failure that broke monday and
178
+ // Figma. These scopes all require Shopify's prior approval.
179
+ const gated = [
180
+ "read_all_orders",
181
+ "read_customer_payment_methods",
182
+ "read_own_subscription_contracts",
183
+ "write_own_subscription_contracts",
184
+ "read_shopify_payments_dispute_evidences",
185
+ "write_shopify_payments_dispute_evidences",
186
+ ];
187
+ for (const scope of gated) {
188
+ expect(PROVIDER_SEED_DATA.shopify.defaultScopes).not.toContain(scope);
189
+ }
190
+ });
191
+
192
+ test("shopify injects the Admin API's own auth header", () => {
193
+ // The Admin API reads X-Shopify-Access-Token and ignores an
194
+ // Authorization: Bearer header entirely, so a Bearer template here would
195
+ // make every proxied request unauthenticated.
196
+ const templates = PROVIDER_SEED_DATA.shopify.injectionTemplates;
197
+ expect(templates).toBeDefined();
198
+ const [template] = templates ?? [];
199
+ expect(template?.headerName).toBe("X-Shopify-Access-Token");
200
+ expect(template?.valuePrefix).toBe("");
201
+ expect(template?.hostPattern).toBe("*.myshopify.com");
202
+ });
203
+
204
+ test("quickbooks is wired up for managed mode behind its flag", () => {
205
+ const quickbooks = PROVIDER_SEED_DATA.quickbooks;
206
+ expect(quickbooks).toBeDefined();
207
+ expect(quickbooks.managedServiceConfigKey).toBe("quickbooks-oauth");
208
+ expect("quickbooks-oauth" in ServicesSchema.shape).toBe(true);
209
+ // Hidden until the platform side and client ids are live, like Figma
210
+ // and Shopify.
211
+ expect(quickbooks.featureFlag).toBe("quickbooks-oauth");
212
+ });
213
+
214
+ test("quickbooks uses Intuit's OAuth endpoints with HTTP Basic", () => {
215
+ const quickbooks = PROVIDER_SEED_DATA.quickbooks;
216
+ expect(quickbooks.authorizeUrl).toBe(
217
+ "https://appcenter.intuit.com/connect/oauth2",
218
+ );
219
+ expect(quickbooks.tokenExchangeUrl).toBe(
220
+ "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
221
+ );
222
+ expect(quickbooks.refreshUrl).toBe(quickbooks.tokenExchangeUrl);
223
+ expect(quickbooks.tokenEndpointAuthMethod).toBe("client_secret_basic");
224
+ });
225
+
226
+ test("quickbooks keeps its per-company URL templates", () => {
227
+ // Every Accounting API path is scoped to the realm the user picked on
228
+ // Intuit's consent screen. The platform fills {realm_id} from the
229
+ // connection; a hardcoded realm here would send every company's calls
230
+ // to the wrong books.
231
+ const quickbooks = PROVIDER_SEED_DATA.quickbooks;
232
+ for (const url of [quickbooks.baseUrl, quickbooks.identityUrl]) {
233
+ expect(url).toContain("/v3/company/{realm_id}");
234
+ }
235
+ });
236
+
237
+ test("quickbooks seeds no ping or revoke URL", () => {
238
+ // The ping route sends the URL's origin as a base override and cannot
239
+ // fill {realm_id}; the daemon's revoke helper posts an unauthenticated
240
+ // form body that Intuit's JSON + Basic-auth endpoint rejects. Either
241
+ // would fail silently on every your-own connection.
242
+ const quickbooks = PROVIDER_SEED_DATA.quickbooks;
243
+ expect(quickbooks.pingUrl).toBeUndefined();
244
+ expect(quickbooks.revokeUrl).toBeUndefined();
245
+ });
246
+
247
+ test("quickbooks requests only the accounting scope by default", () => {
248
+ // Payments needs a Payments-enabled app and the OpenID scopes only add
249
+ // the user's profile; requesting either widens the consent screen for
250
+ // nothing the Accounting API needs.
251
+ const quickbooks = PROVIDER_SEED_DATA.quickbooks;
252
+ expect(quickbooks.defaultScopes).toEqual([
253
+ "com.intuit.quickbooks.accounting",
254
+ ]);
255
+ const available = quickbooks.availableScopes;
256
+ expect(Array.isArray(available)).toBe(true);
257
+ if (Array.isArray(available)) {
258
+ expect(available.map(({ scope }) => scope)).toContain(
259
+ "com.intuit.quickbooks.payment",
260
+ );
261
+ }
262
+ });
263
+
264
+ test("quickbooks allows the credential on both Intuit API hosts", () => {
265
+ // Development keys only work against sandbox companies, which live on
266
+ // the sandbox host. Both hosts belong to Intuit and take the same
267
+ // bearer token.
268
+ const templates = PROVIDER_SEED_DATA.quickbooks.injectionTemplates ?? [];
269
+ expect(templates.map((t) => t.hostPattern).sort()).toEqual([
270
+ "quickbooks.api.intuit.com",
271
+ "sandbox-quickbooks.api.intuit.com",
272
+ ]);
273
+ for (const template of templates) {
274
+ expect(template.headerName).toBe("Authorization");
275
+ expect(template.valuePrefix).toBe("Bearer ");
276
+ }
277
+ });
278
+
130
279
  test("every managedServiceConfigKey resolves to a ServicesSchema key", () => {
131
280
  // Cross-repo invariant: a provider with managedServiceConfigKey but no
132
281
  // matching ServicesSchema entry silently falls back to BYO mode in
@@ -308,6 +308,66 @@ describe("BYOOAuthConnection", () => {
308
308
  expect((init as RequestInit).method).toBe("GET");
309
309
  });
310
310
 
311
+ test("follows a cross-origin redirect without the credential and returns the target's bytes", async () => {
312
+ // Real fetch against two loopback origins: the credential goes to the
313
+ // host the caller named, the redirect target sees no Authorization,
314
+ // and the target's bytes come back as the response body. This is the
315
+ // path a Slack file download takes when files.slack.com answers with a
316
+ // 302 to its CDN.
317
+ globalThis.fetch = originalFetch;
318
+ await setupCredential("google");
319
+ const bytes = Buffer.from([
320
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
321
+ ]);
322
+ const seen: { origin?: string | null; target?: string | null } = {};
323
+
324
+ const target = Bun.serve({
325
+ port: 0,
326
+ hostname: "127.0.0.1",
327
+ fetch: (req) => {
328
+ seen.target = req.headers.get("authorization");
329
+ return new Response(bytes, {
330
+ status: 200,
331
+ headers: { "content-type": "image/png" },
332
+ });
333
+ },
334
+ });
335
+ const origin = Bun.serve({
336
+ port: 0,
337
+ hostname: "127.0.0.1",
338
+ fetch: (req) => {
339
+ seen.origin = req.headers.get("authorization");
340
+ return new Response(null, {
341
+ status: 302,
342
+ headers: { Location: `http://127.0.0.1:${target.port}/signed` },
343
+ });
344
+ },
345
+ });
346
+
347
+ try {
348
+ const conn = new BYOOAuthConnection({
349
+ id: "conn-google",
350
+ provider: "google",
351
+ baseUrl: `http://127.0.0.1:${origin.port}`,
352
+ accountInfo: null,
353
+ });
354
+
355
+ const result = await conn.request({
356
+ method: "GET",
357
+ path: "/files-pri/T0123-F0456/download/shot.png",
358
+ });
359
+
360
+ expect(seen.origin).toBe("Bearer test-access-token");
361
+ expect(seen.target).toBeNull();
362
+ expect(result.status).toBe(200);
363
+ expect(Buffer.isBuffer(result.body)).toBe(true);
364
+ expect(Buffer.from(result.body as Buffer).equals(bytes)).toBe(true);
365
+ } finally {
366
+ origin.stop(true);
367
+ target.stop(true);
368
+ }
369
+ });
370
+
311
371
  test("appends query parameters", async () => {
312
372
  await setupCredential("google");
313
373
  const conn = createConnection();
@@ -708,6 +768,51 @@ describe("BYOOAuthConnection", () => {
708
768
  expect(headers.get("Authorization")).toBe("Bearer test-access-token");
709
769
  });
710
770
 
771
+ test("sends the token in the provider's own header when one is configured", async () => {
772
+ // Shopify's Admin API reads X-Shopify-Access-Token and ignores
773
+ // Authorization, so a Bearer header reaches the shop unauthenticated.
774
+ await setupCredential("google");
775
+ const conn = new BYOOAuthConnection({
776
+ id: "conn-google",
777
+ provider: "google",
778
+ baseUrl: "https://example-store.myshopify.com",
779
+ accountInfo: null,
780
+ tokenHeader: { name: "X-Shopify-Access-Token", valuePrefix: "" },
781
+ });
782
+
783
+ await conn.request({
784
+ method: "GET",
785
+ path: "/admin/api/2026-07/shop.json",
786
+ // A caller-supplied Authorization must not ride along.
787
+ headers: { Authorization: "Bearer caller-supplied" },
788
+ });
789
+
790
+ const [url, init] = mockFetch.mock.calls[0];
791
+ expect(url).toBe(
792
+ "https://example-store.myshopify.com/admin/api/2026-07/shop.json",
793
+ );
794
+ const headers = (init as RequestInit).headers as Headers;
795
+ expect(headers.get("X-Shopify-Access-Token")).toBe("test-access-token");
796
+ expect(headers.has("Authorization")).toBe(false);
797
+ });
798
+
799
+ test("keeps the value prefix from the token header template", async () => {
800
+ await setupCredential("google");
801
+ const conn = new BYOOAuthConnection({
802
+ id: "conn-google",
803
+ provider: "google",
804
+ baseUrl: "https://discord.com/api",
805
+ accountInfo: null,
806
+ tokenHeader: { name: "Authorization", valuePrefix: "Bot " },
807
+ });
808
+
809
+ await conn.request({ method: "GET", path: "/users/@me" });
810
+
811
+ const [, init] = mockFetch.mock.calls[0];
812
+ const headers = (init as RequestInit).headers as Headers;
813
+ expect(headers.get("Authorization")).toBe("Bot test-access-token");
814
+ });
815
+
711
816
  test("uses Telegram Bot API token URL format without Bearer auth", async () => {
712
817
  await setupTelegramCredential();
713
818
  const conn = createConnection("telegram");
@@ -21,11 +21,29 @@ const log = getLogger("byo-oauth-connection");
21
21
  /** Default per-request timeout to prevent hung requests from blocking indefinitely. */
22
22
  const REQUEST_TIMEOUT_MS = 30_000;
23
23
 
24
+ /**
25
+ * How the access token is attached to an outbound request. Almost every
26
+ * provider reads `Authorization: Bearer <token>`; a few use their own header
27
+ * (Shopify's Admin API reads `X-Shopify-Access-Token` and ignores
28
+ * Authorization entirely). Derived from the provider's header-type injection
29
+ * template so the seed is the single source of truth.
30
+ */
31
+ export interface BYOTokenHeader {
32
+ name: string;
33
+ /** Text placed before the token, e.g. `"Bearer "`, `"Bot "`, or `""`. */
34
+ valuePrefix: string;
35
+ }
36
+
24
37
  export interface BYOOAuthConnectionOptions {
25
38
  id: string;
26
39
  provider: string;
27
40
  baseUrl: string;
28
41
  accountInfo: string | null;
42
+ /**
43
+ * Token header override. When omitted the connection falls back to
44
+ * `Authorization: Bearer` (or `Bot` for discord_channel).
45
+ */
46
+ tokenHeader?: BYOTokenHeader | null;
29
47
  }
30
48
 
31
49
  export class BYOOAuthConnection implements OAuthConnection {
@@ -34,12 +52,14 @@ export class BYOOAuthConnection implements OAuthConnection {
34
52
  readonly accountInfo: string | null;
35
53
 
36
54
  private readonly baseUrl: string;
55
+ private readonly tokenHeader: BYOTokenHeader | null;
37
56
 
38
57
  constructor(opts: BYOOAuthConnectionOptions) {
39
58
  this.id = opts.id;
40
59
  this.provider = opts.provider;
41
60
  this.baseUrl = opts.baseUrl;
42
61
  this.accountInfo = opts.accountInfo;
62
+ this.tokenHeader = opts.tokenHeader ?? null;
43
63
  }
44
64
 
45
65
  async request(req: OAuthConnectionRequest): Promise<OAuthConnectionResponse> {
@@ -51,8 +71,12 @@ export class BYOOAuthConnection implements OAuthConnection {
51
71
  // Discord bot tokens authenticate with the `Bot ` scheme, not
52
72
  // `Bearer`. Sending Bearer here reaches Discord as an unusable
53
73
  // credential and comes back 401, which reads as a revoked token.
54
- const authScheme =
55
- this.provider === "discord_channel" ? "Bot" : "Bearer";
74
+ // Providers with their own token header (Shopify) override the
75
+ // whole thing via `tokenHeader`.
76
+ const tokenHeader: BYOTokenHeader = this.tokenHeader ?? {
77
+ name: "Authorization",
78
+ valuePrefix: this.provider === "discord_channel" ? "Bot " : "Bearer ",
79
+ };
56
80
  const requestPath = isTelegram
57
81
  ? buildTelegramBotApiPath(req.path, token)
58
82
  : req.path;
@@ -93,16 +117,22 @@ export class BYOOAuthConnection implements OAuthConnection {
93
117
  }
94
118
  }
95
119
  if (!isTelegram) {
96
- headers.set("Authorization", `${authScheme} ${token}`);
120
+ // The credential always wins over a caller-supplied value, and a
121
+ // provider that reads its own header must not also receive a
122
+ // stray Authorization the caller happened to send.
123
+ if (tokenHeader.name.toLowerCase() !== "authorization") {
124
+ headers.delete("Authorization");
125
+ }
126
+ headers.set(tokenHeader.name, `${tokenHeader.valuePrefix}${token}`);
97
127
  }
98
128
 
99
129
  const resp = await fetch(fullUrl, {
100
130
  method: req.method,
101
131
  headers,
102
132
  body: hasBody
103
- ? (binaryBody !== undefined
104
- ? Buffer.from(binaryBody)
105
- : (rawBody ?? JSON.stringify(req.body)))
133
+ ? binaryBody !== undefined
134
+ ? Buffer.from(binaryBody)
135
+ : (rawBody ?? JSON.stringify(req.body))
106
136
  : undefined,
107
137
  // Following a redirect would replay a POST as a GET against a URL
108
138
  // the caller never asked for, and hide the 3xx from them.
@@ -72,9 +72,11 @@ import { setConfig } from "../__tests__/helpers/set-config.js";
72
72
  import { BYOOAuthConnection } from "./byo-connection.js";
73
73
  import {
74
74
  formatNoConnectionError,
75
+ platformProxyBaseUrl,
75
76
  resolveEffectiveBaseUrl,
76
77
  resolveOAuthConnection,
77
78
  resolveOAuthConnectionWithMeta,
79
+ resolveTokenHeader,
78
80
  } from "./connection-resolver.js";
79
81
  import { PlatformOAuthConnection } from "./platform-connection.js";
80
82
 
@@ -753,3 +755,101 @@ describe("resolveEffectiveBaseUrl", () => {
753
755
  ).toBe("https://gmail.googleapis.com/gmail/v1/users/me");
754
756
  });
755
757
  });
758
+
759
+ describe("platformProxyBaseUrl", () => {
760
+ test("forwards a concrete seed base URL", () => {
761
+ expect(platformProxyBaseUrl("https://api.figma.com")).toBe(
762
+ "https://api.figma.com",
763
+ );
764
+ });
765
+
766
+ test("withholds a templated base URL so the platform fills it", () => {
767
+ // Shopify's host and QuickBooks' realm are pinned to the connection on
768
+ // the platform. Forwarding the unfilled template would fail the proxy's
769
+ // allowlist check and shadow the platform's own default.
770
+ expect(platformProxyBaseUrl("https://{tenant_host}")).toBeUndefined();
771
+ expect(
772
+ platformProxyBaseUrl(
773
+ "https://quickbooks.api.intuit.com/v3/company/{realm_id}",
774
+ ),
775
+ ).toBeUndefined();
776
+ });
777
+
778
+ test("treats a missing base URL as no override", () => {
779
+ expect(platformProxyBaseUrl(undefined)).toBeUndefined();
780
+ expect(platformProxyBaseUrl(null)).toBeUndefined();
781
+ expect(platformProxyBaseUrl("")).toBeUndefined();
782
+ });
783
+ });
784
+
785
+ describe("resolveTokenHeader", () => {
786
+ const shopify = JSON.stringify([
787
+ {
788
+ hostPattern: "*.myshopify.com",
789
+ injectionType: "header",
790
+ headerName: "X-Shopify-Access-Token",
791
+ valuePrefix: "",
792
+ },
793
+ ]);
794
+
795
+ test("returns null when the provider declares no templates", () => {
796
+ expect(resolveTokenHeader(undefined, "https://api.example.com")).toBeNull();
797
+ expect(resolveTokenHeader(null, "https://api.example.com")).toBeNull();
798
+ expect(resolveTokenHeader("[]", "https://api.example.com")).toBeNull();
799
+ });
800
+
801
+ test("returns null for malformed JSON", () => {
802
+ expect(
803
+ resolveTokenHeader("{not json", "https://api.example.com"),
804
+ ).toBeNull();
805
+ });
806
+
807
+ test("ignores non-header injection templates", () => {
808
+ const query = JSON.stringify([
809
+ {
810
+ hostPattern: "api.example.com",
811
+ injectionType: "query",
812
+ headerName: "",
813
+ },
814
+ ]);
815
+ expect(resolveTokenHeader(query, "https://api.example.com")).toBeNull();
816
+ });
817
+
818
+ test("picks the template whose wildcard host pattern matches the base URL", () => {
819
+ const templates = JSON.stringify([
820
+ {
821
+ hostPattern: "api.other.com",
822
+ injectionType: "header",
823
+ headerName: "Authorization",
824
+ valuePrefix: "Bearer ",
825
+ },
826
+ ...JSON.parse(shopify),
827
+ ]);
828
+ expect(
829
+ resolveTokenHeader(templates, "https://example-store.myshopify.com"),
830
+ ).toEqual({ name: "X-Shopify-Access-Token", valuePrefix: "" });
831
+ });
832
+
833
+ test("falls back to the first header template when the host cannot be matched", () => {
834
+ // A still-templated base URL is not a parseable URL.
835
+ expect(resolveTokenHeader(shopify, "https://{tenant_host}")).toEqual({
836
+ name: "X-Shopify-Access-Token",
837
+ valuePrefix: "",
838
+ });
839
+ });
840
+
841
+ test("preserves the Bot prefix for discord_channel", () => {
842
+ const discord = JSON.stringify([
843
+ {
844
+ hostPattern: "discord.com",
845
+ injectionType: "header",
846
+ headerName: "Authorization",
847
+ valuePrefix: "Bot ",
848
+ },
849
+ ]);
850
+ expect(resolveTokenHeader(discord, "https://discord.com/api")).toEqual({
851
+ name: "Authorization",
852
+ valuePrefix: "Bot ",
853
+ });
854
+ });
855
+ });