@vellumai/assistant 0.7.2 → 0.7.3

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 (347) hide show
  1. package/ARCHITECTURE.md +16 -1
  2. package/docs/architecture/memory.md +5 -2
  3. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  4. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  5. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  6. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  7. package/openapi.yaml +449 -22
  8. package/package.json +1 -1
  9. package/src/__tests__/app-control-flow.test.ts +21 -11
  10. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  11. package/src/__tests__/assistant-event.test.ts +0 -10
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  14. package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
  15. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  16. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  18. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  19. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  20. package/src/__tests__/config-schema.test.ts +1 -0
  21. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  22. package/src/__tests__/config-watcher.test.ts +140 -69
  23. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  24. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  25. package/src/__tests__/context-search-fanout.test.ts +0 -1
  26. package/src/__tests__/context-search-memory-source.test.ts +3 -7
  27. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  28. package/src/__tests__/context-search-pkb-source.test.ts +0 -1
  29. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  30. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
  31. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  32. package/src/__tests__/conversation-agent-loop.test.ts +454 -5
  33. package/src/__tests__/conversation-error.test.ts +150 -3
  34. package/src/__tests__/conversation-process-callsite.test.ts +43 -0
  35. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  36. package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
  37. package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
  38. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  39. package/src/__tests__/conversation-store.test.ts +0 -18
  40. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  41. package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
  42. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
  43. package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
  44. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
  45. package/src/__tests__/credentials-cli.test.ts +7 -0
  46. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  47. package/src/__tests__/date-context.test.ts +164 -2
  48. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  49. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  50. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  51. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  52. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  53. package/src/__tests__/disk-usage.test.ts +150 -0
  54. package/src/__tests__/events-client-registration.test.ts +52 -0
  55. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  56. package/src/__tests__/file-write-tool.test.ts +4 -10
  57. package/src/__tests__/filing-service.test.ts +3 -4
  58. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  59. package/src/__tests__/heartbeat-service.test.ts +260 -11
  60. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  61. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  62. package/src/__tests__/host-bash-routes.test.ts +178 -13
  63. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  64. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  65. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  66. package/src/__tests__/host-file-proxy.test.ts +268 -6
  67. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  68. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  69. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  70. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  71. package/src/__tests__/injector-chain.test.ts +18 -6
  72. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  73. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  74. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  75. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  77. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  78. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  79. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  80. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  81. package/src/__tests__/openai-provider.test.ts +45 -8
  82. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  83. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  84. package/src/__tests__/platform.test.ts +2 -1
  85. package/src/__tests__/playbook-execution.test.ts +0 -43
  86. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  87. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  88. package/src/__tests__/provider-tool-name.test.ts +23 -0
  89. package/src/__tests__/relay-server.test.ts +15 -4
  90. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  91. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  92. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  93. package/src/__tests__/suggestion-routes.test.ts +46 -0
  94. package/src/__tests__/twilio-validation.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  96. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  97. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  98. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
  99. package/src/approvals/guardian-decision-primitive.ts +13 -0
  100. package/src/approvals/guardian-request-resolvers.ts +16 -17
  101. package/src/backup/snapshot-lock.ts +2 -27
  102. package/src/bundler/compiler-tools.ts +3 -2
  103. package/src/calls/call-conversation-messages.ts +46 -10
  104. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  105. package/src/cli/commands/bash.ts +35 -108
  106. package/src/cli/commands/contacts.ts +64 -25
  107. package/src/cli/commands/credentials.ts +56 -0
  108. package/src/cli/commands/memory-v2.ts +7 -6
  109. package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
  110. package/src/cli/commands/oauth/connect.ts +127 -1
  111. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  112. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  113. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  114. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  115. package/src/cli/commands/platform/index.ts +16 -7
  116. package/src/cli/commands/status.ts +57 -0
  117. package/src/cli/program.ts +4 -2
  118. package/src/config/assistant-feature-flags.ts +13 -3
  119. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  120. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  121. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  122. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  123. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  124. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  125. package/src/config/env.ts +0 -8
  126. package/src/config/feature-flag-registry.json +27 -3
  127. package/src/config/loader.ts +127 -8
  128. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  129. package/src/config/schemas/call-site-catalog.ts +14 -0
  130. package/src/config/schemas/channels.ts +0 -5
  131. package/src/config/schemas/heartbeat.ts +1 -1
  132. package/src/config/schemas/llm.ts +2 -0
  133. package/src/config/schemas/memory-lifecycle.ts +13 -0
  134. package/src/config/schemas/memory-v2.ts +75 -11
  135. package/src/config/schemas/platform.ts +43 -3
  136. package/src/config/schemas/services.ts +28 -0
  137. package/src/config/seed-inference-profiles.ts +230 -33
  138. package/src/contacts/contact-store.ts +0 -25
  139. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  140. package/src/daemon/assistant-attachments.ts +4 -4
  141. package/src/daemon/config-watcher.ts +85 -57
  142. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  143. package/src/daemon/conversation-agent-loop.ts +170 -33
  144. package/src/daemon/conversation-error.ts +87 -15
  145. package/src/daemon/conversation-lifecycle.ts +1 -3
  146. package/src/daemon/conversation-process.ts +8 -0
  147. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  148. package/src/daemon/conversation-store.ts +2 -2
  149. package/src/daemon/conversation-surfaces.ts +195 -15
  150. package/src/daemon/conversation-tool-setup.ts +57 -14
  151. package/src/daemon/conversation.ts +17 -22
  152. package/src/daemon/date-context.ts +71 -22
  153. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  154. package/src/daemon/disk-pressure-guard.ts +343 -0
  155. package/src/daemon/disk-pressure-policy.ts +163 -0
  156. package/src/daemon/handlers/shared.ts +0 -1
  157. package/src/daemon/handlers/skills.ts +3 -4
  158. package/src/daemon/host-app-control-proxy.ts +137 -41
  159. package/src/daemon/host-bash-proxy.ts +46 -21
  160. package/src/daemon/host-cu-proxy.ts +49 -3
  161. package/src/daemon/host-file-proxy.ts +43 -7
  162. package/src/daemon/host-transfer-proxy.ts +95 -4
  163. package/src/daemon/lifecycle.ts +79 -28
  164. package/src/daemon/meet-host-supervisor.ts +4 -4
  165. package/src/daemon/meet-manifest-loader.ts +0 -1
  166. package/src/daemon/memory-v2-startup.ts +14 -4
  167. package/src/daemon/message-protocol.ts +3 -0
  168. package/src/daemon/message-types/conversations.ts +4 -0
  169. package/src/daemon/message-types/disk-pressure.ts +9 -0
  170. package/src/daemon/message-types/messages.ts +3 -0
  171. package/src/daemon/profiler-run-store.ts +5 -5
  172. package/src/daemon/tool-setup-types.ts +2 -2
  173. package/src/documents/document-store.ts +85 -0
  174. package/src/filing/filing-service.ts +30 -5
  175. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  176. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  177. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  178. package/src/heartbeat/heartbeat-service.ts +205 -31
  179. package/src/home/feed-scheduler.ts +18 -0
  180. package/src/inbound/platform-callback-registration.ts +8 -15
  181. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  182. package/src/ipc/assistant-server.ts +56 -2
  183. package/src/ipc/gateway-client.ts +37 -3
  184. package/src/live-voice/live-voice-archive.ts +4 -4
  185. package/src/live-voice/protocol.ts +5 -7
  186. package/src/media/image-service.ts +1 -7
  187. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  188. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
  189. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  190. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  191. package/src/memory/admin.ts +5 -9
  192. package/src/memory/context-search/agent-runner.ts +19 -2
  193. package/src/memory/context-search/sources/conversations.ts +2 -11
  194. package/src/memory/context-search/sources/memory-v2.ts +5 -4
  195. package/src/memory/context-search/sources/memory.ts +0 -1
  196. package/src/memory/context-search/types.ts +0 -1
  197. package/src/memory/conversation-crud.ts +4 -12
  198. package/src/memory/db-init.ts +2 -0
  199. package/src/memory/embedding-runtime-manager.ts +119 -5
  200. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
  201. package/src/memory/graph/conversation-graph-memory.ts +42 -54
  202. package/src/memory/graph/extraction.ts +1 -3
  203. package/src/memory/graph/graph-search.test.ts +10 -67
  204. package/src/memory/graph/graph-search.ts +1 -20
  205. package/src/memory/graph/retriever.test.ts +6 -0
  206. package/src/memory/graph/retriever.ts +6 -10
  207. package/src/memory/indexer.ts +54 -45
  208. package/src/memory/job-handlers/backfill.ts +2 -11
  209. package/src/memory/job-handlers/cleanup.ts +43 -0
  210. package/src/memory/job-handlers/embedding.ts +6 -8
  211. package/src/memory/job-handlers/summarization.ts +2 -7
  212. package/src/memory/jobs-store.ts +48 -0
  213. package/src/memory/jobs-worker.ts +81 -43
  214. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  215. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  216. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  217. package/src/memory/migrations/index.ts +1 -0
  218. package/src/memory/pkb/pkb-search.test.ts +6 -0
  219. package/src/memory/qdrant-client.ts +0 -13
  220. package/src/memory/rerank-local.ts +374 -0
  221. package/src/memory/search/semantic.ts +6 -67
  222. package/src/memory/trace-event-store.ts +1 -17
  223. package/src/memory/v2/__tests__/activation.test.ts +311 -250
  224. package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
  225. package/src/memory/v2/__tests__/injection.test.ts +157 -167
  226. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  227. package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
  228. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  229. package/src/memory/v2/__tests__/sim.test.ts +5 -199
  230. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  231. package/src/memory/v2/__tests__/static-context.test.ts +76 -1
  232. package/src/memory/v2/activation.ts +149 -156
  233. package/src/memory/v2/consolidation-job.ts +62 -12
  234. package/src/memory/v2/injection.ts +47 -60
  235. package/src/memory/v2/prompts/consolidation.ts +36 -1
  236. package/src/memory/v2/qdrant.ts +99 -0
  237. package/src/memory/v2/reranker.ts +177 -0
  238. package/src/memory/v2/sim.ts +10 -84
  239. package/src/memory/v2/skill-content.ts +4 -3
  240. package/src/memory/v2/skill-store.ts +82 -59
  241. package/src/memory/v2/static-context.ts +22 -0
  242. package/src/memory/v2/types.ts +10 -10
  243. package/src/notifications/copy-composer.ts +13 -0
  244. package/src/notifications/signal.ts +4 -0
  245. package/src/oauth/AGENTS.md +3 -1
  246. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  247. package/src/oauth/connect-orchestrator.ts +2 -0
  248. package/src/oauth/connection-resolver.test.ts +66 -1
  249. package/src/oauth/connection-resolver.ts +55 -1
  250. package/src/oauth/oauth-connect-state.ts +77 -0
  251. package/src/oauth/seed-providers.ts +58 -1
  252. package/src/plugins/defaults/injectors.ts +35 -2
  253. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  254. package/src/plugins/types.ts +7 -0
  255. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  256. package/src/proactive-artifact/decision.test.ts +226 -0
  257. package/src/proactive-artifact/decision.ts +165 -0
  258. package/src/proactive-artifact/index.ts +7 -0
  259. package/src/proactive-artifact/job.test.ts +867 -0
  260. package/src/proactive-artifact/job.ts +352 -0
  261. package/src/proactive-artifact/message-copy.ts +41 -0
  262. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  263. package/src/proactive-artifact/trigger-state.ts +119 -0
  264. package/src/prompts/normalize-onboarding.ts +80 -0
  265. package/src/prompts/persona-resolver.ts +101 -9
  266. package/src/prompts/system-prompt.ts +21 -7
  267. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  268. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  269. package/src/providers/model-intents.ts +7 -0
  270. package/src/providers/openrouter/client.ts +8 -0
  271. package/src/providers/retry.ts +50 -0
  272. package/src/providers/types.ts +1 -0
  273. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  274. package/src/runtime/agent-wake.ts +238 -100
  275. package/src/runtime/assistant-event-hub.ts +36 -6
  276. package/src/runtime/assistant-event.ts +0 -1
  277. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  278. package/src/runtime/auth/route-policy.ts +14 -1
  279. package/src/runtime/auth/same-actor.ts +216 -0
  280. package/src/runtime/channel-retry-sweep.ts +65 -1
  281. package/src/runtime/guardian-reply-router.ts +10 -0
  282. package/src/runtime/local-actor-identity.ts +52 -11
  283. package/src/runtime/pending-interactions.ts +8 -0
  284. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  285. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  286. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  287. package/src/runtime/routes/client-routes.ts +20 -2
  288. package/src/runtime/routes/contact-routes.ts +0 -25
  289. package/src/runtime/routes/conversation-routes.ts +35 -26
  290. package/src/runtime/routes/debug-bash-routes.ts +163 -0
  291. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  292. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  293. package/src/runtime/routes/documents-routes.ts +2 -75
  294. package/src/runtime/routes/events-routes.ts +41 -9
  295. package/src/runtime/routes/host-bash-routes.ts +23 -3
  296. package/src/runtime/routes/host-cu-routes.ts +33 -6
  297. package/src/runtime/routes/host-file-routes.ts +32 -6
  298. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  299. package/src/runtime/routes/identity-routes.ts +7 -138
  300. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  301. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
  302. package/src/runtime/routes/index.ts +6 -0
  303. package/src/runtime/routes/memory-item-routes.test.ts +41 -15
  304. package/src/runtime/routes/memory-v2-routes.ts +33 -0
  305. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  306. package/src/runtime/verification-outbound-actions.ts +4 -4
  307. package/src/schedule/run-script.ts +37 -5
  308. package/src/schedule/scheduler.ts +20 -1
  309. package/src/security/encrypted-store.ts +2 -0
  310. package/src/security/secure-keys.ts +55 -0
  311. package/src/skills/remote-skill-policy.ts +4 -10
  312. package/src/subagent/index.ts +1 -7
  313. package/src/subagent/manager.ts +1 -15
  314. package/src/tasks/task-runner.ts +0 -1
  315. package/src/tasks/task-store.ts +0 -3
  316. package/src/tools/background-tool-registry.ts +17 -3
  317. package/src/tools/host-filesystem/edit.test.ts +151 -0
  318. package/src/tools/host-filesystem/edit.ts +43 -1
  319. package/src/tools/host-filesystem/read.test.ts +129 -0
  320. package/src/tools/host-filesystem/read.ts +43 -1
  321. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  322. package/src/tools/host-filesystem/transfer.ts +56 -11
  323. package/src/tools/host-filesystem/write.test.ts +134 -0
  324. package/src/tools/host-filesystem/write.ts +43 -1
  325. package/src/tools/host-terminal/host-shell.ts +13 -6
  326. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  327. package/src/tools/memory/register.test.ts +12 -9
  328. package/src/tools/memory/register.ts +1 -2
  329. package/src/tools/provider-tool-name.ts +28 -0
  330. package/src/tools/registry.ts +30 -9
  331. package/src/tools/terminal/shell.ts +9 -1
  332. package/src/tools/tool-approval-handler.ts +31 -6
  333. package/src/tools/types.ts +24 -2
  334. package/src/tts/provider-catalog.ts +3 -5
  335. package/src/util/disk-usage.ts +138 -0
  336. package/src/util/platform.ts +21 -11
  337. package/src/util/process-liveness.ts +26 -0
  338. package/src/workspace/heartbeat-service.ts +19 -0
  339. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  340. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  341. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
  342. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  343. package/src/workspace/migrations/registry.ts +8 -0
  344. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  345. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  346. package/src/memory/v2/skill-qdrant.ts +0 -404
  347. 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
