@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,165 @@
1
+ /**
2
+ * IPC route for executing shell commands through the assistant process.
3
+ *
4
+ * The CLI sends the command over the IPC socket and receives a single
5
+ * response containing stdout, stderr, and the exit code.
6
+ *
7
+ * **Security**: Gated behind VELLUM_DEBUG=1. When debug mode is off (the
8
+ * default), the handler returns an error immediately so the CLI surfaces a
9
+ * clear rejection instead of hanging. The assistant must be restarted with
10
+ * VELLUM_DEBUG=1 for this route to execute commands.
11
+ */
12
+
13
+ import { spawn } from "node:child_process";
14
+
15
+ import { z } from "zod";
16
+
17
+ import { getIsContainerized } from "../../config/env-registry.js";
18
+ import { buildSanitizedEnv } from "../../tools/terminal/safe-env.js";
19
+ import { getWorkspaceDir } from "../../util/platform.js";
20
+ import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
21
+
22
+ const DEFAULT_TIMEOUT_MS = 30_000;
23
+
24
+ function isDebugMode(): boolean {
25
+ return (
26
+ process.env.VELLUM_DEBUG === "1" || process.env.VELLUM_DEBUG === "true"
27
+ );
28
+ }
29
+
30
+ interface DebugBashResult {
31
+ stdout: string;
32
+ stderr: string;
33
+ exitCode: number | null;
34
+ timedOut: boolean;
35
+ error?: string;
36
+ }
37
+
38
+ function handleDebugBash({ body }: RouteHandlerArgs): Promise<DebugBashResult> {
39
+ if (getIsContainerized()) {
40
+ return Promise.resolve({
41
+ stdout: "",
42
+ stderr: "",
43
+ exitCode: null,
44
+ timedOut: false,
45
+ error: "debug bash is not available in containerized environments",
46
+ });
47
+ }
48
+
49
+ if (!isDebugMode()) {
50
+ return Promise.resolve({
51
+ stdout: "",
52
+ stderr: "",
53
+ exitCode: null,
54
+ timedOut: false,
55
+ error:
56
+ "Bash debug execution is disabled. The running assistant process must have been started with VELLUM_DEBUG=1 (setting it on the CLI command alone is not enough). Restart the assistant with: vellum sleep && VELLUM_DEBUG=1 vellum wake",
57
+ });
58
+ }
59
+
60
+ const { command, timeoutMs } = body as {
61
+ command?: string;
62
+ timeoutMs?: number;
63
+ };
64
+
65
+ if (!command || typeof command !== "string") {
66
+ return Promise.resolve({
67
+ stdout: "",
68
+ stderr: "",
69
+ exitCode: null,
70
+ timedOut: false,
71
+ error: "command is required",
72
+ });
73
+ }
74
+
75
+ const effectiveTimeout =
76
+ typeof timeoutMs === "number" && timeoutMs > 0
77
+ ? timeoutMs
78
+ : DEFAULT_TIMEOUT_MS;
79
+
80
+ return new Promise<DebugBashResult>((resolve) => {
81
+ const stdoutChunks: Buffer[] = [];
82
+ const stderrChunks: Buffer[] = [];
83
+ let timedOut = false;
84
+ let settled = false;
85
+
86
+ const finish = (result: DebugBashResult) => {
87
+ if (settled) return;
88
+ settled = true;
89
+ resolve(result);
90
+ };
91
+
92
+ const child = spawn("bash", ["-c", command], {
93
+ cwd: getWorkspaceDir(),
94
+ stdio: ["ignore", "pipe", "pipe"],
95
+ detached: true,
96
+ env: buildSanitizedEnv(),
97
+ });
98
+
99
+ const timer = setTimeout(() => {
100
+ timedOut = true;
101
+ try {
102
+ process.kill(-child.pid!, "SIGKILL");
103
+ } catch {
104
+ // Process group may have already exited.
105
+ }
106
+ }, effectiveTimeout);
107
+
108
+ child.stdout.on("data", (data: Buffer) => {
109
+ stdoutChunks.push(data);
110
+ });
111
+
112
+ child.stderr.on("data", (data: Buffer) => {
113
+ stderrChunks.push(data);
114
+ });
115
+
116
+ child.on("close", (code) => {
117
+ clearTimeout(timer);
118
+ finish({
119
+ stdout: Buffer.concat(stdoutChunks).toString(),
120
+ stderr: Buffer.concat(stderrChunks).toString(),
121
+ exitCode: code,
122
+ timedOut,
123
+ });
124
+ });
125
+
126
+ child.on("error", (err) => {
127
+ clearTimeout(timer);
128
+ finish({
129
+ stdout: "",
130
+ stderr: "",
131
+ exitCode: null,
132
+ timedOut: false,
133
+ error: err.message,
134
+ });
135
+ });
136
+ });
137
+ }
138
+
139
+ export const ROUTES: RouteDefinition[] = [
140
+ {
141
+ operationId: "debug_bash",
142
+ endpoint: "debug/bash",
143
+ method: "POST",
144
+ requireGuardian: false,
145
+ summary: "Execute a shell command in the assistant process",
146
+ description:
147
+ "Developer debugging tool. Requires the assistant to be running with VELLUM_DEBUG=1.",
148
+ tags: ["debug"],
149
+ requestBody: z.object({
150
+ command: z.string().describe("Shell command to execute via bash -c"),
151
+ timeoutMs: z
152
+ .number()
153
+ .optional()
154
+ .describe("Execution timeout in milliseconds (default: 30000)"),
155
+ }),
156
+ responseBody: z.object({
157
+ stdout: z.string(),
158
+ stderr: z.string(),
159
+ exitCode: z.number().nullable(),
160
+ timedOut: z.boolean(),
161
+ error: z.string().optional(),
162
+ }),
163
+ handler: handleDebugBash,
164
+ },
165
+ ];
@@ -0,0 +1,121 @@
1
+ import { z } from "zod";
2
+
3
+ import {
4
+ acknowledgeDiskPressureLock,
5
+ DISK_PRESSURE_OVERRIDE_CONFIRMATION,
6
+ getDiskPressureStatus,
7
+ overrideDiskPressureLock,
8
+ } from "../../daemon/disk-pressure-guard.js";
9
+ import { RouteError } from "./errors.js";
10
+ import type { RouteDefinition } from "./types.js";
11
+
12
+ const DiskPressureStatusSchema = z.object({
13
+ enabled: z.boolean(),
14
+ state: z.enum(["disabled", "ok", "critical", "unknown"]),
15
+ locked: z.boolean(),
16
+ acknowledged: z.boolean(),
17
+ overrideActive: z.boolean(),
18
+ effectivelyLocked: z.boolean(),
19
+ lockId: z.string().nullable(),
20
+ usagePercent: z.number().nullable(),
21
+ thresholdPercent: z.number(),
22
+ path: z.string().nullable(),
23
+ lastCheckedAt: z.string().nullable(),
24
+ blockedCapabilities: z.array(
25
+ z.enum(["agent-turns", "background-work", "remote-ingress"]),
26
+ ),
27
+ error: z.string().nullable(),
28
+ });
29
+
30
+ const DiskPressureActionResponseSchema = z.object({
31
+ status: DiskPressureStatusSchema,
32
+ });
33
+
34
+ const OverrideRequestSchema = z.object({
35
+ confirmation: z.string(),
36
+ });
37
+
38
+ function statusResponse() {
39
+ return { status: getDiskPressureStatus() };
40
+ }
41
+
42
+ function transitionErrorCode(
43
+ reason: "not_locked" | "already_acknowledged" | "already_overridden",
44
+ ): string {
45
+ if (reason === "not_locked") return "NOT_LOCKED";
46
+ if (reason === "already_acknowledged") return "ALREADY_ACKNOWLEDGED";
47
+ return "ALREADY_OVERRIDDEN";
48
+ }
49
+
50
+ export const ROUTES: RouteDefinition[] = [
51
+ {
52
+ operationId: "getDiskPressureStatus",
53
+ endpoint: "disk-pressure/status",
54
+ method: "GET",
55
+ policyKey: "disk-pressure/status",
56
+ requirePolicyEnforcement: true,
57
+ summary: "Get disk pressure status",
58
+ description:
59
+ "Return the current disk pressure status snapshot. When safe storage limits are disabled, returns a disabled status.",
60
+ tags: ["disk-pressure"],
61
+ responseBody: DiskPressureActionResponseSchema,
62
+ handler: () => statusResponse(),
63
+ },
64
+ {
65
+ operationId: "acknowledgeDiskPressure",
66
+ endpoint: "disk-pressure/acknowledge",
67
+ method: "POST",
68
+ policyKey: "disk-pressure/acknowledge",
69
+ requirePolicyEnforcement: true,
70
+ summary: "Acknowledge disk pressure",
71
+ description:
72
+ "Acknowledge the current disk pressure lock and enter cleanup mode without overriding assistant protections.",
73
+ tags: ["disk-pressure"],
74
+ responseBody: DiskPressureActionResponseSchema,
75
+ additionalResponses: {
76
+ "409": { description: "No active lock or lock already acknowledged." },
77
+ },
78
+ handler: () => {
79
+ const result = acknowledgeDiskPressureLock();
80
+ if (result.ok) return { status: result.status };
81
+ if (result.reason === "invalid_confirmation") {
82
+ throw new RouteError(result.message, "INVALID_CONFIRMATION", 400);
83
+ }
84
+ throw new RouteError(
85
+ result.message,
86
+ transitionErrorCode(result.reason),
87
+ 409,
88
+ );
89
+ },
90
+ },
91
+ {
92
+ operationId: "overrideDiskPressure",
93
+ endpoint: "disk-pressure/override",
94
+ method: "POST",
95
+ policyKey: "disk-pressure/override",
96
+ requirePolicyEnforcement: true,
97
+ summary: "Override disk pressure",
98
+ description: `Override the current disk pressure lock only after confirming "${DISK_PRESSURE_OVERRIDE_CONFIRMATION}".`,
99
+ tags: ["disk-pressure"],
100
+ requestBody: OverrideRequestSchema,
101
+ responseBody: DiskPressureActionResponseSchema,
102
+ additionalResponses: {
103
+ "400": { description: "Confirmation phrase is invalid." },
104
+ "409": { description: "No active lock or lock already overridden." },
105
+ },
106
+ handler: ({ body }) => {
107
+ const parsed = OverrideRequestSchema.safeParse(body);
108
+ const confirmation = parsed.success ? parsed.data.confirmation : "";
109
+ const result = overrideDiskPressureLock(confirmation);
110
+ if (result.ok) return { status: result.status };
111
+ if (result.reason === "invalid_confirmation") {
112
+ throw new RouteError(result.message, "INVALID_CONFIRMATION", 400);
113
+ }
114
+ throw new RouteError(
115
+ result.message,
116
+ transitionErrorCode(result.reason),
117
+ 409,
118
+ );
119
+ },
120
+ },
121
+ ];
@@ -146,8 +146,12 @@ export async function renderMarkdownToPDF(
146
146
  const pw = await importPlaywright();
147
147
  const browser = await pw.chromium.launch({ headless: true });
148
148
  try {
149
- const page = await browser.newPage();
150
- await page.setContent(fullHtml, { waitUntil: "networkidle" });
149
+ const context = await browser.newContext({
150
+ javaScriptEnabled: false,
151
+ });
152
+ const page = await context.newPage();
153
+ await page.route("**/*", (route) => route.abort());
154
+ await page.setContent(fullHtml, { waitUntil: "domcontentloaded" });
151
155
  const pdfBuffer = await page.pdf({
152
156
  format: "A4",
153
157
  margin: {
@@ -6,7 +6,8 @@
6
6
  */
7
7
  import { z } from "zod";
8
8
 
9
- import { rawAll, rawGet, rawRun } from "../../memory/raw-query.js";
9
+ import { saveDocument } from "../../documents/document-store.js";
10
+ import { rawAll, rawGet } from "../../memory/raw-query.js";
10
11
  import { getLogger } from "../../util/logger.js";
11
12
  import { renderMarkdownToPDF } from "./document-pdf-renderer.js";
12
13
  import { BadRequestError, InternalError, NotFoundError } from "./errors.js";
@@ -27,80 +28,6 @@ interface DocumentRow {
27
28
 
28
29
  type DocumentListRow = Omit<DocumentRow, "content">;
29
30
 
30
- // ---------------------------------------------------------------------------
31
- // Junction table helper
32
- // ---------------------------------------------------------------------------
33
-
34
- /** Insert a document–conversation association (idempotent via INSERT OR IGNORE). */
35
- function addDocumentConversation(
36
- surfaceId: string,
37
- conversationId: string,
38
- ): void {
39
- rawRun(
40
- /*sql*/ `INSERT OR IGNORE INTO document_conversations (surface_id, conversation_id, created_at) VALUES (?, ?, ?)`,
41
- surfaceId,
42
- conversationId,
43
- Date.now(),
44
- );
45
- }
46
-
47
- // ---------------------------------------------------------------------------
48
- // Shared business logic (used by both message handlers and HTTP routes)
49
- // ---------------------------------------------------------------------------
50
-
51
- function saveDocument(params: {
52
- surfaceId: string;
53
- conversationId: string;
54
- title: string;
55
- content: string;
56
- wordCount: number;
57
- }): { success: true; surfaceId: string } | { success: false; error: string } {
58
- try {
59
- const now = Date.now();
60
- rawRun(
61
- `INSERT INTO documents (surface_id, conversation_id, title, content, word_count, created_at, updated_at)
62
- VALUES (?, ?, ?, ?, ?, ?, ?)
63
- ON CONFLICT(surface_id) DO UPDATE SET
64
- title = excluded.title,
65
- content = excluded.content,
66
- word_count = excluded.word_count,
67
- updated_at = excluded.updated_at`,
68
- params.surfaceId,
69
- params.conversationId,
70
- params.title,
71
- params.content,
72
- params.wordCount,
73
- now,
74
- now,
75
- );
76
- log.info(
77
- { surfaceId: params.surfaceId, title: params.title },
78
- "Saved document",
79
- );
80
-
81
- // Best-effort: associate the document with the conversation.
82
- // Failures (e.g. migration not yet applied, table missing) must not
83
- // cause the save response to report failure — the document itself is
84
- // already persisted at this point.
85
- try {
86
- addDocumentConversation(params.surfaceId, params.conversationId);
87
- } catch (err) {
88
- log.warn(
89
- { err, surfaceId: params.surfaceId },
90
- "Failed to record document–conversation association",
91
- );
92
- }
93
-
94
- return { success: true, surfaceId: params.surfaceId };
95
- } catch (error) {
96
- log.error({ err: error, surfaceId: params.surfaceId }, "Save error");
97
- return {
98
- success: false,
99
- error: error instanceof Error ? error.message : "Unknown error",
100
- };
101
- }
102
- }
103
-
104
31
  function loadDocument(surfaceId: string):
105
32
  | {
106
33
  success: true;
@@ -18,14 +18,14 @@
18
18
  * handles registration, touch (heartbeat), and unregistration (dispose).
19
19
  */
20
20
 
21
+ import { z } from "zod";
22
+
21
23
  import type { HostProxyCapability } from "../../channels/types.js";
22
24
  import { parseInterfaceId, supportsHostProxy } from "../../channels/types.js";
25
+ import { emitContactChange } from "../../contacts/contact-events.js";
23
26
  import { getOrCreateConversation } from "../../memory/conversation-key-store.js";
24
27
  import { getLogger } from "../../util/logger.js";
25
- import {
26
- formatSseFrame,
27
- formatSseHeartbeatWithData,
28
- } from "../assistant-event.js";
28
+ import { formatSseFrame, formatSseHeartbeat } from "../assistant-event.js";
29
29
  import type {
30
30
  AssistantEventCallback,
31
31
  AssistantEventFilter,
@@ -35,13 +35,14 @@ import {
35
35
  AssistantEventHub,
36
36
  assistantEventHub,
37
37
  } from "../assistant-event-hub.js";
38
+ import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
38
39
  import { BadRequestError, ServiceUnavailableError } from "./errors.js";
39
40
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
40
41
 
41
42
  const log = getLogger("events-routes");
42
43
 
43
- /** Keep-alive comment sent to idle clients every 5 s by default. */
44
- const DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;
44
+ /** Keep-alive comment sent to idle clients every 7 s by default. */
45
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 7_000;
45
46
 
46
47
  /**
47
48
  * Stream assistant events as Server-Sent Events.
@@ -61,7 +62,7 @@ const DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;
61
62
  *
62
63
  * Options (for testing):
63
64
  * hub -- override the event hub (defaults to process singleton).
64
- * heartbeatIntervalMs -- how often to emit keep-alive comments (default 5 s).
65
+ * heartbeatIntervalMs -- how often to emit keep-alive comments (default 7 s).
65
66
  */
66
67
  export function handleSubscribeAssistantEvents(
67
68
  args: RouteHandlerArgs,
@@ -81,10 +82,18 @@ export function handleSubscribeAssistantEvents(
81
82
  const rawClientId = headers?.["x-vellum-client-id"];
82
83
  const rawInterfaceId = headers?.["x-vellum-interface-id"];
83
84
  const rawMachineName = headers?.["x-vellum-machine-name"];
85
+ const rawActorPrincipalId = headers?.["x-vellum-actor-principal-id"];
84
86
  const clientId = rawClientId?.trim() || null;
85
87
  const interfaceId = clientId
86
88
  ? parseInterfaceId(rawInterfaceId?.trim())
87
89
  : null;
90
+ // Verified by RuntimeHttpServer and forwarded by the http-adapter from the
91
+ // bearer token's AuthContext. May be absent for legacy / service-token
92
+ // connections that have no principal. See `resolveActorPrincipalId` for the
93
+ // dev-bypass translation rationale.
94
+ const actorPrincipalId = resolveActorPrincipalIdForLocalGuardian(
95
+ rawActorPrincipalId?.trim() || undefined,
96
+ );
88
97
 
89
98
  if (clientId && !interfaceId) {
90
99
  log.error(
@@ -171,6 +180,7 @@ export function handleSubscribeAssistantEvents(
171
180
  supportsHostProxy(interfaceId, cap),
172
181
  ),
173
182
  machineName: rawMachineName?.trim() || undefined,
183
+ actorPrincipalId,
174
184
  })
175
185
  : hub.subscribe({
176
186
  ...subscriberBase,
@@ -194,7 +204,7 @@ export function handleSubscribeAssistantEvents(
194
204
  return;
195
205
  }
196
206
 
197
- controller.enqueue(encoder.encode(formatSseHeartbeatWithData()));
207
+ controller.enqueue(encoder.encode(formatSseHeartbeat()));
198
208
 
199
209
  heartbeatTimer = setInterval(() => {
200
210
  try {
@@ -206,7 +216,7 @@ export function handleSubscribeAssistantEvents(
206
216
  if (clientId) {
207
217
  hub.touchClient(clientId);
208
218
  }
209
- controller.enqueue(encoder.encode(formatSseHeartbeatWithData()));
219
+ controller.enqueue(encoder.encode(formatSseHeartbeat()));
210
220
  } catch {
211
221
  sub.dispose();
212
222
  cleanup();
@@ -237,7 +247,29 @@ export function handleSubscribeAssistantEvents(
237
247
  // Route definitions
238
248
  // ---------------------------------------------------------------------------
239
249
 
250
+ const EmitEventBodySchema = z.object({
251
+ kind: z.enum(["contacts_changed"]),
252
+ });
253
+
240
254
  export const ROUTES: RouteDefinition[] = [
255
+ {
256
+ operationId: "emit_event",
257
+ endpoint: "events/emit",
258
+ method: "POST",
259
+ summary: "Emit an assistant event",
260
+ description:
261
+ "Trigger an in-process assistant event by kind. Used by the gateway after owning a write that the assistant runtime would normally emit.",
262
+ tags: ["events"],
263
+ requestBody: EmitEventBodySchema,
264
+ responseStatus: "204",
265
+ handler: ({ body }) => {
266
+ const { kind } = EmitEventBodySchema.parse(body);
267
+ if (kind === "contacts_changed") {
268
+ emitContactChange();
269
+ }
270
+ return null;
271
+ },
272
+ },
241
273
  {
242
274
  operationId: "subscribe_assistant_events",
243
275
  endpoint: "events",
@@ -2,14 +2,13 @@
2
2
  * Route handlers for filing management.
3
3
  *
4
4
  * `available` reflects whether the filing service is the active background
5
- * memory job for this instance. When the `memory-v2-enabled` flag is on,
5
+ * memory job for this instance. When `config.memory.v2.enabled` is true,
6
6
  * filing yields to the consolidation job (see consolidation-routes.ts) and
7
7
  * returns `available: false` so the UI can hide the row.
8
8
  */
9
9
 
10
10
  import { z } from "zod";
11
11
 
12
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
13
12
  import { getConfig } from "../../config/loader.js";
14
13
  import { FilingService } from "../../filing/filing-service.js";
15
14
  import { getLogger } from "../../util/logger.js";
@@ -19,7 +18,7 @@ import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
19
18
  const log = getLogger("filing-routes");
20
19
 
21
20
  function isFilingAvailable(): boolean {
22
- return !isAssistantFeatureFlagEnabled("memory-v2-enabled", getConfig());
21
+ return !getConfig().memory.v2.enabled;
23
22
  }
24
23
 
25
24
  // ---------------------------------------------------------------------------
@@ -7,6 +7,11 @@
7
7
  import { z } from "zod";
8
8
 
9
9
  import { HostBashProxy } from "../../daemon/host-bash-proxy.js";
10
+ import {
11
+ enforceSameActorOrThrow,
12
+ SAME_ACTOR_FORBIDDEN_DESCRIPTION,
13
+ } from "../auth/same-actor.js";
14
+ import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
10
15
  import * as pendingInteractions from "../pending-interactions.js";
11
16
  import {
12
17
  BadRequestError,
@@ -37,7 +42,11 @@ function handleHostBashResult({ body, headers }: RouteHandlerArgs) {
37
42
  throw new BadRequestError("requestId is required");
38
43
  }
39
44
 
40
- const submittingClientId = headers?.["x-vellum-client-id"]?.trim() || undefined;
45
+ const submittingClientId =
46
+ headers?.["x-vellum-client-id"]?.trim() || undefined;
47
+ const submittingActorPrincipalId = resolveActorPrincipalIdForLocalGuardian(
48
+ headers?.["x-vellum-actor-principal-id"]?.trim() || undefined,
49
+ );
41
50
 
42
51
  const peeked = pendingInteractions.get(requestId);
43
52
  if (!peeked) {
@@ -62,6 +71,18 @@ function handleHostBashResult({ body, headers }: RouteHandlerArgs) {
62
71
  `Client "${submittingClientId}" is not the target for this request (expected "${targetClientId}"). The targeted client must submit the result.`,
63
72
  );
64
73
  }
74
+
75
+ // Defense-in-depth on top of the client-id header binding above: the
76
+ // submitting actor's principal must match the actor principal stored
77
+ // for the target client at SSE subscription time. This prevents a
78
+ // cross-user submission even when the attacker can guess or spoof the
79
+ // target's client ID.
80
+ enforceSameActorOrThrow({
81
+ sourceActorPrincipalId: submittingActorPrincipalId,
82
+ targetActorPrincipalId: peeked.targetActorPrincipalId,
83
+ targetClientId,
84
+ op: "host_bash",
85
+ });
65
86
  }
66
87
 
67
88
  HostBashProxy.instance.resolveResult(requestId, {
@@ -103,8 +124,7 @@ export const ROUTES: RouteDefinition[] = [
103
124
  "x-vellum-client-id header is missing for a targeted host bash request.",
104
125
  },
105
126
  "403": {
106
- description:
107
- "Submitting client does not match the targeted client for this request.",
127
+ description: SAME_ACTOR_FORBIDDEN_DESCRIPTION,
108
128
  },
109
129
  "404": {
110
130
  description: "No pending interaction found for the given requestId.",
@@ -7,8 +7,18 @@
7
7
  import { z } from "zod";
8
8
 
9
9
  import { findConversation } from "../../daemon/conversation-store.js";
10
+ import {
11
+ enforceSameActorOrThrow,
12
+ SAME_ACTOR_FORBIDDEN_DESCRIPTION,
13
+ } from "../auth/same-actor.js";
14
+ import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
10
15
  import * as pendingInteractions from "../pending-interactions.js";
11
- import { BadRequestError, ConflictError, ForbiddenError, NotFoundError } from "./errors.js";
16
+ import {
17
+ BadRequestError,
18
+ ConflictError,
19
+ ForbiddenError,
20
+ NotFoundError,
21
+ } from "./errors.js";
12
22
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
13
23
 
14
24
  // ---------------------------------------------------------------------------
@@ -65,16 +75,34 @@ function handleHostCuResult({ body, headers }: RouteHandlerArgs) {
65
75
 
66
76
  // Validate submitting client matches the targeted client (if any).
67
77
  if (peeked.targetClientId != null) {
68
- const rawClientId = (headers as Record<string, string | undefined>)?.["x-vellum-client-id"];
69
- const submittingClientId = rawClientId?.trim() || undefined;
78
+ const headerMap = (headers as Record<string, string | undefined>) ?? {};
79
+ const submittingClientId =
80
+ headerMap["x-vellum-client-id"]?.trim() || undefined;
70
81
  if (!submittingClientId) {
71
- throw new BadRequestError("x-vellum-client-id header is missing for a targeted host CU request.");
82
+ throw new BadRequestError(
83
+ "x-vellum-client-id header is missing for a targeted host CU request.",
84
+ );
72
85
  }
73
86
  if (submittingClientId !== peeked.targetClientId) {
74
87
  throw new ForbiddenError(
75
88
  `Client "${submittingClientId}" is not the target for this request (expected "${peeked.targetClientId}"). The targeted client must submit the result.`,
76
89
  );
77
90
  }
91
+
92
+ // Defense-in-depth: require the submitting actor's principal id to match
93
+ // the actor principal id captured when the target client opened its SSE
94
+ // stream. This prevents a different authenticated user with knowledge of
95
+ // both the requestId and target clientId from submitting a result on
96
+ // behalf of the targeted client.
97
+ const submittingActorPrincipalId = resolveActorPrincipalIdForLocalGuardian(
98
+ headerMap["x-vellum-actor-principal-id"]?.trim() || undefined,
99
+ );
100
+ enforceSameActorOrThrow({
101
+ sourceActorPrincipalId: submittingActorPrincipalId,
102
+ targetActorPrincipalId: peeked.targetActorPrincipalId,
103
+ targetClientId: peeked.targetClientId,
104
+ op: "host_cu",
105
+ });
78
106
  }
79
107
 
80
108
  const conversation = findConversation(peeked.conversationId);
@@ -141,8 +169,7 @@ export const ROUTES: RouteDefinition[] = [
141
169
  "x-vellum-client-id header is missing for a targeted host CU request.",
142
170
  },
143
171
  "403": {
144
- description:
145
- "Submitting client does not match the targeted client for this request.",
172
+ description: SAME_ACTOR_FORBIDDEN_DESCRIPTION,
146
173
  },
147
174
  "404": {
148
175
  description: