@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
@@ -3,11 +3,16 @@ import { join } from "node:path";
3
3
 
4
4
  import { getConfig } from "../config/loader.js";
5
5
  import type { HeartbeatConfig } from "../config/schemas/heartbeat.js";
6
+ import {
7
+ checkDiskPressureBackgroundGate,
8
+ diskPressureBackgroundSkipLogFields,
9
+ shouldLogDiskPressureBackgroundSkip,
10
+ } from "../daemon/disk-pressure-background-gate.js";
6
11
  import type { HeartbeatAlert } from "../daemon/message-protocol.js";
7
12
  import { processMessage } from "../daemon/process-message.js";
8
13
  import { emitFeedEvent } from "../home/emit-feed-event.js";
9
14
  import { bootstrapConversation } from "../memory/conversation-bootstrap.js";
10
- import { getConversation } from "../memory/conversation-crud.js";
15
+ import { getConversation, getMessages } from "../memory/conversation-crud.js";
11
16
  import { GENERATING_TITLE } from "../memory/conversation-title-service.js";
12
17
  import {
13
18
  GUARDIAN_PERSONA_TEMPLATE,
@@ -21,6 +26,7 @@ import { getWorkspaceDir, getWorkspacePromptPath } from "../util/platform.js";
21
26
  import { stripCommentLines } from "../util/strip-comment-lines.js";
22
27
  import {
23
28
  completeHeartbeatRun,
29
+ countCompletedHeartbeatRuns,
24
30
  insertPendingHeartbeatRun,
25
31
  markStaleRunningAsError,
26
32
  markStaleRunsAsMissed,
@@ -38,8 +44,12 @@ const DEFAULT_CHECKLIST = `- Check in with yourself. Read NOW.md. Is it still ac
38
44
  - If you have a thought worth sharing, send it. A follow-up, a useful find, a check-in. Not every beat, but when it feels right.
39
45
  - If something has happened since your last journal entry, write one. Even a few sentences. The journal is how future-you stays connected.`;
40
46
 
47
+ const EARLY_HEARTBEAT_THRESHOLD = 3;
41
48
  const REENGAGEMENT_COOLDOWN_MS = 18 * 60 * 60 * 1000; // 18 hours
42
49
  const HEARTBEAT_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
50
+ const HEARTBEAT_ALERT_MARKER = "HEARTBEAT_ALERT";
51
+ const HEARTBEAT_OK_MARKER = "HEARTBEAT_OK";
52
+ const HEARTBEAT_ALERT_SUMMARY_MAX_CHARS = 700;
43
53
 
44
54
  // Stripped-comment form of the guardian persona scaffold. Computed
45
55
  // once at module load because stripping comment lines is deterministic
@@ -92,6 +102,69 @@ function recordReengagementTimestamp(): void {
92
102
  }
93
103
  }
94
104
 
105
+ type HeartbeatDisposition = "alert" | "ok" | "unknown";
106
+
107
+ function parseHeartbeatDisposition(text: string | null): HeartbeatDisposition {
108
+ if (!text) return "unknown";
109
+ const lines = text
110
+ .trim()
111
+ .split(/\r?\n/)
112
+ .map((line) => line.trim())
113
+ .filter((line) => line.length > 0);
114
+ const lastLine = lines.at(-1);
115
+ if (lastLine === HEARTBEAT_ALERT_MARKER) return "alert";
116
+ if (lastLine === HEARTBEAT_OK_MARKER) return "ok";
117
+ return "unknown";
118
+ }
119
+
120
+ function stripHeartbeatDispositionMarkers(text: string): string {
121
+ return text
122
+ .replace(
123
+ new RegExp(
124
+ `(?:\\r?\\n)?\\s*(?:${HEARTBEAT_ALERT_MARKER}|${HEARTBEAT_OK_MARKER})\\s*$`,
125
+ ),
126
+ "",
127
+ )
128
+ .trim();
129
+ }
130
+
131
+ function truncateSummary(text: string, maxChars: number): string {
132
+ if (text.length <= maxChars) return text;
133
+ return `${text.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
134
+ }
135
+
136
+ function buildHeartbeatAlertSummary(text: string | null): string {
137
+ const summary = text ? stripHeartbeatDispositionMarkers(text) : "";
138
+ return truncateSummary(
139
+ summary || "Your assistant found something worth your attention.",
140
+ HEARTBEAT_ALERT_SUMMARY_MAX_CHARS,
141
+ );
142
+ }
143
+
144
+ function extractVisibleTextFromStoredMessageContent(raw: string): string {
145
+ try {
146
+ const parsed = JSON.parse(raw) as unknown;
147
+ if (typeof parsed === "string") return parsed;
148
+ if (!Array.isArray(parsed)) return "";
149
+ const texts: string[] = [];
150
+ for (const block of parsed) {
151
+ if (
152
+ block != null &&
153
+ typeof block === "object" &&
154
+ "type" in block &&
155
+ block.type === "text" &&
156
+ "text" in block &&
157
+ typeof block.text === "string"
158
+ ) {
159
+ texts.push(block.text);
160
+ }
161
+ }
162
+ return texts.join("\n").trim();
163
+ } catch {
164
+ return raw;
165
+ }
166
+ }
167
+
95
168
  export interface HeartbeatDeps {
96
169
  alerter: (alert: HeartbeatAlert) => void;
97
170
  onConversationCreated?: (info: {
@@ -168,18 +241,22 @@ export class HeartbeatService {
168
241
  "Recovered stale heartbeat runs on startup",
169
242
  );
170
243
 
171
- const total = this._startupMissedCount + this._startupCrashedCount;
172
- const today = new Date().toISOString().split("T")[0];
173
- void emitFeedEvent({
174
- source: "assistant",
175
- title: "Heartbeat Runs Missed",
176
- summary: `${total} heartbeat run${total > 1 ? "s were" : " was"} missed while the assistant was offline.`,
177
- dedupKey: `heartbeat:missed:${today}`,
178
- priority: 55,
179
- urgency: "high",
180
- }).catch((err) => {
181
- log.warn({ err }, "Failed to emit missed heartbeat feed event");
182
- });
244
+ if (!isDiskPressureBackgroundLocked("heartbeat-startup")) {
245
+ const total = this._startupMissedCount + this._startupCrashedCount;
246
+ const today = new Date().toISOString().split("T")[0];
247
+ void emitFeedEvent({
248
+ source: "assistant",
249
+ title: "Heartbeat Runs Missed",
250
+ summary: `${total} heartbeat run${
251
+ total > 1 ? "s were" : " was"
252
+ } missed while the assistant was offline.`,
253
+ dedupKey: `heartbeat:missed:${today}`,
254
+ priority: 55,
255
+ urgency: "high",
256
+ }).catch((err) => {
257
+ log.warn({ err }, "Failed to emit missed heartbeat feed event");
258
+ });
259
+ }
183
260
  }
184
261
  }
185
262
 
@@ -328,6 +405,10 @@ export class HeartbeatService {
328
405
  async runOnce({ force = false }: { force?: boolean } = {}): Promise<boolean> {
329
406
  const config = getConfig().heartbeat;
330
407
 
408
+ if (!force && isDiskPressureBackgroundLocked("heartbeat")) {
409
+ return false;
410
+ }
411
+
331
412
  let runId: string | null;
332
413
  let scheduledFor: number;
333
414
  if (force) {
@@ -579,6 +660,65 @@ export class HeartbeatService {
579
660
  }
580
661
  }
581
662
 
663
+ private getLatestAssistantMessage(
664
+ conversationId: string,
665
+ ): { id: string; text: string } | null {
666
+ try {
667
+ const messages = getMessages(conversationId);
668
+ for (let i = messages.length - 1; i >= 0; i--) {
669
+ const message = messages[i]!;
670
+ if (message.role !== "assistant") continue;
671
+ return {
672
+ id: message.id,
673
+ text: extractVisibleTextFromStoredMessageContent(message.content),
674
+ };
675
+ }
676
+ } catch (err) {
677
+ log.warn(
678
+ { err, conversationId },
679
+ "Failed to read heartbeat assistant message",
680
+ );
681
+ }
682
+ return null;
683
+ }
684
+
685
+ private async emitHeartbeatAlertNotification(params: {
686
+ runId: string;
687
+ conversationId: string;
688
+ messageId?: string;
689
+ conversationTitle: string;
690
+ summary: string;
691
+ }): Promise<void> {
692
+ const { emitNotificationSignal } =
693
+ await import("../notifications/emit-signal.js");
694
+
695
+ await emitNotificationSignal({
696
+ sourceEventName: "heartbeat.alert",
697
+ sourceChannel: "watcher",
698
+ sourceContextId: params.runId,
699
+ dedupeKey: `heartbeat:alert:${params.runId}`,
700
+ attentionHints: {
701
+ requiresAction: true,
702
+ urgency: "medium",
703
+ isAsyncBackground: true,
704
+ visibleInSourceNow: false,
705
+ },
706
+ contextPayload: {
707
+ title: "Heartbeat Alert",
708
+ summary: params.summary,
709
+ conversationTitle: params.conversationTitle,
710
+ conversationId: params.conversationId,
711
+ messageId: params.messageId,
712
+ },
713
+ routingIntent: "single_channel",
714
+ conversationAffinityHint: { vellum: params.conversationId },
715
+ conversationMetadata: {
716
+ source: "heartbeat",
717
+ groupId: "system:background",
718
+ },
719
+ });
720
+ }
721
+
582
722
  private async executeRun(runId: string, scheduledFor: number): Promise<void> {
583
723
  log.info("Running heartbeat");
584
724
 
@@ -595,9 +735,11 @@ export class HeartbeatService {
595
735
  let conversationId: string | undefined;
596
736
  try {
597
737
  const checklist = this.readChecklist();
738
+ const completedRunCount = countCompletedHeartbeatRuns();
598
739
  const { prompt, includedReengagement } = this.buildPrompt(
599
740
  checklist,
600
741
  unhealthyProviders,
742
+ completedRunCount,
601
743
  );
602
744
 
603
745
  const conversation = bootstrapConversation({
@@ -609,11 +751,6 @@ export class HeartbeatService {
609
751
  });
610
752
  conversationId = conversation.id;
611
753
 
612
- this.deps.onConversationCreated?.({
613
- conversationId: conversation.id,
614
- title: "Heartbeat",
615
- });
616
-
617
754
  await processMessage(conversation.id, prompt, undefined, {
618
755
  trustContext: {
619
756
  sourceChannel: "vellum",
@@ -644,20 +781,32 @@ export class HeartbeatService {
644
781
  // Best-effort; fall back to generic title.
645
782
  }
646
783
 
647
- const today = new Date().toISOString().split("T")[0];
648
- void emitFeedEvent({
649
- source: "assistant",
650
- title,
651
- summary: "Periodic check completed. Tap to see details.",
652
- dedupKey: `heartbeat:ok:${today}`,
653
- priority: 30,
654
- }).catch((err) => {
655
- log.warn(
656
- { err, conversationId: conversation.id },
657
- "Failed to emit heartbeat feed event",
658
- );
659
- });
784
+ const assistantMessage = this.getLatestAssistantMessage(
785
+ conversation.id,
786
+ );
787
+ const disposition = parseHeartbeatDisposition(
788
+ assistantMessage?.text ?? null,
789
+ );
790
+ if (disposition === "alert") {
791
+ this.deps.onConversationCreated?.({
792
+ conversationId: conversation.id,
793
+ title,
794
+ });
795
+ void this.emitHeartbeatAlertNotification({
796
+ runId,
797
+ conversationId: conversation.id,
798
+ messageId: assistantMessage?.id,
799
+ conversationTitle: title,
800
+ summary: buildHeartbeatAlertSummary(assistantMessage?.text ?? null),
801
+ }).catch((err) => {
802
+ log.warn(
803
+ { err, conversationId: conversation.id },
804
+ "Failed to emit heartbeat alert notification",
805
+ );
806
+ });
807
+ }
660
808
 
809
+ const today = new Date().toISOString().split("T")[0];
661
810
  if (latenessMs > LATE_THRESHOLD_MS) {
662
811
  const lateMinutes = Math.round(latenessMs / 60_000);
663
812
  void emitFeedEvent({
@@ -714,6 +863,7 @@ export class HeartbeatService {
714
863
  buildPrompt(
715
864
  checklist: string,
716
865
  unhealthyProviders: string[] = [],
866
+ completedRunCount: number = Infinity,
717
867
  ): { prompt: string; includedReengagement: boolean } {
718
868
  let prompt = `You are running a periodic heartbeat check. Review the following checklist and take any necessary actions.
719
869
 
@@ -730,11 +880,20 @@ Do NOT attempt to use tools for these providers — they will fail. Skip any che
730
880
  }
731
881
 
732
882
  prompt += `\n\n<heartbeat-disposition>
883
+ This heartbeat runs frequently. Do not manufacture a report just because it ran.
884
+ If there is nothing genuinely useful, actionable, or interesting to surface, keep the response brief and end with HEARTBEAT_OK.
885
+ If there is something worth interrupting the guardian for, write a concise guardian-facing note first: what happened, why it matters, and the recommended next step. Address the guardian directly as "you"; do not write instructions to yourself or another intermediary. Then end with HEARTBEAT_ALERT. That note may be used as notification copy.
733
886
  After completing your review, end your response with one of:
734
887
  - HEARTBEAT_OK — if everything looks good, no action needed
735
888
  - HEARTBEAT_ALERT — if you found issues that need attention (describe them before this marker)
736
889
  </heartbeat-disposition>`;
737
890
 
891
+ if (completedRunCount < EARLY_HEARTBEAT_THRESHOLD) {
892
+ prompt += `\n\n<early-heartbeat>
893
+ This is one of your first heartbeats. Your user hasn't heard from you yet and may not know you're here. Find something genuinely useful to share — a follow-up from a recent conversation, something you noticed, or a quick check-in. Lean toward HEARTBEAT_ALERT this time. First impressions matter.
894
+ </early-heartbeat>`;
895
+ }
896
+
738
897
  let includedReengagement = false;
739
898
  if (isShallowProfile() && isReengagementCooldownElapsed()) {
740
899
  includedReengagement = true;
@@ -745,6 +904,21 @@ After completing your review, end your response with one of:
745
904
  }
746
905
  }
