@vellumai/assistant 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. package/ARCHITECTURE.md +45 -29
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/docs/architecture/memory.md +5 -2
  6. package/knip.json +1 -0
  7. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  8. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  9. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  11. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  12. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  13. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  16. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  17. package/openapi.yaml +470 -25
  18. package/package.json +3 -1
  19. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  20. package/src/__tests__/app-control-flow.test.ts +21 -11
  21. package/src/__tests__/approval-cascade.test.ts +8 -16
  22. package/src/__tests__/approval-routes-http.test.ts +6 -0
  23. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  24. package/src/__tests__/assistant-event.test.ts +0 -10
  25. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  26. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  27. package/src/__tests__/auto-analysis-end-to-end.test.ts +48 -0
  28. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  29. package/src/__tests__/call-constants.test.ts +10 -1
  30. package/src/__tests__/call-controller.test.ts +127 -0
  31. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  32. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  33. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  34. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  35. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  36. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  37. package/src/__tests__/config-schema.test.ts +1 -0
  38. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  39. package/src/__tests__/config-watcher.test.ts +140 -69
  40. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  41. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  42. package/src/__tests__/context-search-fanout.test.ts +0 -1
  43. package/src/__tests__/context-search-memory-source.test.ts +6 -33
  44. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  45. package/src/__tests__/context-search-pkb-source.test.ts +12 -7
  46. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  47. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
  48. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  51. package/src/__tests__/conversation-agent-loop.test.ts +457 -8
  52. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  53. package/src/__tests__/conversation-error.test.ts +150 -3
  54. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  55. package/src/__tests__/conversation-process-callsite.test.ts +38 -0
  56. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +74 -0
  58. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  59. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  60. package/src/__tests__/conversation-store.test.ts +0 -18
  61. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  62. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  63. package/src/__tests__/conversation-surfaces-data-persist.test.ts +476 -0
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +61 -5
  65. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  66. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  67. package/src/__tests__/credentials-cli.test.ts +7 -0
  68. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  69. package/src/__tests__/date-context.test.ts +164 -2
  70. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  71. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  72. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  73. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  74. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  75. package/src/__tests__/disk-usage.test.ts +150 -0
  76. package/src/__tests__/events-client-registration.test.ts +52 -0
  77. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  78. package/src/__tests__/file-write-tool.test.ts +4 -10
  79. package/src/__tests__/filing-service.test.ts +2 -20
  80. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  82. package/src/__tests__/heartbeat-service.test.ts +260 -11
  83. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  84. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  85. package/src/__tests__/host-bash-routes.test.ts +178 -13
  86. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  87. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  88. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  89. package/src/__tests__/host-file-proxy.test.ts +268 -6
  90. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  91. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  92. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  93. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  94. package/src/__tests__/injector-chain.test.ts +36 -16
  95. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  96. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  98. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  99. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  100. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  101. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  102. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  103. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  104. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  105. package/src/__tests__/oauth-cli.test.ts +121 -0
  106. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  107. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  108. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  109. package/src/__tests__/openai-provider.test.ts +45 -8
  110. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  111. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  112. package/src/__tests__/platform.test.ts +2 -1
  113. package/src/__tests__/playbook-execution.test.ts +0 -43
  114. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  115. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  116. package/src/__tests__/provider-tool-name.test.ts +23 -0
  117. package/src/__tests__/relay-server.test.ts +60 -5
  118. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  119. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  120. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  121. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  122. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  123. package/src/__tests__/secret-response-routing.test.ts +7 -5
  124. package/src/__tests__/server-history-render.test.ts +82 -0
  125. package/src/__tests__/skill-include-graph.test.ts +31 -0
  126. package/src/__tests__/skill-load-tool.test.ts +44 -16
  127. package/src/__tests__/skills.test.ts +39 -0
  128. package/src/__tests__/suggestion-routes.test.ts +46 -0
  129. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  130. package/src/__tests__/tool-executor.test.ts +155 -0
  131. package/src/__tests__/twilio-validation.test.ts +2 -2
  132. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  133. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  134. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  136. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  137. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  138. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +78 -0
  139. package/src/agent/loop.ts +11 -0
  140. package/src/approvals/guardian-request-resolvers.ts +3 -32
  141. package/src/backup/snapshot-lock.ts +2 -27
  142. package/src/bundler/compiler-tools.ts +3 -2
  143. package/src/calls/call-constants.ts +5 -8
  144. package/src/calls/call-controller.ts +130 -67
  145. package/src/calls/call-conversation-messages.ts +46 -10
  146. package/src/calls/relay-server.ts +7 -1
  147. package/src/calls/voice-session-bridge.ts +1 -1
  148. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  149. package/src/cli/commands/bash.ts +35 -108
  150. package/src/cli/commands/contacts.ts +64 -25
  151. package/src/cli/commands/credentials.ts +56 -0
  152. package/src/cli/commands/memory-v2.ts +11 -10
  153. package/src/cli/commands/oauth/__tests__/connect.test.ts +401 -219
  154. package/src/cli/commands/oauth/connect.ts +124 -40
  155. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  156. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  157. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  158. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  159. package/src/cli/commands/platform/index.ts +16 -7
  160. package/src/cli/commands/status.ts +57 -0
  161. package/src/cli/program.ts +4 -2
  162. package/src/config/assistant-feature-flags.ts +13 -3
  163. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  164. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  165. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  166. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  167. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  168. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  169. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  170. package/src/config/env.ts +0 -8
  171. package/src/config/feature-flag-registry.json +13 -5
  172. package/src/config/loader.ts +199 -27
  173. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  174. package/src/config/schemas/call-site-catalog.ts +14 -0
  175. package/src/config/schemas/channels.ts +0 -5
  176. package/src/config/schemas/heartbeat.ts +1 -1
  177. package/src/config/schemas/llm.ts +2 -0
  178. package/src/config/schemas/memory-lifecycle.ts +13 -0
  179. package/src/config/schemas/memory-v2.ts +76 -12
  180. package/src/config/schemas/platform.ts +43 -3
  181. package/src/config/schemas/services.ts +28 -0
  182. package/src/config/seed-inference-profiles.ts +230 -33
  183. package/src/contacts/contact-store.ts +0 -25
  184. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  185. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  186. package/src/daemon/assistant-attachments.ts +4 -4
  187. package/src/daemon/config-watcher.ts +85 -57
  188. package/src/daemon/conversation-agent-loop-handlers.ts +38 -0
  189. package/src/daemon/conversation-agent-loop.ts +183 -43
  190. package/src/daemon/conversation-error.ts +87 -15
  191. package/src/daemon/conversation-lifecycle.ts +22 -10
  192. package/src/daemon/conversation-process.ts +8 -0
  193. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  194. package/src/daemon/conversation-store.ts +2 -2
  195. package/src/daemon/conversation-surfaces.ts +211 -29
  196. package/src/daemon/conversation-tool-setup.ts +66 -19
  197. package/src/daemon/conversation.ts +18 -23
  198. package/src/daemon/date-context.ts +71 -22
  199. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  200. package/src/daemon/disk-pressure-guard.ts +343 -0
  201. package/src/daemon/disk-pressure-policy.ts +163 -0
  202. package/src/daemon/handlers/shared.ts +26 -1
  203. package/src/daemon/handlers/skills.ts +3 -4
  204. package/src/daemon/host-app-control-proxy.ts +137 -41
  205. package/src/daemon/host-bash-proxy.ts +47 -22
  206. package/src/daemon/host-browser-proxy.ts +1 -1
  207. package/src/daemon/host-cu-proxy.ts +50 -4
  208. package/src/daemon/host-file-proxy.ts +44 -8
  209. package/src/daemon/host-transfer-proxy.ts +97 -6
  210. package/src/daemon/lifecycle.ts +167 -101
  211. package/src/daemon/meet-host-supervisor.ts +4 -4
  212. package/src/daemon/meet-manifest-loader.ts +0 -1
  213. package/src/daemon/memory-v2-startup.ts +66 -15
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/conversations.ts +4 -0
  216. package/src/daemon/message-types/disk-pressure.ts +9 -0
  217. package/src/daemon/message-types/messages.ts +22 -1
  218. package/src/daemon/profiler-run-store.ts +5 -5
  219. package/src/daemon/tool-setup-types.ts +2 -2
  220. package/src/documents/document-store.ts +119 -0
  221. package/src/filing/filing-service.ts +29 -5
  222. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  223. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  224. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  225. package/src/heartbeat/heartbeat-service.ts +205 -31
  226. package/src/home/feed-scheduler.ts +18 -0
  227. package/src/inbound/platform-callback-registration.ts +8 -15
  228. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  229. package/src/ipc/assistant-server.ts +149 -38
  230. package/src/ipc/gateway-client.ts +37 -3
  231. package/src/ipc/skill-server.ts +99 -42
  232. package/src/live-voice/live-voice-archive.ts +4 -4
  233. package/src/live-voice/protocol.ts +5 -7
  234. package/src/media/image-service.ts +1 -7
  235. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  236. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +34 -51
  237. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  238. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  239. package/src/memory/admin.ts +5 -9
  240. package/src/memory/context-search/agent-runner.ts +19 -2
  241. package/src/memory/context-search/sources/conversations.ts +2 -11
  242. package/src/memory/context-search/sources/memory-v2.ts +1 -16
  243. package/src/memory/context-search/sources/memory.ts +2 -3
  244. package/src/memory/context-search/sources/pkb.ts +2 -3
  245. package/src/memory/context-search/types.ts +0 -1
  246. package/src/memory/conversation-crud.ts +4 -12
  247. package/src/memory/db-init.ts +2 -0
  248. package/src/memory/embedding-runtime-manager.ts +119 -5
  249. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +136 -82
  250. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  251. package/src/memory/graph/conversation-graph-memory.ts +72 -61
  252. package/src/memory/graph/extraction.ts +1 -3
  253. package/src/memory/graph/graph-search.test.ts +11 -67
  254. package/src/memory/graph/graph-search.ts +4 -24
  255. package/src/memory/graph/retriever.test.ts +12 -1
  256. package/src/memory/graph/retriever.ts +10 -15
  257. package/src/memory/graph/tool-handlers.ts +3 -4
  258. package/src/memory/graph/tools.ts +4 -4
  259. package/src/memory/indexer.ts +53 -45
  260. package/src/memory/job-handlers/backfill.ts +2 -11
  261. package/src/memory/job-handlers/cleanup.ts +43 -0
  262. package/src/memory/job-handlers/embedding.ts +6 -8
  263. package/src/memory/job-handlers/summarization.ts +2 -7
  264. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  265. package/src/memory/jobs/embed-concept-page.ts +223 -87
  266. package/src/memory/jobs-store.ts +48 -0
  267. package/src/memory/jobs-worker.ts +85 -43
  268. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  269. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  270. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  271. package/src/memory/migrations/index.ts +1 -0
  272. package/src/memory/pkb/pkb-search.test.ts +7 -0
  273. package/src/memory/pkb/pkb-search.ts +4 -5
  274. package/src/memory/qdrant-client.ts +3 -13
  275. package/src/memory/rerank-local.ts +374 -0
  276. package/src/memory/search/semantic.ts +10 -72
  277. package/src/memory/trace-event-store.ts +1 -17
  278. package/src/memory/v2/__tests__/activation.test.ts +346 -255
  279. package/src/memory/v2/__tests__/consolidation-job.test.ts +61 -40
  280. package/src/memory/v2/__tests__/injection.test.ts +297 -190
  281. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  282. package/src/memory/v2/__tests__/qdrant.test.ts +326 -9
  283. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  284. package/src/memory/v2/__tests__/sim.test.ts +113 -196
  285. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  286. package/src/memory/v2/__tests__/static-context.test.ts +77 -14
  287. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  288. package/src/memory/v2/activation.ts +149 -156
  289. package/src/memory/v2/consolidation-job.ts +69 -20
  290. package/src/memory/v2/injection.ts +75 -68
  291. package/src/memory/v2/page-store.ts +39 -0
  292. package/src/memory/v2/prompts/consolidation.ts +41 -1
  293. package/src/memory/v2/qdrant.ts +306 -46
  294. package/src/memory/v2/reranker.ts +177 -0
  295. package/src/memory/v2/sim.ts +77 -110
  296. package/src/memory/v2/skill-content.ts +4 -3
  297. package/src/memory/v2/skill-store.ts +82 -59
  298. package/src/memory/v2/static-context.ts +26 -8
  299. package/src/memory/v2/sweep-job.ts +5 -6
  300. package/src/memory/v2/types.ts +17 -10
  301. package/src/notifications/copy-composer.ts +47 -0
  302. package/src/notifications/decision-engine.ts +46 -0
  303. package/src/notifications/signal.ts +4 -0
  304. package/src/oauth/AGENTS.md +3 -1
  305. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  306. package/src/oauth/connect-orchestrator.ts +2 -0
  307. package/src/oauth/connection-resolver.test.ts +66 -1
  308. package/src/oauth/connection-resolver.ts +55 -1
  309. package/src/oauth/oauth-connect-state.ts +77 -0
  310. package/src/oauth/seed-providers.ts +58 -1
  311. package/src/permissions/gateway-threshold-reader.ts +116 -8
  312. package/src/permissions/prompter.ts +86 -96
  313. package/src/permissions/secret-prompter.ts +31 -31
  314. package/src/plugins/defaults/injectors.ts +36 -4
  315. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  316. package/src/plugins/types.ts +7 -0
  317. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  318. package/src/proactive-artifact/decision.test.ts +226 -0
  319. package/src/proactive-artifact/decision.ts +165 -0
  320. package/src/proactive-artifact/index.ts +7 -0
  321. package/src/proactive-artifact/job.test.ts +914 -0
  322. package/src/proactive-artifact/job.ts +366 -0
  323. package/src/proactive-artifact/message-copy.ts +58 -0
  324. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  325. package/src/proactive-artifact/trigger-state.ts +119 -0
  326. package/src/prompts/normalize-onboarding.ts +80 -0
  327. package/src/prompts/persona-resolver.ts +101 -9
  328. package/src/prompts/system-prompt.ts +21 -7
  329. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  330. package/src/prompts/templates/SOUL.md +13 -28
  331. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  332. package/src/providers/model-intents.ts +7 -0
  333. package/src/providers/openrouter/client.ts +8 -0
  334. package/src/providers/retry.ts +50 -0
  335. package/src/providers/types.ts +1 -0
  336. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  337. package/src/runtime/agent-wake.ts +238 -100
  338. package/src/runtime/assistant-event-hub.ts +36 -6
  339. package/src/runtime/assistant-event.ts +0 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  341. package/src/runtime/auth/route-policy.ts +15 -1
  342. package/src/runtime/auth/same-actor.ts +216 -0
  343. package/src/runtime/channel-approvals.ts +3 -2
  344. package/src/runtime/channel-retry-sweep.ts +65 -1
  345. package/src/runtime/local-actor-identity.ts +52 -11
  346. package/src/runtime/pending-interactions.ts +27 -15
  347. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  348. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  349. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  350. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  351. package/src/runtime/routes/approval-routes.ts +7 -3
  352. package/src/runtime/routes/client-routes.ts +20 -2
  353. package/src/runtime/routes/consolidation-routes.ts +8 -9
  354. package/src/runtime/routes/contact-routes.ts +0 -25
  355. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  356. package/src/runtime/routes/conversation-routes.ts +35 -26
  357. package/src/runtime/routes/debug-bash-routes.ts +165 -0
  358. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  359. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  360. package/src/runtime/routes/documents-routes.ts +2 -75
  361. package/src/runtime/routes/events-routes.ts +41 -9
  362. package/src/runtime/routes/filing-routes.ts +2 -3
  363. package/src/runtime/routes/host-bash-routes.ts +23 -3
  364. package/src/runtime/routes/host-cu-routes.ts +33 -6
  365. package/src/runtime/routes/host-file-routes.ts +32 -6
  366. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  367. package/src/runtime/routes/identity-routes.ts +7 -138
  368. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  369. package/src/runtime/routes/index.ts +6 -0
  370. package/src/runtime/routes/memory-item-routes.test.ts +37 -17
  371. package/src/runtime/routes/memory-item-routes.ts +5 -6
  372. package/src/runtime/routes/memory-v2-routes.ts +136 -17
  373. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  374. package/src/runtime/verification-outbound-actions.ts +4 -4
  375. package/src/schedule/run-script.ts +37 -5
  376. package/src/schedule/scheduler.ts +20 -1
  377. package/src/security/encrypted-store.ts +2 -0
  378. package/src/security/secure-keys.ts +55 -0
  379. package/src/skills/include-graph.ts +35 -13
  380. package/src/skills/remote-skill-policy.ts +4 -10
  381. package/src/subagent/index.ts +1 -7
  382. package/src/subagent/manager.ts +1 -15
  383. package/src/tasks/task-runner.ts +0 -1
  384. package/src/tasks/task-store.ts +0 -3
  385. package/src/tools/background-tool-registry.ts +17 -3
  386. package/src/tools/document/document-tool.ts +20 -0
  387. package/src/tools/executor.ts +18 -2
  388. package/src/tools/host-filesystem/edit.test.ts +151 -0
  389. package/src/tools/host-filesystem/edit.ts +43 -1
  390. package/src/tools/host-filesystem/read.test.ts +129 -0
  391. package/src/tools/host-filesystem/read.ts +43 -1
  392. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  393. package/src/tools/host-filesystem/transfer.ts +56 -11
  394. package/src/tools/host-filesystem/write.test.ts +134 -0
  395. package/src/tools/host-filesystem/write.ts +43 -1
  396. package/src/tools/host-terminal/host-shell.ts +13 -6
  397. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  398. package/src/tools/memory/register.test.ts +14 -9
  399. package/src/tools/memory/register.ts +1 -2
  400. package/src/tools/permission-checker.ts +15 -0
  401. package/src/tools/provider-tool-name.ts +28 -0
  402. package/src/tools/registry.ts +30 -9
  403. package/src/tools/skills/load.ts +24 -20
  404. package/src/tools/terminal/shell.ts +9 -1
  405. package/src/tools/tool-approval-handler.ts +31 -6
  406. package/src/tools/tool-name-aliases.ts +19 -0
  407. package/src/tools/types.ts +43 -3
  408. package/src/tts/provider-catalog.ts +3 -5
  409. package/src/util/disk-usage.ts +138 -0
  410. package/src/util/platform.ts +21 -11
  411. package/src/util/process-liveness.ts +26 -0
  412. package/src/workspace/heartbeat-service.ts +19 -0
  413. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  414. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  415. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +14 -0
  416. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  417. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  418. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  419. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  420. package/src/workspace/migrations/registry.ts +14 -0
  421. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  422. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  423. package/src/memory/v2/skill-qdrant.ts +0 -404
  424. package/src/signals/bash.ts +0 -198
@@ -0,0 +1,129 @@
1
+ import { mkdtempSync, realpathSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, mock, test } from "bun:test";
5
+
6
+ import type { ToolContext } from "../types.js";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Singleton mocks — must precede the tool import so bun's module mock applies.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ let mockProxyAvailable = false;
13
+
14
+ mock.module("../../daemon/host-file-proxy.js", () => ({
15
+ HostFileProxy: {
16
+ get instance() {
17
+ return {
18
+ isAvailable: () => mockProxyAvailable,
19
+ request: () => Promise.resolve({ content: "ok", isError: false }),
20
+ };
21
+ },
22
+ },
23
+ }));
24
+
25
+ mock.module("../../runtime/assistant-event-hub.js", () => ({
26
+ assistantEventHub: {
27
+ listClientsByCapability: () => [],
28
+ },
29
+ }));
30
+
31
+ const { hostFileReadTool } = await import("./read.js");
32
+
33
+ const testDirs: string[] = [];
34
+
35
+ afterEach(() => {
36
+ mockProxyAvailable = false;
37
+ for (const dir of testDirs.splice(0)) {
38
+ rmSync(dir, { recursive: true, force: true });
39
+ }
40
+ });
41
+
42
+ function makeTempDir(): string {
43
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "host-read-test-")));
44
+ testDirs.push(dir);
45
+ return dir;
46
+ }
47
+
48
+ function makeContext(
49
+ workingDir: string,
50
+ transportInterface: ToolContext["transportInterface"],
51
+ ): ToolContext {
52
+ return {
53
+ workingDir,
54
+ conversationId: "test-conv",
55
+ trustClass: "guardian",
56
+ transportInterface,
57
+ };
58
+ }
59
+
60
+ describe("host_file_read cross-client guards", () => {
61
+ test("returns 'no client' error on web transport when proxy unavailable and no targetClientId", async () => {
62
+ const workingDir = makeTempDir();
63
+ const result = await hostFileReadTool.execute(
64
+ { path: "/some/host/path.txt" },
65
+ makeContext(workingDir, "web"),
66
+ );
67
+ expect(result.isError).toBe(true);
68
+ expect(result.content).toContain(
69
+ "no client with host_file capability is connected",
70
+ );
71
+ });
72
+
73
+ test("returns 'specified client disconnected' error when targetClientId set but proxy unavailable on web", async () => {
74
+ const workingDir = makeTempDir();
75
+ const result = await hostFileReadTool.execute(
76
+ { path: "/some/host/path.txt", target_client_id: "abc-123" },
77
+ makeContext(workingDir, "web"),
78
+ );
79
+ expect(result.isError).toBe(true);
80
+ expect(result.content).toContain(
81
+ 'target client "abc-123" is no longer connected',
82
+ );
83
+ });
84
+
85
+ test("falls through to local fs on macos transport when proxy unavailable and path is non-image", async () => {
86
+ const workingDir = makeTempDir();
87
+ const result = await hostFileReadTool.execute(
88
+ { path: "/nonexistent/x.txt" },
89
+ makeContext(workingDir, "macos"),
90
+ );
91
+ // Proves the guard did NOT fire on macOS — instead we got the
92
+ // local FileSystemOps NOT_FOUND error.
93
+ expect(result.isError).toBe(true);
94
+ expect(result.content).toContain("File not found");
95
+ });
96
+
97
+ test("does NOT reject on macos transport with a stale target_client_id when proxy unavailable (regression: P2 fix)", async () => {
98
+ const workingDir = makeTempDir();
99
+ const result = await hostFileReadTool.execute(
100
+ { path: "/nonexistent/x.txt", target_client_id: "stale-mac" },
101
+ makeContext(workingDir, "macos"),
102
+ );
103
+ // The disconnected-target guard is scoped to non-host-proxy transports
104
+ // (!supportsHostProxy). On macos, a stale target_client_id auto-filled
105
+ // from a prior cross-client turn must be silently ignored and the call
106
+ // must fall through to local FileSystemOps (NOT_FOUND for a fake path),
107
+ // NOT reject with "target client ... is no longer connected".
108
+ expect(result.isError).toBe(true);
109
+ expect(result.content).toContain("File not found");
110
+ expect(result.content).not.toContain("is no longer connected");
111
+ });
112
+
113
+ test("rejects when target_client_id is set but transport metadata is missing (legacy/backwards-compat path)", async () => {
114
+ const workingDir = makeTempDir();
115
+ const result = await hostFileReadTool.execute(
116
+ { path: "/some/host/path.txt", target_client_id: "abc-123" },
117
+ // transportInterface intentionally undefined (legacy callers).
118
+ makeContext(workingDir, undefined),
119
+ );
120
+ // When transport metadata is missing we cannot rule out a non-host-proxy
121
+ // turn, so falling through to local fs would silently target the daemon
122
+ // container. The guard fires for both undefined transport AND
123
+ // non-host-proxy transports — only macos turns skip it.
124
+ expect(result.isError).toBe(true);
125
+ expect(result.content).toContain(
126
+ 'target client "abc-123" is no longer connected',
127
+ );
128
+ });
129
+ });
@@ -63,7 +63,8 @@ class HostFileReadTool implements Tool {
63
63
  }
