@vellumai/assistant 0.7.2 → 0.8.0

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 (424) hide show
  1. package/ARCHITECTURE.md +45 -29
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/docs/architecture/memory.md +5 -2
  6. package/knip.json +1 -0
  7. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  8. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  9. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  11. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  12. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  13. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  16. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  17. package/openapi.yaml +470 -25
  18. package/package.json +3 -1
  19. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  20. package/src/__tests__/app-control-flow.test.ts +21 -11
  21. package/src/__tests__/approval-cascade.test.ts +8 -16
  22. package/src/__tests__/approval-routes-http.test.ts +6 -0
  23. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  24. package/src/__tests__/assistant-event.test.ts +0 -10
  25. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  26. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  27. package/src/__tests__/auto-analysis-end-to-end.test.ts +48 -0
  28. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  29. package/src/__tests__/call-constants.test.ts +10 -1
  30. package/src/__tests__/call-controller.test.ts +127 -0
  31. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  32. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  33. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  34. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  35. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  36. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  37. package/src/__tests__/config-schema.test.ts +1 -0
  38. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  39. package/src/__tests__/config-watcher.test.ts +140 -69
  40. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  41. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  42. package/src/__tests__/context-search-fanout.test.ts +0 -1
  43. package/src/__tests__/context-search-memory-source.test.ts +6 -33
  44. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  45. package/src/__tests__/context-search-pkb-source.test.ts +12 -7
  46. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  47. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
  48. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  51. package/src/__tests__/conversation-agent-loop.test.ts +457 -8
  52. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  53. package/src/__tests__/conversation-error.test.ts +150 -3
  54. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  55. package/src/__tests__/conversation-process-callsite.test.ts +38 -0
  56. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +74 -0
  58. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  59. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  60. package/src/__tests__/conversation-store.test.ts +0 -18
  61. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  62. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  63. package/src/__tests__/conversation-surfaces-data-persist.test.ts +476 -0
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +61 -5
  65. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  66. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  67. package/src/__tests__/credentials-cli.test.ts +7 -0
  68. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  69. package/src/__tests__/date-context.test.ts +164 -2
  70. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  71. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  72. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  73. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  74. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  75. package/src/__tests__/disk-usage.test.ts +150 -0
  76. package/src/__tests__/events-client-registration.test.ts +52 -0
  77. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  78. package/src/__tests__/file-write-tool.test.ts +4 -10
  79. package/src/__tests__/filing-service.test.ts +2 -20
  80. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  82. package/src/__tests__/heartbeat-service.test.ts +260 -11
  83. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  84. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  85. package/src/__tests__/host-bash-routes.test.ts +178 -13
  86. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  87. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  88. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  89. package/src/__tests__/host-file-proxy.test.ts +268 -6
  90. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  91. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  92. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  93. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  94. package/src/__tests__/injector-chain.test.ts +36 -16
  95. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  96. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  98. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  99. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  100. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  101. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  102. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  103. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  104. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  105. package/src/__tests__/oauth-cli.test.ts +121 -0
  106. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  107. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  108. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  109. package/src/__tests__/openai-provider.test.ts +45 -8
  110. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  111. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  112. package/src/__tests__/platform.test.ts +2 -1
  113. package/src/__tests__/playbook-execution.test.ts +0 -43
  114. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  115. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  116. package/src/__tests__/provider-tool-name.test.ts +23 -0
  117. package/src/__tests__/relay-server.test.ts +60 -5
  118. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  119. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  120. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  121. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  122. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  123. package/src/__tests__/secret-response-routing.test.ts +7 -5
  124. package/src/__tests__/server-history-render.test.ts +82 -0
  125. package/src/__tests__/skill-include-graph.test.ts +31 -0
  126. package/src/__tests__/skill-load-tool.test.ts +44 -16
  127. package/src/__tests__/skills.test.ts +39 -0
  128. package/src/__tests__/suggestion-routes.test.ts +46 -0
  129. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  130. package/src/__tests__/tool-executor.test.ts +155 -0
  131. package/src/__tests__/twilio-validation.test.ts +2 -2
  132. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  133. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  134. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  136. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  137. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  138. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +78 -0
  139. package/src/agent/loop.ts +11 -0
  140. package/src/approvals/guardian-request-resolvers.ts +3 -32
  141. package/src/backup/snapshot-lock.ts +2 -27
  142. package/src/bundler/compiler-tools.ts +3 -2
  143. package/src/calls/call-constants.ts +5 -8
  144. package/src/calls/call-controller.ts +130 -67
  145. package/src/calls/call-conversation-messages.ts +46 -10
  146. package/src/calls/relay-server.ts +7 -1
  147. package/src/calls/voice-session-bridge.ts +1 -1
  148. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  149. package/src/cli/commands/bash.ts +35 -108
  150. package/src/cli/commands/contacts.ts +64 -25
  151. package/src/cli/commands/credentials.ts +56 -0
  152. package/src/cli/commands/memory-v2.ts +11 -10
  153. package/src/cli/commands/oauth/__tests__/connect.test.ts +401 -219
  154. package/src/cli/commands/oauth/connect.ts +124 -40
  155. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  156. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  157. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  158. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  159. package/src/cli/commands/platform/index.ts +16 -7
  160. package/src/cli/commands/status.ts +57 -0
  161. package/src/cli/program.ts +4 -2
  162. package/src/config/assistant-feature-flags.ts +13 -3
  163. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  164. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  165. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  166. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  167. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  168. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  169. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  170. package/src/config/env.ts +0 -8
  171. package/src/config/feature-flag-registry.json +13 -5
  172. package/src/config/loader.ts +199 -27
  173. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  174. package/src/config/schemas/call-site-catalog.ts +14 -0
  175. package/src/config/schemas/channels.ts +0 -5
  176. package/src/config/schemas/heartbeat.ts +1 -1
  177. package/src/config/schemas/llm.ts +2 -0
  178. package/src/config/schemas/memory-lifecycle.ts +13 -0
  179. package/src/config/schemas/memory-v2.ts +76 -12
  180. package/src/config/schemas/platform.ts +43 -3
  181. package/src/config/schemas/services.ts +28 -0
  182. package/src/config/seed-inference-profiles.ts +230 -33
  183. package/src/contacts/contact-store.ts +0 -25
  184. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  185. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  186. package/src/daemon/assistant-attachments.ts +4 -4
  187. package/src/daemon/config-watcher.ts +85 -57
  188. package/src/daemon/conversation-agent-loop-handlers.ts +38 -0
  189. package/src/daemon/conversation-agent-loop.ts +183 -43
  190. package/src/daemon/conversation-error.ts +87 -15
  191. package/src/daemon/conversation-lifecycle.ts +22 -10
  192. package/src/daemon/conversation-process.ts +8 -0
  193. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  194. package/src/daemon/conversation-store.ts +2 -2
  195. package/src/daemon/conversation-surfaces.ts +211 -29
  196. package/src/daemon/conversation-tool-setup.ts +66 -19
  197. package/src/daemon/conversation.ts +18 -23
  198. package/src/daemon/date-context.ts +71 -22
  199. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  200. package/src/daemon/disk-pressure-guard.ts +343 -0
  201. package/src/daemon/disk-pressure-policy.ts +163 -0
  202. package/src/daemon/handlers/shared.ts +26 -1
  203. package/src/daemon/handlers/skills.ts +3 -4
  204. package/src/daemon/host-app-control-proxy.ts +137 -41
  205. package/src/daemon/host-bash-proxy.ts +47 -22
  206. package/src/daemon/host-browser-proxy.ts +1 -1
  207. package/src/daemon/host-cu-proxy.ts +50 -4
  208. package/src/daemon/host-file-proxy.ts +44 -8
  209. package/src/daemon/host-transfer-proxy.ts +97 -6
  210. package/src/daemon/lifecycle.ts +167 -101
  211. package/src/daemon/meet-host-supervisor.ts +4 -4
  212. package/src/daemon/meet-manifest-loader.ts +0 -1
  213. package/src/daemon/memory-v2-startup.ts +66 -15
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/conversations.ts +4 -0
  216. package/src/daemon/message-types/disk-pressure.ts +9 -0
  217. package/src/daemon/message-types/messages.ts +22 -1
  218. package/src/daemon/profiler-run-store.ts +5 -5
  219. package/src/daemon/tool-setup-types.ts +2 -2
  220. package/src/documents/document-store.ts +119 -0
  221. package/src/filing/filing-service.ts +29 -5
  222. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  223. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  224. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  225. package/src/heartbeat/heartbeat-service.ts +205 -31
  226. package/src/home/feed-scheduler.ts +18 -0
  227. package/src/inbound/platform-callback-registration.ts +8 -15
  228. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  229. package/src/ipc/assistant-server.ts +149 -38
  230. package/src/ipc/gateway-client.ts +37 -3
  231. package/src/ipc/skill-server.ts +99 -42
  232. package/src/live-voice/live-voice-archive.ts +4 -4
  233. package/src/live-voice/protocol.ts +5 -7
  234. package/src/media/image-service.ts +1 -7
  235. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  236. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +34 -51
  237. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  238. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  239. package/src/memory/admin.ts +5 -9
  240. package/src/memory/context-search/agent-runner.ts +19 -2
  241. package/src/memory/context-search/sources/conversations.ts +2 -11
  242. package/src/memory/context-search/sources/memory-v2.ts +1 -16
  243. package/src/memory/context-search/sources/memory.ts +2 -3
  244. package/src/memory/context-search/sources/pkb.ts +2 -3
  245. package/src/memory/context-search/types.ts +0 -1
  246. package/src/memory/conversation-crud.ts +4 -12
  247. package/src/memory/db-init.ts +2 -0
  248. package/src/memory/embedding-runtime-manager.ts +119 -5
  249. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +136 -82
  250. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  251. package/src/memory/graph/conversation-graph-memory.ts +72 -61
  252. package/src/memory/graph/extraction.ts +1 -3
  253. package/src/memory/graph/graph-search.test.ts +11 -67
  254. package/src/memory/graph/graph-search.ts +4 -24
  255. package/src/memory/graph/retriever.test.ts +12 -1
  256. package/src/memory/graph/retriever.ts +10 -15
  257. package/src/memory/graph/tool-handlers.ts +3 -4
  258. package/src/memory/graph/tools.ts +4 -4
  259. package/src/memory/indexer.ts +53 -45
  260. package/src/memory/job-handlers/backfill.ts +2 -11
  261. package/src/memory/job-handlers/cleanup.ts +43 -0
  262. package/src/memory/job-handlers/embedding.ts +6 -8
  263. package/src/memory/job-handlers/summarization.ts +2 -7
  264. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  265. package/src/memory/jobs/embed-concept-page.ts +223 -87
  266. package/src/memory/jobs-store.ts +48 -0
  267. package/src/memory/jobs-worker.ts +85 -43
  268. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  269. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  270. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  271. package/src/memory/migrations/index.ts +1 -0
  272. package/src/memory/pkb/pkb-search.test.ts +7 -0
  273. package/src/memory/pkb/pkb-search.ts +4 -5
  274. package/src/memory/qdrant-client.ts +3 -13
  275. package/src/memory/rerank-local.ts +374 -0
  276. package/src/memory/search/semantic.ts +10 -72
  277. package/src/memory/trace-event-store.ts +1 -17
  278. package/src/memory/v2/__tests__/activation.test.ts +346 -255
  279. package/src/memory/v2/__tests__/consolidation-job.test.ts +61 -40
  280. package/src/memory/v2/__tests__/injection.test.ts +297 -190
  281. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  282. package/src/memory/v2/__tests__/qdrant.test.ts +326 -9
  283. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  284. package/src/memory/v2/__tests__/sim.test.ts +113 -196
  285. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  286. package/src/memory/v2/__tests__/static-context.test.ts +77 -14
  287. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  288. package/src/memory/v2/activation.ts +149 -156
  289. package/src/memory/v2/consolidation-job.ts +69 -20
  290. package/src/memory/v2/injection.ts +75 -68
  291. package/src/memory/v2/page-store.ts +39 -0
  292. package/src/memory/v2/prompts/consolidation.ts +41 -1
  293. package/src/memory/v2/qdrant.ts +306 -46
  294. package/src/memory/v2/reranker.ts +177 -0
  295. package/src/memory/v2/sim.ts +77 -110
  296. package/src/memory/v2/skill-content.ts +4 -3
  297. package/src/memory/v2/skill-store.ts +82 -59
  298. package/src/memory/v2/static-context.ts +26 -8
  299. package/src/memory/v2/sweep-job.ts +5 -6
  300. package/src/memory/v2/types.ts +17 -10
  301. package/src/notifications/copy-composer.ts +47 -0
  302. package/src/notifications/decision-engine.ts +46 -0
  303. package/src/notifications/signal.ts +4 -0
  304. package/src/oauth/AGENTS.md +3 -1
  305. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  306. package/src/oauth/connect-orchestrator.ts +2 -0
  307. package/src/oauth/connection-resolver.test.ts +66 -1
  308. package/src/oauth/connection-resolver.ts +55 -1
  309. package/src/oauth/oauth-connect-state.ts +77 -0
  310. package/src/oauth/seed-providers.ts +58 -1
  311. package/src/permissions/gateway-threshold-reader.ts +116 -8
  312. package/src/permissions/prompter.ts +86 -96
  313. package/src/permissions/secret-prompter.ts +31 -31
  314. package/src/plugins/defaults/injectors.ts +36 -4
  315. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  316. package/src/plugins/types.ts +7 -0
  317. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  318. package/src/proactive-artifact/decision.test.ts +226 -0
  319. package/src/proactive-artifact/decision.ts +165 -0
  320. package/src/proactive-artifact/index.ts +7 -0
  321. package/src/proactive-artifact/job.test.ts +914 -0
  322. package/src/proactive-artifact/job.ts +366 -0
  323. package/src/proactive-artifact/message-copy.ts +58 -0
  324. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  325. package/src/proactive-artifact/trigger-state.ts +119 -0
  326. package/src/prompts/normalize-onboarding.ts +80 -0
  327. package/src/prompts/persona-resolver.ts +101 -9
  328. package/src/prompts/system-prompt.ts +21 -7
  329. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  330. package/src/prompts/templates/SOUL.md +13 -28
  331. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  332. package/src/providers/model-intents.ts +7 -0
  333. package/src/providers/openrouter/client.ts +8 -0
  334. package/src/providers/retry.ts +50 -0
  335. package/src/providers/types.ts +1 -0
  336. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  337. package/src/runtime/agent-wake.ts +238 -100
  338. package/src/runtime/assistant-event-hub.ts +36 -6
  339. package/src/runtime/assistant-event.ts +0 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  341. package/src/runtime/auth/route-policy.ts +15 -1
  342. package/src/runtime/auth/same-actor.ts +216 -0
  343. package/src/runtime/channel-approvals.ts +3 -2
  344. package/src/runtime/channel-retry-sweep.ts +65 -1
  345. package/src/runtime/local-actor-identity.ts +52 -11
  346. package/src/runtime/pending-interactions.ts +27 -15
  347. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  348. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  349. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  350. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  351. package/src/runtime/routes/approval-routes.ts +7 -3
  352. package/src/runtime/routes/client-routes.ts +20 -2
  353. package/src/runtime/routes/consolidation-routes.ts +8 -9
  354. package/src/runtime/routes/contact-routes.ts +0 -25
  355. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  356. package/src/runtime/routes/conversation-routes.ts +35 -26
  357. package/src/runtime/routes/debug-bash-routes.ts +165 -0
  358. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  359. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  360. package/src/runtime/routes/documents-routes.ts +2 -75
  361. package/src/runtime/routes/events-routes.ts +41 -9
  362. package/src/runtime/routes/filing-routes.ts +2 -3
  363. package/src/runtime/routes/host-bash-routes.ts +23 -3
  364. package/src/runtime/routes/host-cu-routes.ts +33 -6
  365. package/src/runtime/routes/host-file-routes.ts +32 -6
  366. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  367. package/src/runtime/routes/identity-routes.ts +7 -138
  368. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  369. package/src/runtime/routes/index.ts +6 -0
  370. package/src/runtime/routes/memory-item-routes.test.ts +37 -17
  371. package/src/runtime/routes/memory-item-routes.ts +5 -6
  372. package/src/runtime/routes/memory-v2-routes.ts +136 -17
  373. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  374. package/src/runtime/verification-outbound-actions.ts +4 -4
  375. package/src/schedule/run-script.ts +37 -5
  376. package/src/schedule/scheduler.ts +20 -1
  377. package/src/security/encrypted-store.ts +2 -0
  378. package/src/security/secure-keys.ts +55 -0
  379. package/src/skills/include-graph.ts +35 -13
  380. package/src/skills/remote-skill-policy.ts +4 -10
  381. package/src/subagent/index.ts +1 -7
  382. package/src/subagent/manager.ts +1 -15
  383. package/src/tasks/task-runner.ts +0 -1
  384. package/src/tasks/task-store.ts +0 -3
  385. package/src/tools/background-tool-registry.ts +17 -3
  386. package/src/tools/document/document-tool.ts +20 -0
  387. package/src/tools/executor.ts +18 -2
  388. package/src/tools/host-filesystem/edit.test.ts +151 -0
  389. package/src/tools/host-filesystem/edit.ts +43 -1
  390. package/src/tools/host-filesystem/read.test.ts +129 -0
  391. package/src/tools/host-filesystem/read.ts +43 -1
  392. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  393. package/src/tools/host-filesystem/transfer.ts +56 -11
  394. package/src/tools/host-filesystem/write.test.ts +134 -0
  395. package/src/tools/host-filesystem/write.ts +43 -1
  396. package/src/tools/host-terminal/host-shell.ts +13 -6
  397. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  398. package/src/tools/memory/register.test.ts +14 -9
  399. package/src/tools/memory/register.ts +1 -2
  400. package/src/tools/permission-checker.ts +15 -0
  401. package/src/tools/provider-tool-name.ts +28 -0
  402. package/src/tools/registry.ts +30 -9
  403. package/src/tools/skills/load.ts +24 -20
  404. package/src/tools/terminal/shell.ts +9 -1
  405. package/src/tools/tool-approval-handler.ts +31 -6
  406. package/src/tools/tool-name-aliases.ts +19 -0
  407. package/src/tools/types.ts +43 -3
  408. package/src/tts/provider-catalog.ts +3 -5
  409. package/src/util/disk-usage.ts +138 -0
  410. package/src/util/platform.ts +21 -11
  411. package/src/util/process-liveness.ts +26 -0
  412. package/src/workspace/heartbeat-service.ts +19 -0
  413. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  414. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  415. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +14 -0
  416. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  417. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  418. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  419. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  420. package/src/workspace/migrations/registry.ts +14 -0
  421. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  422. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  423. package/src/memory/v2/skill-qdrant.ts +0 -404
  424. package/src/signals/bash.ts +0 -198
