@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
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Same-actor (same-user) binding check used by host proxies and result
3
+ * routes.
4
+ *
5
+ * Verifies that the submitting (source) actor's principal id matches the
6
+ * actor principal id captured for the target client at SSE subscription
7
+ * time. This is the authoritative gate that prevents cross-user
8
+ * execution and cross-user result submission across all three host-proxy
9
+ * capabilities (host_bash, host_file, host_cu).
10
+ *
11
+ * Two entry points map onto the two control-flow styles in the codebase:
12
+ * - {@link enforceSameActorOrErrorResult} for proxies — returns a
13
+ * tool-execution error result on rejection, `null` on success.
14
+ * - {@link enforceSameActorOrThrow} for HTTP/IPC route handlers —
15
+ * throws {@link ForbiddenError} on rejection so the route adapter
16
+ * maps it to HTTP 403.
17
+ *
18
+ * Both paths log a single structured warn line on rejection with the
19
+ * shape `{ sourceActorPrincipalId, targetClientId, targetActorPrincipalId,
20
+ * op, reason }` so that bash, file, and CU rejections render identically
21
+ * in the audit log.
22
+ */
23
+ import type { HostProxyCapability } from "../../channels/types.js";
24
+ import { getLogger } from "../../util/logger.js";
25
+ import type { AssistantEventHub } from "../assistant-event-hub.js";
26
+ import { ForbiddenError } from "../routes/errors.js";
27
+
28
+ const log = getLogger("same-actor");
29
+
30
+ /**
31
+ * Canonical user-facing rejection message. Used by both the proxy and
32
+ * route paths so operators and auditors see identical wording regardless
33
+ * of whether the failure surfaced as a tool-execution result or an HTTP
34
+ * 403.
35
+ */
36
+ const REJECTION_MESSAGE =
37
+ "Submitting actor does not match the target client's actor for this request. The targeted client's authenticated user must submit the result.";
38
+
39
+ /** OpenAPI 403 description for `*-result` endpoints, kept identical. */
40
+ export const SAME_ACTOR_FORBIDDEN_DESCRIPTION =
41
+ "Submitting client does not match the targeted client, or the submitting actor's principal does not match the target client's actor.";
42
+
43
+ /** Per-capability scope for the structured warn log entry. */
44
+ export type SameActorOp =
45
+ | "host_bash"
46
+ | "host_file"
47
+ | "host_cu"
48
+ | "host_transfer";
49
+
50
+ /**
51
+ * Args for the live-lookup variant: caller supplies the hub + target client
52
+ * id, and the helper looks up the target's actor principal in real time.
53
+ * Used at proxy request time (registration), where the SSE subscription is
54
+ * present by definition.
55
+ */
56
+ export interface SameActorLiveArgs {
57
+ hub: Pick<AssistantEventHub, "getActorPrincipalIdForClient">;
58
+ sourceActorPrincipalId: string | undefined;
59
+ targetClientId: string;
60
+ op: SameActorOp;
61
+ }
62
+
63
+ /**
64
+ * Args for the persisted-value variant: caller supplies a target actor
65
+ * principal id captured at registration time. Used at result-submission
66
+ * time, where the SSE subscription may have briefly disconnected and the
67
+ * live hub lookup would falsely 403 a legitimate result.
68
+ */
69
+ export interface SameActorPersistedArgs {
70
+ sourceActorPrincipalId: string | undefined;
71
+ targetActorPrincipalId: string | undefined;
72
+ targetClientId: string;
73
+ op: SameActorOp;
74
+ }
75
+
76
+ export type SameActorArgs = SameActorLiveArgs;
77
+
78
+ type RejectionReason = "missing_source" | "missing_target" | "mismatch";
79
+
80
+ function isLive(
81
+ args: SameActorLiveArgs | SameActorPersistedArgs,
82
+ ): args is SameActorLiveArgs {
83
+ return (args as SameActorLiveArgs).hub != null;
84
+ }
85
+
86
+ /**
87
+ * Internal: returns the rejection reason or `undefined` when the source
88
+ * matches the target. Always logs on rejection so all callers share the
89
+ * same audit shape.
90
+ */
91
+ function detectRejection(
92
+ args: SameActorLiveArgs | SameActorPersistedArgs,
93
+ ): RejectionReason | undefined {
94
+ const { sourceActorPrincipalId, targetClientId, op } = args;
95
+ const targetActorPrincipalId = isLive(args)
96
+ ? args.hub.getActorPrincipalIdForClient(targetClientId)
97
+ : args.targetActorPrincipalId;
98
+
99
+ let reason: RejectionReason | undefined;
100
+ if (sourceActorPrincipalId == null) {
101
+ reason = "missing_source";
102
+ } else if (targetActorPrincipalId == null) {
103
+ reason = "missing_target";
104
+ } else if (sourceActorPrincipalId !== targetActorPrincipalId) {
105
+ reason = "mismatch";
106
+ }
107
+ if (reason == null) return undefined;
108
+
109
+ log.warn(
110
+ {
111
+ sourceActorPrincipalId,
112
+ targetClientId,
113
+ targetActorPrincipalId,
114
+ op,
115
+ reason,
116
+ },
117
+ "Rejecting cross-user host proxy request",
118
+ );
119
+ return reason;
120
+ }
121
+
122
+ /**
123
+ * Route-flavored variant: throws {@link ForbiddenError} on rejection so
124
+ * the existing route adapter maps it to HTTP 403. Returns void on
125
+ * success.
126
+ *
127
+ * Accepts EITHER {@link SameActorLiveArgs} (live hub lookup, used at
128
+ * proxy registration time) OR {@link SameActorPersistedArgs} (compare
129
+ * against a value captured earlier, used at result-submission time so a
130
+ * brief SSE reconnect doesn't 403 a legitimate result).
131
+ */
132
+ export function enforceSameActorOrThrow(
133
+ args: SameActorLiveArgs | SameActorPersistedArgs,
134
+ ): void {
135
+ if (detectRejection(args) != null) {
136
+ throw new ForbiddenError(REJECTION_MESSAGE);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Proxy-flavored variant: returns a tool-execution-shaped error result
142
+ * on rejection (so the proxy can pass it directly back to the agent),
143
+ * or `null` on success. Always uses the live hub lookup — proxy
144
+ * registration runs while the target SSE subscription is active.
145
+ */
146
+ export function enforceSameActorOrErrorResult(
147
+ args: SameActorLiveArgs,
148
+ ): { content: string; isError: true } | null {
149
+ if (detectRejection(args) == null) return null;
150
+ return { content: REJECTION_MESSAGE, isError: true };
151
+ }
152
+
153
+ /**
154
+ * Result of attempting to auto-resolve a single same-user target client.
155
+ *
156
+ * - `match`: exactly one same-user client supports the capability. Use the
157
+ * returned clientId.
158
+ * - `none`: no same-user client supports the capability. Caller's choice
159
+ * how to handle (typically: fall through to no-target, which broadcasts
160
+ * to nobody when no clients are connected).
161
+ * - `ambiguous`: more than one same-user client supports the capability.
162
+ * Caller MUST refuse to silently broadcast across them; instead surface
163
+ * an error asking the caller to specify `target_client_id`.
164
+ */
165
+ export type AutoResolveResult =
166
+ | { kind: "match"; clientId: string }
167
+ | { kind: "none" }
168
+ | { kind: "ambiguous" };
169
+
170
+ /**
171
+ * Filter capable clients by `actorPrincipalId === sourcePrincipalId` and
172
+ * report whether exactly one matched, zero matched, or more than one
173
+ * matched.
174
+ *
175
+ * Used by host proxies to auto-resolve a target client when the caller
176
+ * did not specify one. Skipping when the caller has no principal keeps
177
+ * the same-user binding closed: an unauthenticated caller cannot
178
+ * piggyback on a connected user's session.
179
+ *
180
+ * Why three outcomes (vs. just `string | undefined`)? Earlier revisions
181
+ * collapsed `none` and `ambiguous` into `undefined`, which caused the
182
+ * proxy to fall through to an untargeted broadcast — fanning a single
183
+ * targeted-style request out across every same-user machine. Surfacing
184
+ * `ambiguous` separately lets the proxy reject with a clear "specify
185
+ * target_client_id" error instead.
186
+ */
187
+ export function pickSameUserAutoResolve(args: {
188
+ hub: Pick<AssistantEventHub, "listClientsByCapability">;
189
+ capability: HostProxyCapability;
190
+ sourceActorPrincipalId: string | undefined;
191
+ }): AutoResolveResult {
192
+ const { hub, capability, sourceActorPrincipalId } = args;
193
+ if (sourceActorPrincipalId == null) return { kind: "none" };
194
+ const sameUser = hub
195
+ .listClientsByCapability(capability)
196
+ .filter((c) => c.actorPrincipalId === sourceActorPrincipalId);
197
+ if (sameUser.length === 0) return { kind: "none" };
198
+ if (sameUser.length === 1) {
199
+ return { kind: "match", clientId: sameUser[0].clientId };
200
+ }
201
+ return { kind: "ambiguous" };
202
+ }
203
+
204
+ /**
205
+ * Standard error result for proxies when {@link pickSameUserAutoResolve}
206
+ * returns `ambiguous`. Asks the caller to specify `target_client_id`.
207
+ */
208
+ export function ambiguousSameUserError(capability: HostProxyCapability): {
209
+ content: string;
210
+ isError: true;
211
+ } {
212
+ return {
213
+ content: `Multiple ${capability} clients are connected for this user. Specify target_client_id to disambiguate. Run \`assistant clients list --capability ${capability}\` to see client IDs.`,
214
+ isError: true,
215
+ };
216
+ }
@@ -160,8 +160,9 @@ export function handleChannelDecision(
160
160
  : pending[0];
161
161
  if (!info) return { applied: false };
162
162
 
163
- // Resolve the interaction to get the conversation and remove from tracker
164
- const resolved = pendingInteractions.resolve(info.requestId);
163
+ // Peek (not consume) resolveConfirmation() owns deregistration and
164
+ // must fire the promptResolve callback stored in the interaction.
165
+ const resolved = pendingInteractions.get(info.requestId);
165
166
  if (!resolved) return { applied: false };
166
167
 
167
168
  // Map channel-level action to the permission system's UserDecision type.
@@ -7,9 +7,11 @@ import {
7
7
  parseChannelId,
8
8
  parseInterfaceId,
9
9
  } from "../channels/types.js";
10
+ import { getDiskPressureStatus } from "../daemon/disk-pressure-guard.js";
11
+ import { classifyDiskPressureTurnPolicy } from "../daemon/disk-pressure-policy.js";
10
12
  import type { TrustContext } from "../daemon/trust-context.js";
11
13
  import { updateDeliveredSegmentCount } from "../memory/delivery-channels.js";
12
- import { linkMessage } from "../memory/delivery-crud.js";
14
+ import { clearPayload, linkMessage } from "../memory/delivery-crud.js";
13
15
  import {
14
16
  getRetryableEvents,
15
17
  markProcessed,
@@ -18,10 +20,13 @@ import {
18
20
  } from "../memory/delivery-status.js";
19
21
  import { getLogger } from "../util/logger.js";
20
22
  import { deliverReplyViaCallback } from "./channel-reply-delivery.js";
23
+ import { deliverChannelReply } from "./gateway-client.js";
21
24
  import type { MessageProcessor } from "./http-types.js";
22
25
  import { resolveRoutingStateFromRuntime } from "./trust-context-resolver.js";
23
26
 
24
27
  const log = getLogger("runtime-http");
28
+ const DISK_PRESSURE_REMOTE_BLOCK_REPLY =
29
+ "Storage is critically low, so remote messages are ignored until the guardian frees enough space. Please try again later.";
25
30
 
26
31
  function parseTrustRuntimeContext(value: unknown): TrustContext | undefined {
27
32
  if (!value || typeof value !== "object") return undefined;
@@ -163,6 +168,65 @@ export async function sweepFailedEvents(
163
168
  trustClass: "unknown",
164
169
  };
165
170
 
171
+ const diskPressureDecision = classifyDiskPressureTurnPolicy(
172
+ getDiskPressureStatus(),
173
+ {
174
+ sourceChannel,
175
+ sourceInterface,
176
+ trustContext: {
177
+ sourceChannel: trustContext.sourceChannel,
178
+ trustClass: trustContext.trustClass,
179
+ },
180
+ },
181
+ );
182
+ if (diskPressureDecision.action === "block") {
183
+ clearPayload(event.id);
184
+ markProcessed(event.id);
185
+ log.info(
186
+ {
187
+ eventId: event.id,
188
+ conversationId: event.conversationId,
189
+ reason: diskPressureDecision.reason,
190
+ trustClass: trustContext.trustClass,
191
+ },
192
+ "Skipped channel retry during disk pressure cleanup mode",
193
+ );
194
+
195
+ const replyCallbackUrl =
196
+ typeof payload.replyCallbackUrl === "string"
197
+ ? payload.replyCallbackUrl
198
+ : undefined;
199
+ const externalChatId =
200
+ typeof payload.externalChatId === "string"
201
+ ? payload.externalChatId
202
+ : undefined;
203
+ if (replyCallbackUrl && externalChatId) {
204
+ const requesterExternalUserId =
205
+ trustContext.requesterExternalUserId ??
206
+ (typeof payload.senderExternalUserId === "string"
207
+ ? payload.senderExternalUserId
208
+ : undefined);
209
+ const replyPayload: Parameters<typeof deliverChannelReply>[1] = {
210
+ chatId: externalChatId,
211
+ text: DISK_PRESSURE_REMOTE_BLOCK_REPLY,
212
+ assistantId,
213
+ };
214
+ if (sourceChannel === "slack" && requesterExternalUserId) {
215
+ replyPayload.ephemeral = true;
216
+ replyPayload.user = requesterExternalUserId;
217
+ }
218
+ try {
219
+ await deliverChannelReply(replyCallbackUrl, replyPayload);
220
+ } catch (err) {
221
+ log.warn(
222
+ { err, eventId: event.id, conversationId: event.conversationId },
223
+ "Failed to deliver disk pressure retry block reply",
224
+ );
225
+ }
226
+ }
227
+ continue;
228
+ }
229
+
166
230
  const metadataHintsRaw = sourceMetadata?.hints;
167
231
  const metadataHints = Array.isArray(metadataHintsRaw)
168
232
  ? metadataHintsRaw.filter(
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import type { ChannelId } from "../channels/types.js";
15
+ import { isHttpAuthDisabled } from "../config/env.js";
15
16
  import { findGuardianForChannel } from "../contacts/contact-store.js";
16
17
  import type { TrustContext } from "../daemon/trust-context.js";
17
18
  import { getLogger } from "../util/logger.js";
@@ -43,6 +44,52 @@ export function buildLocalAuthContext(conversationId: string): AuthContext {
43
44
  };
44
45
  }
45
46
 
47
+ /**
48
+ * Look up the local vellum guardian's principalId from the contacts table.
49
+ *
50
+ * Returns `undefined` when no vellum guardian binding exists (e.g. fresh
51
+ * install before bootstrap). Callers should treat that case as
52
+ * "not yet available" and either fall back or proceed without a principalId.
53
+ */
54
+ export function findLocalGuardianPrincipalId(): string | undefined {
55
+ return findGuardianForChannel("vellum")?.contact.principalId ?? undefined;
56
+ }
57
+
58
+ /**
59
+ * Translate the synthetic dev-bypass actor principal to the real local
60
+ * guardian's principalId when running in `DISABLE_HTTP_AUTH=true` mode.
61
+ *
62
+ * The dev-bypass `AuthContext` (`runtime/auth/middleware.ts`) injects
63
+ * `"dev-bypass"` as the actor principal id for every request, but tool-side
64
+ * trust resolution (`resolveLocalTrustContext`) and SSE registration both
65
+ * carry the real local guardian principalId. Without this translation, every
66
+ * targeted host_bash/host_file/host_cu/host_transfer result POST mismatches
67
+ * the same-user check and is rejected with 403, and conversation/surface/
68
+ * guardian-action routes resolve trust against the wrong principal.
69
+ *
70
+ * Returns the input unchanged when:
71
+ * - HTTP auth is enabled (production / non-dev-bypass deployments), OR
72
+ * - the input is not literally `"dev-bypass"` (e.g. service tokens).
73
+ *
74
+ * Returns the local guardian principalId when both gates are true. Returns
75
+ * `undefined` when dev-bypass is set but no guardian binding has been created
76
+ * yet (e.g. fresh install before bootstrap); callers must treat this the
77
+ * same as a missing principal.
78
+ */
79
+ export function resolveActorPrincipalIdForLocalGuardian(
80
+ rawHeader: string | undefined,
81
+ ): string | undefined {
82
+ if (rawHeader !== "dev-bypass" || !isHttpAuthDisabled()) return rawHeader;
83
+
84
+ const guardianPrincipalId = findLocalGuardianPrincipalId();
85
+ if (guardianPrincipalId) return guardianPrincipalId;
86
+
87
+ log.warn(
88
+ "dev-bypass actor principal received but no vellum guardian binding found; returning undefined",
89
+ );
90
+ return undefined;
91
+ }
92
+
46
93
  /**
47
94
  * Resolve the guardian runtime context for a local connection.
48
95
  *
@@ -60,10 +107,8 @@ export function resolveLocalTrustContext(
60
107
  ): TrustContext {
61
108
  const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
62
109
 
63
- // Try contacts-first for the vellum guardian channel
64
- const guardianResult = findGuardianForChannel("vellum");
65
- if (guardianResult && guardianResult.contact.principalId) {
66
- const guardianPrincipalId = guardianResult.contact.principalId;
110
+ const guardianPrincipalId = findLocalGuardianPrincipalId();
111
+ if (guardianPrincipalId) {
67
112
  const trustCtx = resolveTrustContext({
68
113
  assistantId,
69
114
  sourceChannel: "vellum",
@@ -97,13 +142,9 @@ export function resolveLocalTrustContext(
97
142
  export function resolveLocalAuthContext(conversationId: string): AuthContext {
98
143
  const authContext = buildLocalAuthContext(conversationId);
99
144
 
100
- // Enrich with the guardian principal ID from contacts-first path
101
- const guardianResult = findGuardianForChannel("vellum");
102
- if (guardianResult && guardianResult.contact.principalId) {
103
- return {
104
- ...authContext,
105
- actorPrincipalId: guardianResult.contact.principalId,
106
- };
145
+ const guardianPrincipalId = findLocalGuardianPrincipalId();
146
+ if (guardianPrincipalId) {
147
+ return { ...authContext, actorPrincipalId: guardianPrincipalId };
107
148
  }
108
149
 
109
150
  log.warn(
@@ -3,22 +3,22 @@
3
3
  * confirmation, secret, host_bash, host_file, host_cu, host_browser, and
4
4
  * host_transfer interactions.
5
5
  *
6
- * For confirmation_request and secret_request, the onEvent callback in
7
- * assistant-event-hub registers the interaction here.
6
+ * All request types self-register with their full RPC lifecycle state
7
+ * (resolve/reject callbacks, timer, abort detach):
8
8
  *
9
- * For host proxy interactions (host_bash, host_file, host_cu, host_browser,
10
- * host_transfer), the proxy itself registers with full RPC lifecycle state
11
- * (resolve/reject callbacks, timer, abort detach). This eliminates the
12
- * per-proxy `private pending` maps — all pending state lives here.
9
+ * - Host proxies (host_bash, host_file, host_cu, host_browser,
10
+ * host_app_control, host_transfer): register in request(), using
11
+ * rpcResolve/rpcReject/timer/detachAbort/metadata.
12
+ *
13
+ * - Prompters (PermissionPrompter, SecretPrompter): register in prompt(),
14
+ * using promptResolve/promptReject/timer/toolUseId.
13
15
  *
14
16
  * Standalone HTTP endpoints (/v1/confirm, /v1/secret, /v1/trust-rules,
15
- * /v1/host-bash-result, /v1/host-file-result, /v1/host-cu-result,
16
- * /v1/host-browser-result) look up the conversation from this tracker to
17
+ * /v1/host-bash-result, etc.) look up the conversation from this tracker to
17
18
  * resolve the interaction.
18
19
  */
19
20
 
20
21
  import type { UserDecision } from "../permissions/types.js";
21
- import type { ToolExecutionResult } from "../tools/types.js";
22
22
 
23
23
  export interface ConfirmationDetails {
24
24
  toolName: string;
@@ -59,19 +59,29 @@ export interface PendingInteraction {
59
59
  directResolve?: (decision: UserDecision) => void;
60
60
  /** When set, the host_bash request should be routed to this specific client. */
61
61
  targetClientId?: string;
62
+ /**
63
+ * Snapshot of `targetClientId`'s `actorPrincipalId` taken at registration
64
+ * time. Persisted so the result-route same-actor check compares against
65
+ * a stable value rather than the live hub — the target client's SSE
66
+ * subscription may have briefly disconnected between dispatch and result
67
+ * submission, which would otherwise 403 a legitimate result.
68
+ */
69
+ targetActorPrincipalId?: string;
62
70
 
63
- // -- RPC lifecycle (populated by host proxies) --
71
+ // -- RPC lifecycle (all interaction types) --
64
72
 
65
- /** Resolve the caller's Promise with a tool execution result. */
66
- rpcResolve?: (result: ToolExecutionResult) => void;
73
+ /** Resolve the caller's Promise. Typed as unknown; callers cast at use sites. */
74
+ rpcResolve?: (value: unknown) => void;
67
75
  /** Reject the caller's Promise with an error. */
68
76
  rpcReject?: (err: Error) => void;
69
- /** Proxy-side timeout timer. Cleared on resolve/abort/dispose. */
77
+ /** Timeout timer. Cleared automatically on resolve(). */
70
78
  timer?: ReturnType<typeof setTimeout>;
71
79
  /** Detach the abort listener from the caller's signal. No-op when no signal was passed. */
72
80
  detachAbort?: () => void;
73
81
  /** Proxy-specific metadata (e.g. timeoutSec for bash, operation/path for file). */
74
82
  metadata?: Record<string, unknown>;
83
+ /** toolUseId associated with a confirmation_request (PermissionPrompter). */
84
+ toolUseId?: string;
75
85
  }
76
86
 
77
87
  const pending = new Map<string, PendingInteraction>();
@@ -136,7 +146,8 @@ export function getByConversation(
136
146
  * proxy timer would fire with a spurious timeout error.
137
147
  */
138
148
  export function removeByConversation(conversationId: string): void {
139
- for (const [requestId, interaction] of pending) {
149
+ // Snapshot keys to avoid mutation-during-iteration.
150
+ for (const [requestId, interaction] of [...pending]) {
140
151
  if (
141
152
  interaction.conversationId === conversationId &&
142
153
  interaction.kind !== "host_bash" &&
@@ -147,7 +158,8 @@ export function removeByConversation(conversationId: string): void {
147
158
  interaction.kind !== "host_transfer" &&
148
159
  interaction.kind !== "acp_confirmation"
149
160
  ) {
150
- pending.delete(requestId);
161
+ // resolve() clears the stored timer and detaches abort listeners.
162
+ resolve(requestId);
151
163
  }
152
164
  }
153
165
  }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Tests for the GET /v1/clients (list_clients) route.
3
+ *
4
+ * Validates the same-user filter applied to client listings:
5
+ * - Caller sees only clients owned by their `actorPrincipalId`.
6
+ * - Clients with no stored `actorPrincipalId` are filtered out (fail-closed).
7
+ * - Dev-bypass mode (`isHttpAuthDisabled()`) returns all clients.
8
+ */
9
+
10
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
11
+
12
+ // ── Module mocks (must be set up before importing the route) ──────────────
13
+
14
+ let fakeHttpAuthDisabled = false;
15
+
16
+ mock.module("../../../config/env.js", () => ({
17
+ isHttpAuthDisabled: () => fakeHttpAuthDisabled,
18
+ hasUngatedHttpAuthDisabled: () => false,
19
+ }));
20
+
21
+ mock.module("../../../util/logger.js", () => ({
22
+ getLogger: () =>
23
+ new Proxy({} as Record<string, unknown>, {
24
+ get: () => () => {},
25
+ }),
26
+ }));
27
+
28
+ // ── Real imports (after mocks) ────────────────────────────────────────────
29
+
30
+ import { assistantEventHub } from "../../assistant-event-hub.js";
31
+ import { ROUTES } from "../client-routes.js";
32
+ import type { RouteDefinition } from "../types.js";
33
+
34
+ afterAll(() => {
35
+ mock.restore();
36
+ });
37
+
38
+ // ── Test helpers ──────────────────────────────────────────────────────────
39
+
40
+ function findHandler(operationId: string): RouteDefinition["handler"] {
41
+ const route = ROUTES.find((r) => r.operationId === operationId);
42
+ if (!route) throw new Error(`Route ${operationId} not found`);
43
+ return route.handler;
44
+ }
45
+
46
+ type ListClientsResponse = {
47
+ clients: Array<{
48
+ clientId: string;
49
+ interfaceId: string;
50
+ capabilities: string[];
51
+ machineName?: string;
52
+ connectedAt: string;
53
+ lastActiveAt: string;
54
+ }>;
55
+ };
56
+
57
+ function registerClient(args: {
58
+ clientId: string;
59
+ actorPrincipalId?: string;
60
+ }): void {
61
+ assistantEventHub.subscribe({
62
+ type: "client",
63
+ clientId: args.clientId,
64
+ interfaceId: "macos",
65
+ capabilities: ["host_bash", "host_file", "host_cu"],
66
+ actorPrincipalId: args.actorPrincipalId,
67
+ callback: () => {},
68
+ });
69
+ }
70
+
71
+ function clearHub(): void {
72
+ const ids = assistantEventHub.listClients().map((c) => c.clientId);
73
+ for (const id of ids) {
74
+ assistantEventHub.disposeClient(id);
75
+ }
76
+ }
77
+
78
+ // ── Tests ────────────────────────────────────────────────────────────────
79
+
80
+ describe("list_clients route — same-user filter", () => {
81
+ beforeEach(() => {
82
+ fakeHttpAuthDisabled = false;
83
+ clearHub();
84
+ });
85
+
86
+ test("returns only clients owned by the calling actor", () => {
87
+ registerClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
88
+ registerClient({ clientId: "client-A2", actorPrincipalId: "user-A" });
89
+ registerClient({ clientId: "client-B1", actorPrincipalId: "user-B" });
90
+
91
+ const handler = findHandler("list_clients");
92
+ const result = handler({
93
+ headers: { "x-vellum-actor-principal-id": "user-A" },
94
+ }) as ListClientsResponse;
95
+
96
+ const ids = result.clients.map((c) => c.clientId).sort();
97
+ expect(ids).toEqual(["client-A1", "client-A2"]);
98
+ });
99
+
100
+ test("filters out cross-user clients when listing as a different user", () => {
101
+ registerClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
102
+ registerClient({ clientId: "client-B1", actorPrincipalId: "user-B" });
103
+
104
+ const handler = findHandler("list_clients");
105
+ const result = handler({
106
+ headers: { "x-vellum-actor-principal-id": "user-B" },
107
+ }) as ListClientsResponse;
108
+
109
+ const ids = result.clients.map((c) => c.clientId);
110
+ expect(ids).toEqual(["client-B1"]);
111
+ });
112
+
113
+ test("filters out clients with no stored actorPrincipalId (fail-closed)", () => {
114
+ registerClient({
115
+ clientId: "client-noprincipal",
116
+ actorPrincipalId: undefined,
117
+ });
118
+ registerClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
119
+
120
+ const handler = findHandler("list_clients");
121
+ const result = handler({
122
+ headers: { "x-vellum-actor-principal-id": "user-A" },
123
+ }) as ListClientsResponse;
124
+
125
+ const ids = result.clients.map((c) => c.clientId);
126
+ expect(ids).toEqual(["client-A1"]);
127
+ });
128
+
129
+ test("filters out all clients when caller has no actorPrincipalId header (fail-closed)", () => {
130
+ registerClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
131
+
132
+ const handler = findHandler("list_clients");
133
+ const result = handler({}) as ListClientsResponse;
134
+
135
+ expect(result.clients).toEqual([]);
136
+ });
137
+
138
+ test("dev-bypass mode returns all clients regardless of actor", () => {
139
+ fakeHttpAuthDisabled = true;
140
+ registerClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
141
+ registerClient({ clientId: "client-B1", actorPrincipalId: "user-B" });
142
+ registerClient({
143
+ clientId: "client-noprincipal",
144
+ actorPrincipalId: undefined,
145
+ });
146
+
147
+ const handler = findHandler("list_clients");
148
+ const result = handler({
149
+ headers: { "x-vellum-actor-principal-id": "user-A" },
150
+ }) as ListClientsResponse;
151
+
152
+ const ids = result.clients.map((c) => c.clientId).sort();
153
+ expect(ids).toEqual(["client-A1", "client-B1", "client-noprincipal"]);
154
+ });
155
+ });