747
906
 
907
+ function isDiskPressureBackgroundLocked(logKey: string): boolean {
908
+ const diskPressureGate = checkDiskPressureBackgroundGate("background-work");
909
+ if (diskPressureGate.action === "allow") return false;
910
+ if (shouldLogDiskPressureBackgroundSkip(logKey)) {
911
+ log.warn(
912
+ {
913
+ source: "heartbeat",
914
+ ...diskPressureBackgroundSkipLogFields(diskPressureGate),
915
+ },
916
+ "Heartbeat skipped during disk pressure cleanup mode",
917
+ );
918
+ }
919
+ return true;
920
+ }
921
+
748
922
  /**
749
923
  * Check if the given hour falls within the active window.
750
924
  * Handles overnight windows (e.g. start=22, end=6).
@@ -28,6 +28,11 @@
28
28
  * scheduler needing to touch the event hub.
29
29
  */
30
30
 
31
+ import {
32
+ checkDiskPressureBackgroundGate,
33
+ diskPressureBackgroundSkipLogFields,
34
+ shouldLogDiskPressureBackgroundSkip,
35
+ } from "../daemon/disk-pressure-background-gate.js";
31
36
  import { getLogger } from "../util/logger.js";
32
37
  import type { FeedItem } from "./feed-types.js";