@@ -32,6 +32,51 @@ export function nonEmpty(value: string | undefined): string | undefined {
32
32
  return trimmed.length > 0 ? trimmed : undefined;
33
33
  }
34
34
 
35
+ export function looksLikeIntermediaryInstruction(text: string): boolean {
36
+ const normalized = text.replace(/\s+/g, " ").trim();
37
+ const intermediaryAction =
38
+ "(?:tell|telling|ask|asking|remind|reminding|nudge|nudging|prompt|prompting|notify|notifying|encourage|encouraging|prime|priming|brief|briefing|coach|coaching)";
39
+ const target = "(?:the\\s+)?(?:guardian|recipient|user)";
40
+ return (
41
+ /\b(?:assistant|agent|system|model|watcher)\s+(?:should|needs?\s+to|must|can|could)\b/i.test(
42
+ normalized,
43
+ ) ||
44
+ new RegExp(
45
+ `\\b(?:consider|try|please)\\s+${intermediaryAction}\\s+${target}\\b`,
46
+ "i",
47
+ ).test(normalized) ||
48
+ new RegExp(
49
+ `\\b${intermediaryAction}\\s+${target}\\s+(?:to|that|about|with)\\b`,
50
+ "i",
51
+ ).test(normalized) ||
52
+ new RegExp(
53
+ `\\b${target}\\s+(?:should|needs?\\s+to|must|might\\s+want\\s+to)\\b`,
54
+ "i",
55
+ ).test(normalized) ||
56
+ new RegExp(`\\b(?:for|to)\\s+${target}\\s+to\\b`, "i").test(normalized)
57
+ );
58
+ }
59
+
60
+ function buildHeartbeatAlertCopy(
61
+ payload: Record<string, unknown>,
62
+ ): RenderedChannelCopy {
63
+ const summary = str(
64
+ payload.summary,
65
+ str(payload.body, "Your assistant found something worth your attention."),
66
+ ).trim();
67
+ const safePopupBody = looksLikeIntermediaryInstruction(summary)
68
+ ? "I found something worth your attention in a heartbeat check. Open the conversation for details."
69
+ : summary;
70
+
71
+ return {
72
+ title: str(payload.title, "Heartbeat Alert"),
73
+ body: safePopupBody,
74
+ deliveryText: safePopupBody,
75
+ conversationTitle: str(payload.conversationTitle, "Heartbeat"),
76
+ conversationSeedMessage: summary,
77
+ };
78
+ }
79
+
35
80
  // ── Access-request copy contract ─────────────────────────────────────────────
36
81
  //
37
82
  // Deterministic helpers for building guardian-facing access-request copy.
@@ -505,6 +550,8 @@ const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
505
550
  body: str(payload.body, "A watcher event requires your attention"),
506
551
  }),
507
552
 
553
+ "heartbeat.alert": buildHeartbeatAlertCopy,
554
+
508
555
  "tool_confirmation.required_action": (payload) => ({
509
556
  title: "Tool Confirmation",
510
557
  body: str(payload.toolName, "A tool") + " requires your confirmation",
@@ -35,6 +35,7 @@ import {
35
35
  composeFallbackCopy,
36
36
  hasAccessRequestInstructions,
37
37
  hasInviteFlowDirective,
38
+ looksLikeIntermediaryInstruction,
38
39
  } from "./copy-composer.js";
39
40
  import { createDecision } from "./decisions-store.js";
40
41
  import {
@@ -127,11 +128,13 @@ function buildSystemPrompt(
127
128
  ``,
128
129
  `Copy guidelines (three distinct outputs):`,
129
130
  `- \`title\` and \`body\` are for native notification popups (e.g. vellum desktop/mobile) — keep them short and glanceable (title ≤ 8 words, body ≤ 2 sentences).`,
131
+ ` - Write popup copy as final copy for the guardian or recipient. Do not write instructions for the assistant or another intermediary.`,
130
132
  `- \`deliveryText\` is the channel-native message for chat channels (e.g. telegram). It must read naturally as a standalone message.`,
131
133
  ` - Do not prepend mechanical labels like "Conversation:".`,
132
134
  ` - Do not mention channel or transport names (e.g. Telegram, Slack, email) unless the event context explicitly requires it.`,
133
135
  ` - Do not repeat title/body verbatim unless that repetition is truly necessary.`,
134
136
  ` - Avoid meta-send phrasing (e.g. "I'd like to send a notification", "May I go ahead with that?"). Write the recipient-facing message directly.`,
137
+ ` - Avoid intermediary-instruction phrasing like "consider telling the guardian", "ask the recipient to", or "the assistant should remind them". Rewrite it as final copy the recipient can act on directly.`,
135
138
  ` - For telegram: 1-2 concise sentences.`,
136
139
  `- \`conversationSeedMessage\` is the opening message in the internal notification conversation — it can be richer and more contextual.`,
137
140
  ` - For vellum (desktop): 2-4 short sentences with useful context and clear next step if action is required.`,
@@ -664,6 +667,47 @@ function enforceAccessRequestInstructions(
664
667
  };
665
668
  }
666
669
 
670
+ function enforceHeartbeatAlertCopy(
671
+ decision: NotificationDecision,
672
+ signal: NotificationSignal,
673
+ ): NotificationDecision {
674
+ if (signal.sourceEventName !== "heartbeat.alert") return decision;
675
+ if (!decision.shouldNotify || decision.selectedChannels.length === 0)
676
+ return decision;
677
+
678
+ const fallbackCopy = composeFallbackCopy(signal, decision.selectedChannels);
679
+ const nextCopy: Partial<Record<NotificationChannel, RenderedChannelCopy>> = {
680
+ ...decision.renderedCopy,
681
+ };
682
+
683
+ for (const channel of decision.selectedChannels) {
684
+ const currentCopy = nextCopy[channel];
685
+ if (
686
+ currentCopy &&
687
+ !heartbeatCopyLooksLikeIntermediaryInstruction(currentCopy)
688
+ ) {
689
+ continue;
690
+ }
691
+ const safeCopy = fallbackCopy[channel];
692
+ if (!safeCopy) continue;
693
+ nextCopy[channel] = safeCopy;
694
+ }
695
+
696
+ return {
697
+ ...decision,
698
+ renderedCopy: nextCopy,
699
+ };
700
+ }
701
+
702
+ function heartbeatCopyLooksLikeIntermediaryInstruction(
703
+ copy: RenderedChannelCopy,
704
+ ): boolean {
705
+ return [copy.title, copy.body, copy.deliveryText].some(
706
+ (value) =>
707
+ typeof value === "string" && looksLikeIntermediaryInstruction(value),
708
+ );
709
+ }
710
+
667
711
  function ensureAccessRequestInstructionsInCopy(
668
712
  copy: RenderedChannelCopy,
669
713
  requestCode: string,
@@ -754,6 +798,7 @@ export async function evaluateSignal(
754
798
  let decision = buildFallbackDecision(signal, availableChannels);
755
799
  decision = enforceGuardianRequestCode(decision, signal);
756
800
  decision = enforceAccessRequestInstructions(decision, signal);
801
+ decision = enforceHeartbeatAlertCopy(decision, signal);
757
802
  decision = enforceGuardianCallConversationAffinity(decision, signal);
758
803
  decision = enforceConversationAffinity(
759
804
  decision,
@@ -783,6 +828,7 @@ export async function evaluateSignal(
783
828
 
784
829
  decision = enforceGuardianRequestCode(decision, signal);
785
830
  decision = enforceAccessRequestInstructions(decision, signal);
831
+ decision = enforceHeartbeatAlertCopy(decision, signal);
786
832
  decision = enforceGuardianCallConversationAffinity(decision, signal);
787
833
  decision = enforceConversationAffinity(
788
834
  decision,
@@ -101,6 +101,10 @@ export const NOTIFICATION_SOURCE_EVENT_NAMES = [
101
101
  description:
102
102
  "OAuth credential health issue detected (expired, revoked, missing scopes)",
103
103
  },
104
+ {
105
+ id: "heartbeat.alert",
106
+ description: "Heartbeat found something worth surfacing to the guardian",
107
+ },
104
108
  ] as const;
105
109
 
106
110
  export type NotificationSourceEventName =
@@ -54,7 +54,9 @@ Most existing logos come from [Simple Icons](https://simpleicons.org) (CC0-licen
54
54
 
55
55
  If the service is not on Simple Icons, source or create an SVG and convert it the same way. The result must be a true vector PDF (not a rasterized image wrapped in PDF) so it scales cleanly.
56
56
 
57
- The `logoUrl` field in `seed-providers.ts` still serves as the remote fallback (typically a Simple Icons CDN URL like `https://cdn.simpleicons.org/acme`). The client renders the local PDF first, then falls back to `logoUrl`, then to an initials avatar.
57
+ The `logoUrl` field in `seed-providers.ts` serves as the remote fallback (most providers use a Simple Icons CDN URL like `https://cdn.simpleicons.org/acme`). The client renders the local PDF first, then falls back to `logoUrl`, then to an initials avatar.
58
+
59
+ For brands Simple Icons doesn't host (e.g. Salesforce, which Simple Icons removed for trademark reasons), use the same `glincker/thesvg` source via jsDelivr — `https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/<key>/default.svg`. The recognised `logoUrl` prefixes are enforced by `oauth-provider-seed-logos.test.ts`; if you need a third source, extend that allowlist alongside the manifest in `clients/shared/Resources/integration-logos-manifest.json`.
58
60
 
59
61
  ### 5. Secret patterns (if applicable) — `../security/secret-patterns.ts`
60
62
 
@@ -0,0 +1,137 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ _clearAllOAuthConnectStates,
5
+ clearExpiredOAuthConnectStates,
6
+ getOAuthConnectState,
7
+ setOAuthConnectComplete,
8
+ setOAuthConnectError,
9
+ setOAuthConnectPending,
10
+ } from "../oauth-connect-state.js";
11
+
12
+ describe("oauth-connect-state", () => {
13
+ beforeEach(() => {
14
+ _clearAllOAuthConnectStates();
15
+ });
16
+
17
+ test("setOAuthConnectPending → getOAuthConnectState returns pending", () => {
18
+ setOAuthConnectPending("state-1", "google");
19
+ const result = getOAuthConnectState("state-1");
20
+ expect(result).toMatchObject({ status: "pending", service: "google" });
21
+ });
22
+
23
+ test("setOAuthConnectComplete without accountInfo → returns complete", () => {
24
+ setOAuthConnectComplete("state-1", "google");
25
+ const result = getOAuthConnectState("state-1");
26
+ expect(result).toMatchObject({ status: "complete", service: "google" });
27
+ });
28
+
29
+ test("setOAuthConnectComplete with accountInfo → returns complete with accountInfo", () => {
30
+ setOAuthConnectComplete("state-1", "google", "user@example.com");
31
+ const result = getOAuthConnectState("state-1");
32
+ expect(result).toMatchObject({
33
+ status: "complete",
34
+ service: "google",
35
+ accountInfo: "user@example.com",
36
+ });
37
+ });
38
+
39
+ test("setOAuthConnectComplete with grantedScopes → returns complete with grantedScopes", () => {
40
+ setOAuthConnectComplete("state-1", "google", "user@example.com", ["scope:read", "scope:write"]);
41
+ const result = getOAuthConnectState("state-1");
42
+ expect(result).toMatchObject({
43
+ status: "complete",
44
+ service: "google",
45
+ accountInfo: "user@example.com",
46
+ grantedScopes: ["scope:read", "scope:write"],
47
+ });
48
+ });
49
+
50
+ test("setOAuthConnectError → returns error with message", () => {
51
+ setOAuthConnectError("state-1", "google", "token exchange failed");
52
+ const result = getOAuthConnectState("state-1");
53
+ expect(result).toMatchObject({
54
+ status: "error",
55
+ service: "google",
56
+ error: "token exchange failed",
57
+ });
58
+ });
59
+
60
+ test("re-setting same state token overwrites previous", () => {
61
+ setOAuthConnectPending("state-1", "google");
62
+ setOAuthConnectComplete("state-1", "google", "user@example.com");
63
+ const result = getOAuthConnectState("state-1");
64
+ expect(result?.status).toBe("complete");
65
+ });
66
+
67
+ test("getOAuthConnectState returns null for unknown state", () => {
68
+ expect(getOAuthConnectState("nonexistent")).toBeNull();
69
+ });
70
+
71
+ test("_clearAllOAuthConnectStates removes all entries", () => {
72
+ setOAuthConnectPending("state-1", "google");
73
+ setOAuthConnectPending("state-2", "github");
74
+ _clearAllOAuthConnectStates();
75
+ expect(getOAuthConnectState("state-1")).toBeNull();
76
+ expect(getOAuthConnectState("state-2")).toBeNull();
77
+ });
78
+
79
+ test("clearExpiredOAuthConnectStates removes expired pending entries", () => {
80
+ setOAuthConnectPending("state-1", "google");
81
+ // Advance Date.now by 6 minutes past PENDING_TTL_MS (5 min)
82
+ const originalNow = Date.now;
83
+ Date.now = () => originalNow() + 6 * 60 * 1000;
84
+ clearExpiredOAuthConnectStates();
85
+ Date.now = originalNow;
86
+ expect(getOAuthConnectState("state-1")).toBeNull();
87
+ });
88
+
89
+ test("clearExpiredOAuthConnectStates removes expired complete entries (past 60s grace)", () => {
90
+ setOAuthConnectComplete("state-1", "google");
91
+ const originalNow = Date.now;
92
+ Date.now = () => originalNow() + 2 * 60 * 1000; // advance 2 minutes past 60s grace
93
+ clearExpiredOAuthConnectStates();
94
+ Date.now = originalNow;
95
+ expect(getOAuthConnectState("state-1")).toBeNull();
96
+ });
97
+
98
+ test("clearExpiredOAuthConnectStates removes expired error entries (past 60s grace)", () => {
99
+ setOAuthConnectError("state-1", "google", "token exchange failed");
100
+ const originalNow = Date.now;
101
+ Date.now = () => originalNow() + 2 * 60 * 1000; // advance 2 minutes past 60s grace
102
+ clearExpiredOAuthConnectStates();
103
+ Date.now = originalNow;
104
+ expect(getOAuthConnectState("state-1")).toBeNull();
105
+ });
106
+
107
+ test("clearExpiredOAuthConnectStates does not remove non-expired pending entries", () => {
108
+ setOAuthConnectPending("state-1", "google");
109
+ clearExpiredOAuthConnectStates(); // called without advancing time
110
+ expect(getOAuthConnectState("state-1")).not.toBeNull();
111
+ });
112
+
113
+ test("sweep-on-insert: setOAuthConnectPending purges expired entries before inserting new one", () => {
114
+ // 1. Add an entry that will expire
115
+ setOAuthConnectPending("expired-state", "google");
116
+
117
+ // 2. Advance Date.now past the PENDING_TTL_MS (5 min)
118
+ const originalNow = Date.now;
119
+ Date.now = () => originalNow() + 6 * 60 * 1000;
120
+
121
+ // 3. Insert a new entry — this should trigger clearExpiredOAuthConnectStates() internally
122
+ setOAuthConnectPending("new-state", "github");
123
+
124
+ // 4. Restore Date.now before assertions (getOAuthConnectState also calls clearExpiredOAuthConnectStates)
125
+ Date.now = originalNow;
126
+
127
+ // The expired entry must have been swept out during the insert
128
+ // Use the map directly via getOAuthConnectState — expired-state is gone
129
+ // We call _clearAllOAuthConnectStates in beforeEach so we know the map started empty.
130
+ // After the insert the map should only contain "new-state".
131
+ const expiredResult = getOAuthConnectState("expired-state");
132
+ expect(expiredResult).toBeNull();
133
+
134
+ const newResult = getOAuthConnectState("new-state");
135
+ expect(newResult).toMatchObject({ status: "pending", service: "github" });
136
+ });
137
+ });
@@ -81,6 +81,7 @@ export interface OAuthConnectOptions {
81
81
  success: boolean;
82
82
  service: string;
83
83
  accountInfo?: string;
84
+ grantedScopes?: string[];
84
85
  error?: string;
85
86
  }) => void;
86
87
  }
@@ -256,6 +257,7 @@ export async function orchestrateOAuthConnect(
256
257
  success: true,
257
258
  service: options.service,
258
259
  accountInfo: stored.accountInfo ?? parsedAccountIdentifier,
260
+ grantedScopes: result.grantedScopes,
259
261
  });
260
262
  } catch (err) {
261
263
  log.error(
@@ -62,7 +62,10 @@ mock.module("../platform/client.js", () => ({
62
62
  // ---------------------------------------------------------------------------
63
63
 
64
64
  import { BYOOAuthConnection } from "./byo-connection.js";
65
- import { resolveOAuthConnection } from "./connection-resolver.js";
65
+ import {
66
+ resolveEffectiveBaseUrl,
67
+ resolveOAuthConnection,
68
+ } from "./connection-resolver.js";
66
69
  import { PlatformOAuthConnection } from "./platform-connection.js";
67
70
 
68
71
  // ---------------------------------------------------------------------------
@@ -214,3 +217,65 @@ describe("resolveOAuthConnection", () => {
214
217
  ).rejects.toThrow(/No active OAuth connection found/);
215
218
  });
216
219
  });
220
+
221
+ describe("resolveEffectiveBaseUrl", () => {
222
+ const fallback = "https://login.salesforce.com";
223
+
224
+ test("uses instance_url from JSON-string metadata for Salesforce", () => {
225
+ const metadata = JSON.stringify({
226
+ instance_url: "https://acme.my.salesforce.com",
227
+ issued_at: "1714000000000",
228
+ });
229
+ expect(resolveEffectiveBaseUrl("salesforce", fallback, metadata)).toBe(
230
+ "https://acme.my.salesforce.com",
231
+ );
232
+ });
233
+
234
+ test("uses instance_url from already-parsed object metadata", () => {
235
+ const metadata = { instance_url: "https://na162.salesforce.com" };
236
+ expect(resolveEffectiveBaseUrl("salesforce", fallback, metadata)).toBe(
237
+ "https://na162.salesforce.com",
238
+ );
239
+ });
240
+
241
+ test("falls back to seed baseUrl when metadata is null", () => {
242
+ expect(resolveEffectiveBaseUrl("salesforce", fallback, null)).toBe(
243
+ fallback,
244
+ );
245
+ });
246
+
247
+ test("falls back to seed baseUrl when instance_url is empty string", () => {
248
+ const metadata = JSON.stringify({ instance_url: "" });
249
+ expect(resolveEffectiveBaseUrl("salesforce", fallback, metadata)).toBe(
250
+ fallback,
251
+ );
252
+ });
253
+
254
+ test("falls back to seed baseUrl when metadata is unparseable JSON", () => {
255
+ expect(
256
+ resolveEffectiveBaseUrl("salesforce", fallback, "{ not valid json"),
257
+ ).toBe(fallback);
258
+ });
259
+
260
+ test("falls back to seed baseUrl when instance_url is the wrong type", () => {
261
+ const metadata = JSON.stringify({ instance_url: 12345 });
262
+ expect(resolveEffectiveBaseUrl("salesforce", fallback, metadata)).toBe(
263
+ fallback,
264
+ );
265
+ });
266
+
267
+ test("ignores instance_url for non-Salesforce providers", () => {
268
+ // A different provider whose token response happens to include an
269
+ // instance_url-shaped field MUST NOT have its baseUrl rewritten.
270
+ const metadata = JSON.stringify({
271
+ instance_url: "https://attacker.example.com",
272
+ });
273
+ expect(
274
+ resolveEffectiveBaseUrl(
275
+ "google",
276
+ "https://gmail.googleapis.com/gmail/v1/users/me",
277
+ metadata,
278
+ ),
279
+ ).toBe("https://gmail.googleapis.com/gmail/v1/users/me");
280
+ });
281
+ });
@@ -116,11 +116,65 @@ export async function resolveOAuthConnection(
116
116
  return new BYOOAuthConnection({
117
117
  id: conn.id,
118
118
  provider: conn.provider,
119
- baseUrl,
119
+ baseUrl: resolveEffectiveBaseUrl(conn.provider, baseUrl, conn.metadata),
120
120
  accountInfo: conn.accountInfo,
121
121
  });
122
122
  }
123
123
 
124
+ /**
125
+ * Resolve the effective API base URL for a connection, preferring per-tenant
126
+ * values stored on the connection's `metadata` over the provider's static
127
+ * seed value when applicable.
128
+ *
129
+ * Salesforce is the only provider that needs this: every org has its own
130
+ * API instance host (``acme.my.salesforce.com``, ``na162.salesforce.com``)
131
+ * which is returned in the OAuth token response as ``instance_url`` and
132
+ * captured into ``oauth_connection.metadata`` by ``storeOAuth2Tokens``.
133
+ * The seed's ``baseUrl`` for Salesforce is the login domain
134
+ * (``https://login.salesforce.com``) — correct for the OAuth handshake but
135
+ * wrong for REST API calls. Pulling the per-connection ``instance_url``
136
+ * here avoids forcing every caller to override ``baseUrl`` per-request.
137
+ *
138
+ * For all other providers the seed value is correct (single API domain),
139
+ * so we return it unchanged.
140
+ *
141
+ * If a future provider needs the same treatment, generalize via a
142
+ * declarative ``baseUrlMetadataKey`` field on the seed entry rather than
143
+ * adding more provider-name branches here.
144
+ */
145
+ export function resolveEffectiveBaseUrl(
146
+ provider: string,
147
+ fallbackBaseUrl: string,
148
+ rawMetadata: unknown,
149
+ ): string {
150
+ if (provider !== "salesforce") return fallbackBaseUrl;
151
+
152
+ const metadata = parseConnectionMetadata(rawMetadata);
153
+ const instanceUrl = metadata?.instance_url;
154
+ if (typeof instanceUrl === "string" && instanceUrl.length > 0) {
155
+ return instanceUrl;
156
+ }
157
+ return fallbackBaseUrl;
158
+ }
159
+
160
+ function parseConnectionMetadata(
161
+ raw: unknown,
162
+ ): Record<string, unknown> | undefined {
163
+ if (raw == null) return undefined;
164
+ if (typeof raw === "object") {
165
+ return raw as Record<string, unknown>;
166
+ }
167
+ if (typeof raw !== "string") return undefined;
168
+ try {
169
+ const parsed = JSON.parse(raw);
170
+ return typeof parsed === "object" && parsed !== null
171
+ ? (parsed as Record<string, unknown>)
172
+ : undefined;
173
+ } catch {
174
+ return undefined;
175
+ }
176
+ }
177
+
124
178
  // ---------------------------------------------------------------------------
125
179
  // Platform connection ID resolution
126
180
  // ---------------------------------------------------------------------------
@@ -0,0 +1,77 @@
1
+ /**
2
+ * In-memory OAuth connect flow status map.
3
+ *
4
+ * Tracks the current state of daemon-owned OAuth connect flows so the CLI
5
+ * can poll for completion via the IPC route.
6
+ */
7
+ type OAuthConnectState =
8
+ | { status: "pending"; service: string; expiresAt: number }
9
+ | { status: "complete"; service: string; accountInfo?: string; grantedScopes?: string[]; completedAt: number }
10
+ | { status: "error"; service: string; error: string; failedAt: number };
11
+
12
+ const activeOAuthConnectFlows = new Map<string, OAuthConnectState>();
13
+
14
+ const PENDING_TTL_MS = 5 * 60 * 1000; // 5 min — matches oauth-callback-registry.ts:14
15
+ const COMPLETION_GRACE_MS = 60 * 1000; // 60s so the polling CLI gets one final read
16
+
17
+ export function setOAuthConnectPending(state: string, service: string): void {
18
+ clearExpiredOAuthConnectStates();
19
+ activeOAuthConnectFlows.set(state, {
20
+ status: "pending",
21
+ service,
22
+ expiresAt: Date.now() + PENDING_TTL_MS,
23
+ });
24
+ }
25
+
26
+ export function setOAuthConnectComplete(
27
+ state: string,
28
+ service: string,
29
+ accountInfo?: string,
30
+ grantedScopes?: string[],
31
+ ): void {
32
+ clearExpiredOAuthConnectStates();
33
+ activeOAuthConnectFlows.set(state, {
34
+ status: "complete",
35
+ service,
36
+ accountInfo,
37
+ grantedScopes,
38
+ completedAt: Date.now(),
39
+ });
40
+ }
41
+
42
+ export function setOAuthConnectError(
43
+ state: string,
44
+ service: string,
45
+ error: string,
46
+ ): void {
47
+ clearExpiredOAuthConnectStates();
48
+ activeOAuthConnectFlows.set(state, {
49
+ status: "error",
50
+ service,
51
+ error,
52
+ failedAt: Date.now(),
53
+ });
54
+ }
55
+
56
+ export function getOAuthConnectState(state: string): OAuthConnectState | null {
57
+ clearExpiredOAuthConnectStates();
58
+ return activeOAuthConnectFlows.get(state) ?? null;
59
+ }
60
+
61
+ export function clearExpiredOAuthConnectStates(): void {
62
+ const now = Date.now();
63
+ for (const [key, state] of activeOAuthConnectFlows) {
64
+ if (state.status === "pending" && now > state.expiresAt) {
65
+ activeOAuthConnectFlows.delete(key);
66
+ } else if (state.status === "complete" && now > state.completedAt + COMPLETION_GRACE_MS) {
67
+ activeOAuthConnectFlows.delete(key);
68
+ } else if (state.status === "error" && now > state.failedAt + COMPLETION_GRACE_MS) {
69
+ activeOAuthConnectFlows.delete(key);
70
+ }
71
+ }
72
+ }
73
+
74
+ /** Test-only helper — clears all state for test isolation. */
75
+ export function _clearAllOAuthConnectStates(): void {
76
+ activeOAuthConnectFlows.clear();
77
+ }
@@ -393,6 +393,7 @@ export const PROVIDER_SEED_DATA: Record<
393
393
  { scope: "project:delete", description: "Delete entire projects" },
394
394
  ],
395
395
  loopbackPort: 17325,
396
+ managedServiceConfigKey: "todoist-oauth",
396
397
  injectionTemplates: [
397
398
  {
398
399
  hostPattern: "api.todoist.com",
@@ -402,7 +403,7 @@ export const PROVIDER_SEED_DATA: Record<
402
403
  },
403
404
  ],
404
405
  appType: "App",
405
- identityUrl: "https://api.todoist.com/sync/v9/sync",
406
+ identityUrl: "https://api.todoist.com/api/v1/sync",
406
407
  identityMethod: "POST",
407
408
  identityHeaders: { "Content-Type": "application/x-www-form-urlencoded" },
408
409
  identityBody: "sync_token=*&resource_types=[%22user%22]",
@@ -429,6 +430,7 @@ export const PROVIDER_SEED_DATA: Record<
429
430
  availableScopes:
430
431
  "https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes",
431
432
  loopbackPort: 17326,
433
+ managedServiceConfigKey: "discord-oauth",
432
434
  injectionTemplates: [
433
435
  {
434
436
  hostPattern: "discord.com",
@@ -497,6 +499,7 @@ export const PROVIDER_SEED_DATA: Record<
497
499
  defaultScopes: ["default"],
498
500
  availableScopes: "https://developers.asana.com/docs/oauth-scopes",
499
501
  loopbackPort: 17328,
502
+ managedServiceConfigKey: "asana-oauth",
500
503
  injectionTemplates: [
501
504
  {
502
505
  hostPattern: "app.asana.com",
@@ -563,6 +566,7 @@ export const PROVIDER_SEED_DATA: Record<
563
566
  availableScopes:
564
567
  "https://developers.hubspot.com/docs/guides/apps/authentication/scopes",
565
568
  loopbackPort: 17330,
569
+ managedServiceConfigKey: "hubspot-oauth",
566
570
  injectionTemplates: [
567
571
  {
568
572
  hostPattern: "api.hubapi.com",
@@ -576,6 +580,59 @@ export const PROVIDER_SEED_DATA: Record<
576
580
  identityResponsePaths: ["user", "hub_domain"],
577
581
  },
578
582
 
583
+ salesforce: {
584
+ provider: "salesforce",
585
+ authorizeUrl: "https://login.salesforce.com/services/oauth2/authorize",
586
+ tokenExchangeUrl: "https://login.salesforce.com/services/oauth2/token",
587
+ refreshUrl: "https://login.salesforce.com/services/oauth2/token",
588
+ pingUrl: "https://login.salesforce.com/services/oauth2/userinfo",
589
+ // baseUrl points at the login domain — correct for the OAuth handshake
590
+ // and for ``/services/oauth2/userinfo``/``revoke`` calls. REST API calls
591
+ // to ``/services/data/...`` go to the per-org instance host returned in
592
+ // the token response as ``instance_url`` and stored on
593
+ // ``oauth_connection.metadata``. ``connection-resolver.ts`` substitutes
594
+ // that instance URL when constructing the BYO connection so callers
595
+ // don't need to override ``baseUrl`` per request.
596
+ baseUrl: "https://login.salesforce.com",
597
+ displayLabel: "Salesforce",
598
+ description: "CRM contacts, leads, and opportunities",
599
+ dashboardUrl:
600
+ "https://help.salesforce.com/s/articleView?id=sf.connected_app_create.htm&type=5",
601
+ clientIdPlaceholder: null,
602
+ logoUrl:
603
+ "https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/salesforce/default.svg",
604
+ defaultScopes: ["api", "refresh_token", "openid", "email", "profile"],
605
+ availableScopes:
606
+ "https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_tokens_scopes.htm",
607
+ authorizeParams: { prompt: "consent" },
608
+ tokenEndpointAuthMethod: "client_secret_post",
609
+ loopbackPort: 17336,
610
+ // Salesforce REST traffic goes to per-org instance hosts like
611
+ // ``acme.my.salesforce.com`` and ``acme.lightning.force.com``.
612
+ // ``matchHostPattern`` only treats ``*.<domain>`` as a wildcard match —
613
+ // bare ``salesforce.com`` would only match the apex. Use wildcards so
614
+ // ``Authorization: Bearer`` injection actually fires on tenant hosts.
615
+ injectionTemplates: [
616
+ {
617
+ hostPattern: "*.salesforce.com",
618
+ injectionType: "header",
619
+ headerName: "Authorization",
620
+ valuePrefix: "Bearer ",
621
+ },
622
+ {
623
+ hostPattern: "*.force.com",
624
+ injectionType: "header",
625
+ headerName: "Authorization",
626
+ valuePrefix: "Bearer ",
627
+ },
628
+ ],
629
+ revokeUrl: "https://login.salesforce.com/services/oauth2/revoke",
630
+ revokeBodyTemplate: { token: "{access_token}" },
631
+ appType: "Connected App",
632
+ identityUrl: "https://login.salesforce.com/services/oauth2/userinfo",
633
+ identityResponsePaths: ["email", "preferred_username"],
634
+ },
635
+
579
636
  figma: {
580
637
  provider: "figma",
581
638
  authorizeUrl: "https://www.figma.com/oauth",