+ }
@@ -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(
@@ -99,6 +99,13 @@ export interface GuardianReplyResult {
99
99
  requestId?: string;
100
100
  /** Detailed result from the canonical decision primitive (when a decision was attempted). */
101
101
  canonicalResult?: CanonicalDecisionResult;
102
+ /** When a voice access request was approved, the contact that should be activated. */
103
+ activatedContact?: {
104
+ sourceChannel: string;
105
+ externalUserId: string;
106
+ externalChatId?: string;
107
+ displayName?: string;
108
+ };
102
109
  /**
103
110
  * When true, the caller should skip legacy approval interception for this
104
111
  * message. Set by the invite handoff bypass so that "open invite flow"
@@ -686,6 +693,9 @@ async function applyDecision(
686
693
  ...(canonicalResult.resolverReplyText
687
694
  ? { replyText: canonicalResult.resolverReplyText }
688
695
  : {}),
696
+ ...(canonicalResult.activatedContact
697
+ ? { activatedContact: canonicalResult.activatedContact }
698
+ : {}),
689
699
  requestId,
690
700
  canonicalResult,
691
701
  };
@@ -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(
@@ -59,6 +59,14 @@ 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
71
  // -- RPC lifecycle (populated by host proxies) --
64
72
 
@@ -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
+ });
@@ -10,7 +10,6 @@ mock.module("../../../util/logger.js", () => ({
10
10
  import {
11
11
  sampleConcepts as sharedSampleConcepts,
12
12
  sampleConfig,
13
- sampleSkills,
14
13
  } from "../../../memory/__tests__/fixtures/memory-v2-activation-fixtures.js";
15
14
 
16
15
  let rawConfigFixture: Record<string, unknown> = {};
@@ -36,7 +35,6 @@ import {
36
35
  backfillMemoryV2ActivationMessageId,
37
36
  type MemoryV2ConceptRowRecord,
38
37
  type MemoryV2ConfigSnapshot,
39
- type MemoryV2SkillRowRecord,
40
38
  recordMemoryV2ActivationLog,
41
39
  } from "../../../memory/memory-v2-activation-log-store.js";
42
40
  import {
@@ -153,7 +151,6 @@ describe("GET /v1/messages/:id/llm-context — memoryV2Activation", () => {
153
151
  turn: 4,
154
152
  mode: "per-turn",
155
153
  concepts: sampleConcepts,
156
- skills: sampleSkills,
157
154
  config: sampleConfig,
158
155
  });
159
156
  backfillMemoryV2ActivationMessageId(conversationId, messageId);
@@ -163,7 +160,6 @@ describe("GET /v1/messages/:id/llm-context — memoryV2Activation", () => {
163
160
  turn: number;
164
161
  mode: "context-load" | "per-turn";
165
162
  concepts: MemoryV2ConceptRowRecord[];
166
- skills: MemoryV2SkillRowRecord[];
167
163
  config: MemoryV2ConfigSnapshot;
168
164
  } | null;
169
165
  memoryRecall: unknown;
@@ -173,7 +169,6 @@ describe("GET /v1/messages/:id/llm-context — memoryV2Activation", () => {
173
169
  expect(body.memoryV2Activation!.turn).toBe(4);
174
170
  expect(body.memoryV2Activation!.mode).toBe("per-turn");
175
171
  expect(body.memoryV2Activation!.concepts).toEqual(sampleConcepts);
176
- expect(body.memoryV2Activation!.skills).toEqual(sampleSkills);
177
172
  expect(body.memoryV2Activation!.config).toEqual(sampleConfig);
178
173
  // Backwards-compat: memoryRecall field still present.
179
174
  expect(body).toHaveProperty("memoryRecall");
@@ -90,7 +90,7 @@ describe("setHeartbeatConfig handler", () => {
90
90
  // invalidation + getConfig() read picked up the new on-disk state.
91
91
  expect(result.success).toBe(true);
92
92
  expect(result.enabled).toBe(true);
93
- expect(result.intervalMs).toBe(6 * 3_600_000);
93
+ expect(result.intervalMs).toBe(30 * 60_000);
94
94
  expect(result.activeHoursStart).toBe(8);
95
95
  expect(result.activeHoursEnd).toBe(22);
96
96
  });
@@ -8,6 +8,7 @@
8
8
  import { z } from "zod";
9
9
 
10
10
  import type { HostProxyCapability } from "../../channels/types.js";
11
+ import { isHttpAuthDisabled } from "../../config/env.js";
11
12
  import { datesToISO } from "../../util/json.js";
12
13
  import { assistantEventHub } from "../assistant-event-hub.js";
13
14
  import { NotFoundError } from "./errors.js";
@@ -33,7 +34,7 @@ export const ROUTES: RouteDefinition[] = [
33
34
  responseBody: z.object({
34
35
  clients: z.array(z.object({}).passthrough()),
35
36
  }),
36
- handler: ({ queryParams }) => {
37
+ handler: ({ queryParams, headers }) => {
37
38
  const capability = queryParams?.capability as
38
39
  | HostProxyCapability
39
40
  | undefined;
@@ -42,8 +43,25 @@ export const ROUTES: RouteDefinition[] = [
42
43
  ? assistantEventHub.listClientsByCapability(capability)
43
44
  : assistantEventHub.listClients();
44
45
 
46
+ // Defense-in-depth: filter the listing to clients owned by the calling
47
+ // actor so users cannot enumerate other users' connected client IDs.
48
+ // Clients with no stored `actorPrincipalId` (legacy SSE subscribers from
49
+ // before host-proxy-same-user, service-gateway tokens) are filtered out
50
+ // — fail-closed is the right default for this security boundary.
51
+ // Dev-bypass mode (DISABLE_HTTP_AUTH=true, mirroring
52
+ // require-bound-guardian.ts) preserves the previous "return all" behavior
53
+ // for platform-managed deployments where the platform handles auth.
54
+ const callerPrincipalId = headers?.["x-vellum-actor-principal-id"];
55
+ const filtered = isHttpAuthDisabled()
56
+ ? clients
57
+ : clients.filter(
58
+ (c) =>
59
+ c.actorPrincipalId !== undefined &&
60
+ c.actorPrincipalId === callerPrincipalId,
61
+ );
62
+
45
63
  return {
46
- clients: clients.map((c) =>
64
+ clients: filtered.map((c) =>
47
65
  datesToISO({
48
66
  clientId: c.clientId,
49
67
  interfaceId: c.interfaceId,