64
64
 
65
65
  const targetClientId =
66
- typeof input.target_client_id === "string" && input.target_client_id !== ""
66
+ typeof input.target_client_id === "string" &&
67
+ input.target_client_id !== ""
67
68
  ? input.target_client_id
68
69
  : undefined;
69
70
 
@@ -80,6 +81,45 @@ class HostFileReadTool implements Tool {
80
81
  };
81
82
  }
82
83
 
84
+ // Guard: non-host-proxy interfaces with no capable clients connected.
85
+ // Without this guard, the request would fall through to local
86
+ // FileSystemOps below and read the daemon container's filesystem
87
+ // instead of the user's host machine.
88
+ if (
89
+ targetClientId == null &&
90
+ transportInterface != null &&
91
+ !supportsHostProxy(transportInterface) &&
92
+ !HostFileProxy.instance.isAvailable()
93
+ ) {
94
+ return {
95
+ content:
96
+ "Error: no client with host_file capability is connected. Connect a macOS client to use host_file from a non-desktop interface.",
97
+ isError: true,
98
+ };
99
+ }
100
+
101
+ // Guard: explicit targetClientId provided but proxy is unavailable.
102
+ // Fires on non-host-proxy transports (web, ios) AND on legacy callers
103
+ // without transport metadata, where falling through to local fs would
104
+ // silently target the daemon container's filesystem instead of the
105
+ // intended host client. Skips only when transport is explicitly
106
+ // host-proxy-capable (macos), where local-fs fallback IS the intended
107
+ // offline behavior — a stale target_client_id auto-filled from a prior
108
+ // cross-client turn is silently ignored on those turns.
109
+ // Note: this scoping deliberately differs from host_bash
110
+ // (host-shell.ts:239-247), which rejects unconditionally for any
111
+ // stale target_client_id regardless of transport.
112
+ if (
113
+ targetClientId != null &&
114
+ !HostFileProxy.instance.isAvailable() &&
115
+ (transportInterface == null || !supportsHostProxy(transportInterface))
116
+ ) {
117
+ return {
118
+ content: `Error: target client "${targetClientId}" is no longer connected. The specified client may have disconnected since the tool was called. Run \`assistant clients list --capability host_file\` to see currently connected clients.`,
119
+ isError: true,
120
+ };
121
+ }
122
+
83
123
  // Proxy to connected client for execution on the user's machine
84
124
  // when a capable client is available (managed/cloud-hosted mode),
85
125
  // including image reads that need the host filesystem view.
@@ -94,6 +134,8 @@ class HostFileReadTool implements Tool {
94
134
  },
95
135
  context.conversationId,
96
136
  context.signal,
137
+ targetClientId,
138
+ context.sourceActorPrincipalId,
97
139
  );
98
140
  }
99
141
 
@@ -31,6 +31,15 @@ mock.module("../../daemon/host-transfer-proxy.js", () => ({
31
31
  },
32
32
  }));
33
33
 
34
+ // Mirror read/write/edit test files: stub the event hub so the multi-client
35
+ // guard at line ~100 of transfer.ts is exercised against an isolated stub
36
+ // rather than the live process-wide singleton.
37
+ mock.module("../../runtime/assistant-event-hub.js", () => ({
38
+ assistantEventHub: {
39
+ listClientsByCapability: () => [],
40
+ },
41
+ }));
42
+
34
43
  const { hostFileTransferTool } = await import("./transfer.js");
35
44
 
36
45
  const testDirs: string[] = [];
@@ -50,8 +59,16 @@ function makeTempDir(): string {
50
59
  return dir;
51
60
  }
52
61
 
53
- function makeContext(workingDir: string): ToolContext {
54
- return { workingDir, conversationId: "test-conv", trustClass: "guardian" };
62
+ function makeContext(
63
+ workingDir: string,
64
+ transportInterface?: ToolContext["transportInterface"],
65
+ ): ToolContext {
66
+ return {
67
+ workingDir,
68
+ conversationId: "test-conv",
69
+ trustClass: "guardian",
70
+ transportInterface,
71
+ };
55
72
  }
56
73
 
57
74
  // ---------------------------------------------------------------------------
@@ -269,3 +286,111 @@ describe("host_file_transfer managed mode", () => {
269
286
  expect(toSandboxCalls.length).toBe(0);
270
287
  });
271
288
  });
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // Cross-client guard tests
292
+ // ---------------------------------------------------------------------------
293
+
294
+ describe("host_file_transfer cross-client guards", () => {
295
+ test("returns 'no client' error on web transport when proxy unavailable and no targetClientId", async () => {
296
+ // mockProxyAvailable defaults to false.
297
+ const workingDir = makeTempDir();
298
+ const srcDir = makeTempDir();
299
+ const srcFile = join(srcDir, "source.txt");
300
+ writeFileSync(srcFile, "content");
301
+
302
+ const result = await hostFileTransferTool.execute(
303
+ {
304
+ source_path: srcFile,
305
+ dest_path: "out.txt",
306
+ direction: "to_sandbox",
307
+ },
308
+ makeContext(workingDir, "web"),
309
+ );
310
+
311
+ expect(result.isError).toBe(true);
312
+ expect(result.content).toContain(
313
+ "no client with host_file capability is connected",
314
+ );
315
+ expect(toSandboxCalls.length).toBe(0);
316
+ });
317
+
318
+ test("returns 'specified client disconnected' error when targetClientId set but proxy unavailable on web", async () => {
319
+ const workingDir = makeTempDir();
320
+ const srcDir = makeTempDir();
321
+ const srcFile = join(srcDir, "source.txt");
322
+ writeFileSync(srcFile, "content");
323
+
324
+ const result = await hostFileTransferTool.execute(
325
+ {
326
+ source_path: srcFile,
327
+ dest_path: "out.txt",
328
+ direction: "to_sandbox",
329
+ target_client_id: "abc-123",
330
+ },
331
+ makeContext(workingDir, "web"),
332
+ );
333
+
334
+ expect(result.isError).toBe(true);
335
+ expect(result.content).toContain(
336
+ 'target client "abc-123" is no longer connected',
337
+ );
338
+ expect(toSandboxCalls.length).toBe(0);
339
+ });
340
+
341
+ test("rejects when target_client_id is set but transport metadata is missing (legacy/backwards-compat path)", async () => {
342
+ const workingDir = makeTempDir();
343
+ const srcDir = makeTempDir();
344
+ const srcFile = join(srcDir, "source.txt");
345
+ writeFileSync(srcFile, "content");
346
+ const destFile = join(workingDir, "should-not-exist.txt");
347
+
348
+ const result = await hostFileTransferTool.execute(
349
+ {
350
+ source_path: srcFile,
351
+ dest_path: destFile,
352
+ direction: "to_sandbox",
353
+ target_client_id: "abc-123",
354
+ },
355
+ // transportInterface intentionally omitted (legacy callers).
356
+ makeContext(workingDir),
357
+ );
358
+
359
+ // Without transport metadata, falling through to executeLocal would
360
+ // silently target the daemon container. The guard fires for undefined
361
+ // transport AND non-host-proxy transports — only macos turns skip it.
362
+ expect(result.isError).toBe(true);
363
+ expect(result.content).toContain(
364
+ 'target client "abc-123" is no longer connected',
365
+ );
366
+ expect(existsSync(destFile)).toBe(false);
367
+ expect(toSandboxCalls.length).toBe(0);
368
+ });
369
+
370
+ test("does NOT reject on macos transport with a stale target_client_id when proxy unavailable (regression: Devin-flagged scope drift fix)", async () => {
371
+ const workingDir = makeTempDir();
372
+ const srcDir = makeTempDir();
373
+ const srcFile = join(srcDir, "source.txt");
374
+ writeFileSync(srcFile, "content");
375
+ const destFile = join(workingDir, "stale-target.txt");
376
+
377
+ const result = await hostFileTransferTool.execute(
378
+ {
379
+ source_path: srcFile,
380
+ dest_path: destFile,
381
+ direction: "to_sandbox",
382
+ target_client_id: "stale-mac",
383
+ },
384
+ makeContext(workingDir, "macos"),
385
+ );
386
+
387
+ // The disconnected-target guard is scoped to non-host-proxy transports
388
+ // (!supportsHostProxy). On macos, a stale target_client_id auto-filled
389
+ // from a prior cross-client turn must be silently ignored and the local
390
+ // copy must succeed, NOT reject with "target client ... is no longer
391
+ // connected" or the older "target_client_id was specified but no host
392
+ // client is available" message.
393
+ expect(result.isError).toBe(false);
394
+ expect(existsSync(destFile)).toBe(true);
395
+ });
396
+ });
@@ -93,7 +93,8 @@ class HostFileTransferTool implements Tool {
93
93
  const overwrite = input.overwrite === true;
94
94
 
95
95
  const targetClientId =
96
- typeof input.target_client_id === "string" && input.target_client_id !== ""
96
+ typeof input.target_client_id === "string" &&
97
+ input.target_client_id !== ""
97
98
  ? input.target_client_id
98
99
  : undefined;
99
100
 
@@ -103,7 +104,51 @@ class HostFileTransferTool implements Tool {
103
104
  !supportsHostProxy(context.transportInterface) &&
104
105
  assistantEventHub.listClientsByCapability("host_file").length > 1
105
106
  ) {
106
- return { content: `Error: multiple clients support host_file. Specify which client to use with \`target_client_id\`. Run \`assistant clients list --capability host_file\` to see client IDs and labels.`, isError: true };
107
+ return {
108
+ content: `Error: multiple clients support host_file. Specify which client to use with \`target_client_id\`. Run \`assistant clients list --capability host_file\` to see client IDs and labels.`,
109
+ isError: true,
110
+ };
111
+ }
112
+
113
+ // Guard: non-host-proxy interfaces with no capable clients connected.
114
+ // Without this guard, a web/ios turn whose host_file client has
115
+ // disconnected since projection would fall through to executeLocal
116
+ // below and act on the daemon container's filesystem instead of
117
+ // the user's host machine.
118
+ if (
119
+ targetClientId == null &&
120
+ context.transportInterface != null &&
121
+ !supportsHostProxy(context.transportInterface) &&
122
+ !HostTransferProxy.instance.isAvailable()
123
+ ) {
124
+ return {
125
+ content:
126
+ "Error: no client with host_file capability is connected. Connect a macOS client to use host_file from a non-desktop interface.",
127
+ isError: true,
128
+ };
129
+ }
130
+
131
+ // Guard: explicit targetClientId provided but proxy is unavailable.
132
+ // Fires on non-host-proxy transports (web, ios) AND on legacy callers
133
+ // without transport metadata, where falling through to executeLocal
134
+ // would silently target the daemon container's filesystem instead of
135
+ // the intended host client. Skips only when transport is explicitly
136
+ // host-proxy-capable (macos), where local-fs fallback IS the intended
137
+ // offline behavior — a stale target_client_id auto-filled from a prior
138
+ // cross-client turn is silently ignored on those turns.
139
+ // Note: this scoping deliberately differs from host_bash
140
+ // (host-shell.ts:239-247), which rejects unconditionally for any
141
+ // stale target_client_id regardless of transport.
142
+ if (
143
+ targetClientId != null &&
144
+ !HostTransferProxy.instance.isAvailable() &&
145
+ (context.transportInterface == null ||
146
+ !supportsHostProxy(context.transportInterface))
147
+ ) {
148
+ return {
149
+ content: `Error: target client "${targetClientId}" is no longer connected. The specified client may have disconnected since the tool was called. Run \`assistant clients list --capability host_file\` to see currently connected clients.`,
150
+ isError: true,
151
+ };
107
152
  }
108
153
 
109
154
  // Validate that host-side paths are absolute.
@@ -136,7 +181,9 @@ class HostFileTransferTool implements Tool {
136
181
 
137
182
  let resolvedDestPath = destPath;
138
183
  if (direction === "to_sandbox") {
139
- const pathCheck = sandboxPolicy(destPath, context.workingDir, { mustExist: false });
184
+ const pathCheck = sandboxPolicy(destPath, context.workingDir, {
185
+ mustExist: false,
186
+ });
140
187
  if (!pathCheck.ok) {
141
188
  return {
142
189
  content: `Invalid destination path: ${pathCheck.error}`,
@@ -158,6 +205,7 @@ class HostFileTransferTool implements Tool {
158
205
  targetClientId,
159
206
  },
160
207
  context.signal,
208
+ context.sourceActorPrincipalId,
161
209
  );
162
210
  }
163
211
  return HostTransferProxy.instance.requestToSandbox(
@@ -169,17 +217,14 @@ class HostFileTransferTool implements Tool {
169
217
  targetClientId,
170
218
  },
171
219
  context.signal,
220
+ context.sourceActorPrincipalId,
172
221
  );
173
222
  }
174
223
 
175
- if (targetClientId != null) {
176
- return {
177
- content: `Error: target_client_id '${targetClientId}' was specified but no host client is available. Ensure the client is connected.`,
178
- isError: true,
179
- };
180
- }
181
-
182
- // Local mode: direct filesystem copy.
224
+ // Local mode: direct filesystem copy. The non-host-proxy + stale
225
+ // target_client_id case is caught by the scoped guard at the top of
226
+ // execute(); on macos a stale target_client_id is silently ignored
227
+ // here, matching the read/write/edit pattern.
183
228
  return this.executeLocal(resolvedSourcePath, resolvedDestPath, overwrite);
184
229
  }
185
230
 
@@ -0,0 +1,134 @@
1
+ import { existsSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, mock, test } from "bun:test";
5
+
6
+ import type { ToolContext } from "../types.js";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Singleton mocks — must precede the tool import so bun's module mock applies.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ let mockProxyAvailable = false;
13
+
14
+ mock.module("../../daemon/host-file-proxy.js", () => ({
15
+ HostFileProxy: {
16
+ get instance() {
17
+ return {
18
+ isAvailable: () => mockProxyAvailable,
19
+ request: () => Promise.resolve({ content: "ok", isError: false }),
20
+ };
21
+ },
22
+ },
23
+ }));
24
+
25
+ mock.module("../../runtime/assistant-event-hub.js", () => ({
26
+ assistantEventHub: {
27
+ listClientsByCapability: () => [],
28
+ },
29
+ }));
30
+
31
+ const { hostFileWriteTool } = await import("./write.js");
32
+
33
+ const testDirs: string[] = [];
34
+
35
+ afterEach(() => {
36
+ mockProxyAvailable = false;
37
+ for (const dir of testDirs.splice(0)) {
38
+ rmSync(dir, { recursive: true, force: true });
39
+ }
40
+ });
41
+
42
+ function makeTempDir(): string {
43
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "host-write-test-")));
44
+ testDirs.push(dir);
45
+ return dir;
46
+ }
47
+
48
+ function makeContext(
49
+ workingDir: string,
50
+ transportInterface: ToolContext["transportInterface"],
51
+ ): ToolContext {
52
+ return {
53
+ workingDir,
54
+ conversationId: "test-conv",
55
+ trustClass: "guardian",
56
+ transportInterface,
57
+ };
58
+ }
59
+
60
+ describe("host_file_write cross-client guards", () => {
61
+ test("returns 'no client' error on web transport when proxy unavailable and no targetClientId", async () => {
62
+ const workingDir = makeTempDir();
63
+ const result = await hostFileWriteTool.execute(
64
+ { path: "/some/host/path.txt", content: "hello" },
65
+ makeContext(workingDir, "web"),
66
+ );
67
+ expect(result.isError).toBe(true);
68
+ expect(result.content).toContain(
69
+ "no client with host_file capability is connected",
70
+ );
71
+ });
72
+
73
+ test("returns 'specified client disconnected' error when targetClientId set but proxy unavailable on web", async () => {
74
+ const workingDir = makeTempDir();
75
+ const result = await hostFileWriteTool.execute(
76
+ {
77
+ path: "/some/host/path.txt",
78
+ content: "hello",
79
+ target_client_id: "abc-123",
80
+ },
81
+ makeContext(workingDir, "web"),
82
+ );
83
+ expect(result.isError).toBe(true);
84
+ expect(result.content).toContain(
85
+ 'target client "abc-123" is no longer connected',
86
+ );
87
+ });
88
+
89
+ test("falls through to local fs on macos transport when proxy unavailable", async () => {
90
+ const workingDir = makeTempDir();
91
+ const destFile = join(workingDir, "out.txt");
92
+ const result = await hostFileWriteTool.execute(
93
+ { path: destFile, content: "hello" },
94
+ makeContext(workingDir, "macos"),
95
+ );
96
+ // Proves the guard did NOT fire on macOS — local write succeeded.
97
+ expect(result.isError).toBe(false);
98
+ expect(existsSync(destFile)).toBe(true);
99
+ });
100
+
101
+ test("does NOT reject on macos transport with a stale target_client_id when proxy unavailable (regression: P2 fix)", async () => {
102
+ const workingDir = makeTempDir();
103
+ const destFile = join(workingDir, "stale-target.txt");
104
+ const result = await hostFileWriteTool.execute(
105
+ { path: destFile, content: "hello", target_client_id: "stale-mac" },
106
+ makeContext(workingDir, "macos"),
107
+ );
108
+ // The disconnected-target guard is scoped to non-host-proxy transports
109
+ // (!supportsHostProxy). On macos, a stale target_client_id auto-filled
110
+ // from a prior cross-client turn must be silently ignored and the local
111
+ // write must succeed, NOT reject with "target client ... is no longer
112
+ // connected".
113
+ expect(result.isError).toBe(false);
114
+ expect(existsSync(destFile)).toBe(true);
115
+ });
116
+
117
+ test("rejects when target_client_id is set but transport metadata is missing (legacy/backwards-compat path)", async () => {
118
+ const workingDir = makeTempDir();
119
+ const destFile = join(workingDir, "should-not-exist.txt");
120
+ const result = await hostFileWriteTool.execute(
121
+ { path: destFile, content: "hello", target_client_id: "abc-123" },
122
+ // transportInterface intentionally undefined (legacy callers).
123
+ makeContext(workingDir, undefined),
124
+ );
125
+ // Without transport metadata, falling through to local fs would
126
+ // silently target the daemon container. The guard fires for undefined
127
+ // transport AND non-host-proxy transports — only macos turns skip it.
128
+ expect(result.isError).toBe(true);
129
+ expect(result.content).toContain(
130
+ 'target client "abc-123" is no longer connected',
131
+ );
132
+ expect(existsSync(destFile)).toBe(false);
133
+ });
134
+ });
@@ -62,7 +62,8 @@ class HostFileWriteTool implements Tool {
62
62
  }
63
63
 
64
64
  const targetClientId =
65
- typeof input.target_client_id === "string" && input.target_client_id !== ""
65
+ typeof input.target_client_id === "string" &&
66
+ input.target_client_id !== ""
66
67
  ? input.target_client_id
67
68
  : undefined;
68
69
 
@@ -79,6 +80,45 @@ class HostFileWriteTool implements Tool {
79
80
  };
80
81
  }
81
82
 
83
+ // Guard: non-host-proxy interfaces with no capable clients connected.
84
+ // Without this guard, the request would fall through to local
85
+ // FileSystemOps below and read the daemon container's filesystem
86
+ // instead of the user's host machine.
87
+ if (
88
+ targetClientId == null &&
89
+ transportInterface != null &&
90
+ !supportsHostProxy(transportInterface) &&
91
+ !HostFileProxy.instance.isAvailable()
92
+ ) {
93
+ return {
94
+ content:
95
+ "Error: no client with host_file capability is connected. Connect a macOS client to use host_file from a non-desktop interface.",
96
+ isError: true,
97
+ };
98
+ }
99
+
100
+ // Guard: explicit targetClientId provided but proxy is unavailable.
101
+ // Fires on non-host-proxy transports (web, ios) AND on legacy callers
102
+ // without transport metadata, where falling through to local fs would
103
+ // silently target the daemon container's filesystem instead of the
104
+ // intended host client. Skips only when transport is explicitly
105
+ // host-proxy-capable (macos), where local-fs fallback IS the intended
106
+ // offline behavior — a stale target_client_id auto-filled from a prior
107
+ // cross-client turn is silently ignored on those turns.
108
+ // Note: this scoping deliberately differs from host_bash
109
+ // (host-shell.ts:239-247), which rejects unconditionally for any
110
+ // stale target_client_id regardless of transport.
111
+ if (
112
+ targetClientId != null &&
113
+ !HostFileProxy.instance.isAvailable() &&
114
+ (transportInterface == null || !supportsHostProxy(transportInterface))
115
+ ) {
116
+ return {
117
+ content: `Error: target client "${targetClientId}" is no longer connected. The specified client may have disconnected since the tool was called. Run \`assistant clients list --capability host_file\` to see currently connected clients.`,
118
+ isError: true,
119
+ };
120
+ }
121
+
82
122
  // Proxy to connected client for execution on the user's machine
83
123
  // when a capable client is available (managed/cloud-hosted mode).
84
124
  if (HostFileProxy.instance.isAvailable()) {
@@ -91,6 +131,8 @@ class HostFileWriteTool implements Tool {
91
131
  },
92
132
  context.conversationId,
93
133
  context.signal,
134
+ targetClientId,
135
+ context.sourceActorPrincipalId,
94
136
  );
95
137
  }
96
138