33
38
  import {
@@ -119,6 +124,19 @@ export function startFeedScheduler(
119
124
  rollupRan: false,
120
125
  };
121
126
  if (stopped || tickRunning) return summary;
127
+ const diskPressureGate = checkDiskPressureBackgroundGate("background-work");
128
+ if (diskPressureGate.action === "skip") {
129
+ if (shouldLogDiskPressureBackgroundSkip("home-feed-scheduler")) {
130
+ log.warn(
131
+ {
132
+ source: "feed-scheduler",
133
+ ...diskPressureBackgroundSkipLogFields(diskPressureGate),
134
+ },
135
+ "Home feed scheduler skipped during disk pressure cleanup mode",
136
+ );
137
+ }
138
+ return summary;
139
+ }
122
140
  tickRunning = true;
123
141
  const nowMs = now.getTime();
124
142
  try {
@@ -16,11 +16,7 @@
16
16
  * callback_url that external services should use.
17
17
  */
18
18
 
19
- import {
20
- getPlatformAssistantId,
21
- getPlatformBaseUrl,
22
- getPlatformInternalApiKey,
23
- } from "../config/env.js";
19
+ import { getPlatformAssistantId, getPlatformBaseUrl } from "../config/env.js";
24
20
  import { getIsPlatform } from "../config/env-registry.js";
25
21
  import { credentialKey } from "../security/credential-key.js";
26
22
  import { getSecureKeyAsync } from "../security/secure-keys.js";
@@ -32,7 +28,6 @@ export interface PlatformCallbackRegistrationContext {
32
28
  isPlatform: boolean;
33
29
  platformBaseUrl: string;
34
30
  assistantId: string;
35
- hasInternalApiKey: boolean;
36
31
  hasAssistantApiKey: boolean;
37
32
  authHeader: string | null;
38
33
  enabled: boolean;
@@ -54,20 +49,18 @@ export async function resolvePlatformCallbackRegistrationContext(): Promise<Plat
54
49
  );
55
50
  const assistantId =
56
51
  getPlatformAssistantId().trim() || storedAssistantIdRaw?.trim() || "";
57
- const internalApiKey = getPlatformInternalApiKey().trim();
58
- const assistantApiKey = storedAssistantApiKeyRaw?.trim() || "";
59
- const authHeader = internalApiKey
60
- ? `Bearer ${internalApiKey}`
61
- : assistantApiKey
62
- ? `Api-Key ${assistantApiKey}`
63
- : null;
52
+ const envAssistantCredential = process.env.ASSISTANT_API_KEY?.trim();
53
+ const assistantCredential =
54
+ storedAssistantApiKeyRaw?.trim() || envAssistantCredential || undefined;
55
+ const authHeader = assistantCredential
56
+ ? `Api-Key ${assistantCredential}`
57
+ : null;
64
58
 
65
59
  return {
66
60
  isPlatform: platform,
67
61
  platformBaseUrl,
68
62
  assistantId,
69
- hasInternalApiKey: internalApiKey.length > 0,
70
- hasAssistantApiKey: assistantApiKey.length > 0,
63
+ hasAssistantApiKey: !!assistantCredential,
71
64
  authHeader,
72
65
  // Enabled when we have enough context to register callback routes.
73
66
  // Does NOT require IS_PLATFORM — self-hosted assistants with stored
@@ -0,0 +1,169 @@
1
+ /**
2
+ * End-to-end test for `assistant clients list` over IPC.
3
+ *
4
+ * Regression test for the gap where the same-user filter on
5
+ * `GET /v1/clients` (which reads `headers["x-vellum-actor-principal-id"]`)
6
+ * silently returned an empty list over IPC because the IPC adapter did
7
+ * not inject the synthetic actor-principal header that the HTTP adapter
8
+ * populates from the verified `AuthContext`.
9
+ *
10
+ * Asserts that in non-dev-bypass mode (`isHttpAuthDisabled() === false`),
11
+ * the CLI sees same-user clients via the IPC path because the IPC server
12
+ * fills in the header from the local guardian principal.
13
+ */
14
+
15
+ import {
16
+ afterAll,
17
+ afterEach,
18
+ beforeEach,
19
+ describe,
20
+ expect,
21
+ mock,
22
+ test,
23
+ } from "bun:test";
24
+
25
+ import { runAssistantCommandFull } from "../../cli/__tests__/run-assistant-command.js";
26
+ import { AssistantIpcServer } from "../assistant-server.js";
27
+
28
+ // ── Module mocks (must be set up before importing the route) ──────────────
29
+
30
+ let fakeHttpAuthDisabled = false;
31
+ let fakeLocalPrincipalId: string | undefined = "guardian-local";
32
+
33
+ mock.module("../../config/env.js", () => ({
34
+ isHttpAuthDisabled: () => fakeHttpAuthDisabled,
35
+ hasUngatedHttpAuthDisabled: () => false,
36
+ }));
37
+
38
+ mock.module("../../runtime/local-actor-identity.js", () => ({
39
+ findLocalGuardianPrincipalId: () => fakeLocalPrincipalId,
40
+ }));
41
+
42
+ // ── Real imports (after mocks) ────────────────────────────────────────────
43
+
44
+ import { assistantEventHub } from "../../runtime/assistant-event-hub.js";
45
+
46
+ // ── Fixtures ──────────────────────────────────────────────────────────────
47
+
48
+ let server: AssistantIpcServer | null = null;
49
+
50
+ function registerClient(args: {
51
+ clientId: string;
52
+ actorPrincipalId?: string;
53
+ }): void {
54
+ assistantEventHub.subscribe({
55
+ type: "client",
56
+ clientId: args.clientId,
57
+ interfaceId: "macos",
58
+ capabilities: ["host_bash", "host_file", "host_cu"],
59
+ actorPrincipalId: args.actorPrincipalId,
60
+ callback: () => {},
61
+ });
62
+ }
63
+
64
+ function clearHub(): void {
65
+ const ids = assistantEventHub.listClients().map((c) => c.clientId);
66
+ for (const id of ids) {
67
+ assistantEventHub.disposeClient(id);
68
+ }
69
+ }
70
+
71
+ async function startServer(): Promise<void> {
72
+ server = new AssistantIpcServer();
73
+ await server.start();
74
+ // Allow the listener to be ready before the CLI tries to connect.
75
+ await new Promise((resolve) => setTimeout(resolve, 50));
76
+ }
77
+
78
+ beforeEach(() => {
79
+ fakeHttpAuthDisabled = false;
80
+ fakeLocalPrincipalId = "guardian-local";
81
+ clearHub();
82
+ });
83
+
84
+ afterEach(() => {
85
+ server?.stop();
86
+ server = null;
87
+ });
88
+
89
+ afterAll(() => {
90
+ mock.restore();
91
+ });
92
+
93
+ // ── Tests ────────────────────────────────────────────────────────────────
94
+
95
+ describe("assistant clients list over IPC — same-user filter", () => {
96
+ test("returns same-user clients in non-dev-bypass mode", async () => {
97
+ registerClient({
98
+ clientId: "client-self-1",
99
+ actorPrincipalId: "guardian-local",
100
+ });
101
+ registerClient({
102
+ clientId: "client-self-2",
103
+ actorPrincipalId: "guardian-local",
104
+ });
105
+ registerClient({
106
+ clientId: "client-other",
107
+ actorPrincipalId: "other-user",
108
+ });
109
+
110
+ await startServer();
111
+
112
+ const { stdout } = await runAssistantCommandFull(
113
+ "clients",
114
+ "list",
115
+ "--json",
116
+ );
117
+
118
+ const parsed = JSON.parse(stdout.trim()) as {
119
+ clients: Array<{ clientId: string }>;
120
+ };
121
+ const ids = parsed.clients.map((c) => c.clientId).sort();
122
+ expect(ids).toEqual(["client-self-1", "client-self-2"]);
123
+ });
124
+
125
+ test("returns empty when no local guardian principal is bound (fail-closed)", async () => {
126
+ fakeLocalPrincipalId = undefined;
127
+ registerClient({
128
+ clientId: "client-self",
129
+ actorPrincipalId: "guardian-local",
130
+ });
131
+
132
+ await startServer();
133
+
134
+ const { stdout } = await runAssistantCommandFull(
135
+ "clients",
136
+ "list",
137
+ "--json",
138
+ );
139
+ const parsed = JSON.parse(stdout.trim()) as {
140
+ clients: Array<{ clientId: string }>;
141
+ };
142
+ expect(parsed.clients).toEqual([]);
143
+ });
144
+
145
+ test("dev-bypass mode returns all clients regardless of principal", async () => {
146
+ fakeHttpAuthDisabled = true;
147
+ registerClient({
148
+ clientId: "client-self",
149
+ actorPrincipalId: "guardian-local",
150
+ });
151
+ registerClient({
152
+ clientId: "client-other",
153
+ actorPrincipalId: "other-user",
154
+ });
155
+
156
+ await startServer();
157
+
158
+ const { stdout } = await runAssistantCommandFull(
159
+ "clients",
160
+ "list",
161
+ "--json",
162
+ );
163
+ const parsed = JSON.parse(stdout.trim()) as {
164
+ clients: Array<{ clientId: string }>;
165
+ };
166
+ const ids = parsed.clients.map((c) => c.clientId).sort();
167
+ expect(ids).toEqual(["client-other", "client-self"]);
168
+ });
169
+ });