@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,163 @@
1
+ import type { InterfaceId } from "../channels/types.js";
2
+ import type { LLMCallSite } from "../config/schemas/llm.js";
3
+ import type { DiskPressureStatus } from "./disk-pressure-guard.js";
4
+ import type { ConversationType } from "./message-types/shared.js";
5
+ import type { TrustContext } from "./trust-context.js";
6
+
7
+ export type DiskPressureCleanupReason = "local-owner" | "guardian";
8
+
9
+ export type DiskPressureBlockReason =
10
+ | "background"
11
+ | "trusted-contact"
12
+ | "non-guardian"
13
+ | "unknown-remote";
14
+
15
+ export type DiskPressureTurnPolicyDecision =
16
+ | { action: "allow-normal" }
17
+ | { action: "allow-cleanup-mode"; reason: DiskPressureCleanupReason }
18
+ | { action: "block"; reason: DiskPressureBlockReason };
19
+
20
+ export type DiskPressureTurnTrustClass =
21
+ | TrustContext["trustClass"]
22
+ | "non_guardian"
23
+ | "non-guardian"
24
+ | (string & {});
25
+
26
+ export interface DiskPressureTurnTrustContext {
27
+ sourceChannel?: TrustContext["sourceChannel"] | (string & {});
28
+ trustClass?: DiskPressureTurnTrustClass;
29
+ }
30
+
31
+ export interface DiskPressureTurnMetadata {
32
+ conversationType?: ConversationType | (string & {}) | null;
33
+ conversationGroupId?: string | null;
34
+ conversationSource?: string | null;
35
+ callSite?: LLMCallSite | (string & {}) | null;
36
+ isInteractive?: boolean | null;
37
+ sourceChannel?: TrustContext["sourceChannel"] | (string & {}) | null;
38
+ sourceInterface?: InterfaceId | "vellum" | (string & {}) | null;
39
+ trustContext?: DiskPressureTurnTrustContext | null;
40
+ isDirectWake?: boolean | null;
41
+ }
42
+
43
+ const BACKGROUND_CONVERSATION_TYPES = new Set(["background", "scheduled"]);
44
+ const BACKGROUND_GROUP_IDS = new Set(["system:background", "system:scheduled"]);
45
+ const BACKGROUND_SOURCES = new Set([
46
+ "auto-analysis",
47
+ "background",
48
+ "compaction",
49
+ "direct",
50
+ "filing",
51
+ "heartbeat",
52
+ "memory",
53
+ "notification",
54
+ "proactive-artifact",
55
+ "reminder",
56
+ "schedule",
57
+ "task",
58
+ "update-bulletin",
59
+ ]);
60
+ const LOCAL_OWNER_INTERFACES = new Set(["macos", "web", "vellum", "cli"]);
61
+
62
+ export function classifyDiskPressureTurnPolicy(
63
+ status: DiskPressureStatus,
64
+ metadata: DiskPressureTurnMetadata,
65
+ ): DiskPressureTurnPolicyDecision {
66
+ if (!status.enabled || !status.locked || status.overrideActive) {
67
+ return { action: "allow-normal" };
68
+ }
69
+
70
+ if (!status.effectivelyLocked) {
71
+ return { action: "allow-normal" };
72
+ }
73
+
74
+ if (isBackgroundTurn(metadata)) {
75
+ return { action: "block", reason: "background" };
76
+ }
77
+
78
+ const trustClass = metadata.trustContext?.trustClass;
79
+ if (trustClass === "guardian") {
80
+ return { action: "allow-cleanup-mode", reason: "guardian" };
81
+ }
82
+
83
+ if (trustClass === "trusted_contact") {
84
+ return { action: "block", reason: "trusted-contact" };
85
+ }
86
+
87
+ if (isNonGuardianTrustClass(trustClass)) {
88
+ return { action: "block", reason: "non-guardian" };
89
+ }
90
+
91
+ if (trustClass === "unknown") {
92
+ return { action: "block", reason: "unknown-remote" };
93
+ }
94
+
95
+ if (trustClass !== undefined) {
96
+ return { action: "block", reason: "non-guardian" };
97
+ }
98
+
99
+ if (isLocalOwnerTurnWithoutTrust(metadata)) {
100
+ return { action: "allow-cleanup-mode", reason: "local-owner" };
101
+ }
102
+
103
+ return { action: "block", reason: "unknown-remote" };
104
+ }
105
+
106
+ function isBackgroundTurn(metadata: DiskPressureTurnMetadata): boolean {
107
+ if (isExplicitLocalOwnerCleanupTurn(metadata)) return false;
108
+ if (metadata.isDirectWake) return true;
109
+ if (metadata.callSite != null && metadata.callSite !== "mainAgent") {
110
+ return true;
111
+ }
112
+ if (
113
+ metadata.conversationType != null &&
114
+ BACKGROUND_CONVERSATION_TYPES.has(metadata.conversationType)
115
+ ) {
116
+ return true;
117
+ }
118
+ if (
119
+ metadata.conversationGroupId != null &&
120
+ BACKGROUND_GROUP_IDS.has(metadata.conversationGroupId)
121
+ ) {
122
+ return true;
123
+ }
124
+ return (
125
+ metadata.conversationSource != null &&
126
+ BACKGROUND_SOURCES.has(metadata.conversationSource)
127
+ );
128
+ }
129
+
130
+ function isNonGuardianTrustClass(
131
+ trustClass: DiskPressureTurnTrustClass | undefined,
132
+ ): boolean {
133
+ return trustClass === "non_guardian" || trustClass === "non-guardian";
134
+ }
135
+
136
+ function isLocalOwnerTurnWithoutTrust(
137
+ metadata: DiskPressureTurnMetadata,
138
+ ): boolean {
139
+ if (metadata.trustContext != null) return false;
140
+
141
+ const channel = metadata.sourceChannel;
142
+ const sourceInterface = metadata.sourceInterface;
143
+ if (channel !== "vellum" || sourceInterface == null) return false;
144
+ return LOCAL_OWNER_INTERFACES.has(sourceInterface);
145
+ }
146
+
147
+ function isExplicitLocalOwnerCleanupTurn(
148
+ metadata: DiskPressureTurnMetadata,
149
+ ): boolean {
150
+ if (metadata.isDirectWake !== true) return false;
151
+ const sourceInterface = metadata.sourceInterface;
152
+ if (
153
+ metadata.sourceChannel !== "vellum" ||
154
+ sourceInterface == null ||
155
+ !LOCAL_OWNER_INTERFACES.has(sourceInterface)
156
+ ) {
157
+ return false;
158
+ }
159
+ return (
160
+ metadata.trustContext == null ||
161
+ metadata.trustContext.trustClass === "guardian"
162
+ );
163
+ }
@@ -63,6 +63,20 @@ export interface HistoryToolCall {
63
63
  approvalReason?: string;
64
64
  /** Snapshot of the auto-approve threshold at execution time. */
65
65
  riskThreshold?: string;
66
+ /**
67
+ * Display-only regex ladder for the rule editor (narrowest → broadest).
68
+ * Persisted on tool_use blocks by `annotatePersistedAssistantMessage` so
69
+ * historical chips render the same ladder as live tool_result events.
70
+ */
71
+ riskScopeOptions?: Array<{ pattern: string; label: string }>;
72
+ /** Minimatch save patterns for the rule editor (narrowest → broadest). */
73
+ riskAllowlistOptions?: Array<{
74
+ label: string;
75
+ description: string;
76
+ pattern: string;
77
+ }>;
78
+ /** Directory scope ladder for the rule editor. */
79
+ riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
66
80
  }
67
81
 
68
82
  export interface HistorySurface {
@@ -129,7 +143,6 @@ export interface ConversationCreateOptions {
129
143
  isInteractive?: boolean;
130
144
  /** Slack-only non-persisted notice injected into the active model turn. */
131
145
  slackRuntimeContextNotice?: string;
132
- memoryScopeId?: string;
133
146
  /** Channel command intent metadata (e.g. Telegram /start). */
134
147
  commandIntent?: { type: string; payload?: string; languageCode?: string };
135
148
 
@@ -369,6 +382,18 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
369
382
  entry.approvalReason = block._approvalReason;
370
383
  if (typeof block._riskThreshold === "string")
371
384
  entry.riskThreshold = block._riskThreshold;
385
+ // Read back the 3 risk-option arrays persisted by
386
+ // `annotatePersistedAssistantMessage`. Validate the array shape only
387
+ // — element shapes are best-effort (we trust our own writer).
388
+ if (Array.isArray(block._riskScopeOptions))
389
+ entry.riskScopeOptions =
390
+ block._riskScopeOptions as HistoryToolCall["riskScopeOptions"];
391
+ if (Array.isArray(block._riskAllowlistOptions))
392
+ entry.riskAllowlistOptions =
393
+ block._riskAllowlistOptions as HistoryToolCall["riskAllowlistOptions"];
394
+ if (Array.isArray(block._riskDirectoryScopeOptions))
395
+ entry.riskDirectoryScopeOptions =
396
+ block._riskDirectoryScopeOptions as HistoryToolCall["riskDirectoryScopeOptions"];
372
397
  toolCalls.push(entry);
373
398
  if (id) pendingToolUses.set(id, entry);
374
399
  contentOrder.push(`tool:${toolCalls.length - 1}`);
@@ -430,7 +430,7 @@ async function listSkillsWithCatalog(): Promise<SlimSkillResponse[]> {
430
430
 
431
431
  // ─── Filtered skill listing ──────────────────────────────────────────────────
432
432
 
433
- export interface SkillListFilter {
433
+ interface SkillListFilter {
434
434
  origin?: string;
435
435
  kind?: string;
436
436
  q?: string;
@@ -698,7 +698,6 @@ export async function getSkill(
698
698
  // depends on `catalog-cache.ts`, which would otherwise be reachable via this
699
699
  // handler module). Re-exported here so handlers can import it alongside
700
700
  // the other skill handler exports.
701
- export type { SkillFileEntry } from "../../skills/catalog-files.js";
702
701
 
703
702
  /**
704
703
  * Returns true if `filePath` is a symlink whose resolved real path escapes
@@ -1428,7 +1427,7 @@ export async function inspectSkill(
1428
1427
  }
1429
1428
  }
1430
1429
 
1431
- export interface DraftResult {
1430
+ interface DraftResult {
1432
1431
  success: boolean;
1433
1432
  draft?: {
1434
1433
  skillId: string;
@@ -1583,7 +1582,7 @@ export async function draftSkill(params: {
1583
1582
  }
1584
1583
  }
1585
1584
 
1586
- export interface CreateSkillParams {
1585
+ interface CreateSkillParams {
1587
1586
  skillId: string;
1588
1587
  name: string;
1589
1588
  description: string;
@@ -11,13 +11,22 @@
11
11
  * (PNG-hash loop guard) and the result-payload → ToolExecutionResult
12
12
  * translation on top.
13
13
  *
14
- * **Singleton lock.** Only one conversation may hold an active app-control
15
- * session at a time. The lock is module-level (`activeAppControlConversationId`)
16
- * because a session targets the user's actual desktop application, which
17
- * is a host-wide resource. The lock is acquired on a successful
18
- * `app_control_start` and released when the owning proxy's `dispose()`
19
- * fires. A second conversation that calls `start` while the lock is held
20
- * receives an `isError: true` tool result naming the holding conversation.
14
+ * **Session lock.** Only one conversation may hold an active app-control
15
+ * session at a time, and that session is bound to a specific target app.
16
+ * The lock is module-level (`activeAppControlSession`) because the session
17
+ * targets the user's actual desktop application, which is a host-wide
18
+ * resource. It is acquired on a successful `app_control_start` (storing
19
+ * `(conversationId, app)`) and released when the owning proxy's
20
+ * `dispose()` fires.
21
+ *
22
+ * `app_control_start` is the only tool that can acquire the lock — the
23
+ * user's medium-risk approval at start time is the consent boundary. All
24
+ * other tools (observe / press / combo / sequence / type / click / drag)
25
+ * require the calling conversation to own an active session targeting the
26
+ * same `app`; otherwise the call is rejected before any host dispatch.
27
+ * This prevents prompt-injected tool calls from sending raw input to
28
+ * arbitrary apps without the user having approved control of that
29
+ * specific app.
21
30
  *
22
31
  * **No step cap.** Unlike {@link HostCuProxy} which enforces a per-session
23
32
  * step ceiling via `loadConfig().maxStepsPerSession`, app-control sessions
@@ -51,36 +60,111 @@ const REQUEST_TIMEOUT_MS = 60 * 1000;
51
60
  const STUCK_REPEAT_THRESHOLD = 4;
52
61
 
53
62
  // ---------------------------------------------------------------------------
54
- // Tool name constants
63
+ // Module-level session lock
55
64
  // ---------------------------------------------------------------------------
56
- //
57
- // Kept here (rather than imported from PR 5's tool registrations) so the
58
- // proxy is independently testable. PR 5 must use these same string values.
59
-
60
- const TOOL_START = "app_control_start";
61
65
 
62
- // ---------------------------------------------------------------------------
63
- // Module-level singleton lock
64
- // ---------------------------------------------------------------------------
66
+ /**
67
+ * Active app-control session: the conversation that owns the lock and the
68
+ * `app` it was approved against. Set on a successful `app_control_start`;
69
+ * cleared by the owning proxy's `dispose()`.
70
+ */
71
+ export interface ActiveAppControlSession {
72
+ conversationId: string;
73
+ /**
74
+ * The exact `app` string the user approved at start time (bundle ID or
75
+ * process name — preserved as-is). Compared case-insensitively against
76
+ * the `app` of subsequent non-start tool calls.
77
+ */
78
+ app: string;
79
+ }
65
80
 
66
81
  /**
67
- * Conversation id that currently owns the active app-control session, or
68
- * `undefined` if no session is active. Set on a successful
69
- * `app_control_start`; cleared by the owning proxy's `dispose()`.
82
+ * Currently active session, or `undefined` when no session is held.
70
83
  *
71
84
  * Exported for test inspection only. Production code paths must not read
72
85
  * or mutate this directly — use the proxy methods.
73
86
  */
74
- let activeAppControlConversationId: string | undefined;
87
+ let activeAppControlSession: ActiveAppControlSession | undefined;
75
88
 
76
- /** Test-only helper: read current lock owner. */
77
- export function _getActiveAppControlConversationId(): string | undefined {
78
- return activeAppControlConversationId;
89
+ /** Test-only helper: read current session. */
90
+ export function _getActiveAppControlSession():
91
+ | ActiveAppControlSession
92
+ | undefined {
93
+ return activeAppControlSession;
79
94
  }
80
95
 
81
- /** Test-only helper: clear lock between test cases. */
82
- export function _resetActiveAppControlConversationId(): void {
83
- activeAppControlConversationId = undefined;
96
+ /** Test-only helper: clear session between test cases. */
97
+ export function _resetActiveAppControlSession(): void {
98
+ activeAppControlSession = undefined;
99
+ }
100
+
101
+ /**
102
+ * Test-only helper: prime the active session without a full `start` round-trip.
103
+ * Useful for tests that exercise non-start tool paths and don't need to
104
+ * verify the start flow itself.
105
+ */
106
+ export function _setActiveAppControlSession(
107
+ session: ActiveAppControlSession,
108
+ ): void {
109
+ activeAppControlSession = session;
110
+ }
111
+
112
+ /**
113
+ * Validate a non-start tool call against the active session. Returns a
114
+ * `ToolExecutionResult` (with `isError: true`) when the call should be
115
+ * rejected; returns `null` when the call is authorized to dispatch.
116
+ *
117
+ * `app` matching is case-insensitive (macOS bundle IDs are
118
+ * case-insensitive in practice) but strict on form: `"Safari"` and
119
+ * `"com.apple.Safari"` do not match — the user approved a specific string
120
+ * and substituting a different form requires a new approval.
121
+ */
122
+ function checkNonStartAuthorization(
123
+ input: HostAppControlInput,
124
+ conversationId: string,
125
+ ): ToolExecutionResult | null {
126
+ if (activeAppControlSession == null) {
127
+ return {
128
+ content:
129
+ "No app-control session is active. Call app_control_start to request " +
130
+ "user approval to control the target app, then retry.",
131
+ isError: true,
132
+ };
133
+ }
134
+ if (activeAppControlSession.conversationId !== conversationId) {
135
+ return {
136
+ content:
137
+ `Another conversation (${activeAppControlSession.conversationId}) currently ` +
138
+ `holds the app-control session. Wait for it to finish, or call ` +
139
+ `app_control_stop from that conversation first.`,
140
+ isError: true,
141
+ };
142
+ }
143
+ // `app` is required on every non-start variant of HostAppControlInput
144
+ // except `stop`, and `stop` short-circuits in conversation-surfaces and
145
+ // does not reach this method in production. A stop reaching here would
146
+ // be a defensive bug — surface it explicitly rather than dispatch.
147
+ const requestedApp = (input as { app?: string }).app;
148
+ if (requestedApp == null) {
149
+ return {
150
+ content:
151
+ "Tool input missing required 'app' field; cannot validate against " +
152
+ "the active app-control session.",
153
+ isError: true,
154
+ };
155
+ }
156
+ if (
157
+ requestedApp.toLowerCase() !== activeAppControlSession.app.toLowerCase()
158
+ ) {
159
+ return {
160
+ content:
161
+ `Active app-control session targets ${activeAppControlSession.app}; ` +
162
+ `cannot send actions to ${requestedApp}. Call app_control_stop and ` +
163
+ `app_control_start to switch apps.`,
164
+ isError: true,
165
+ };
166
+ }
167
+ return null;
84
168
  }
85
169
 
86
170
  // ---------------------------------------------------------------------------
@@ -91,7 +175,7 @@ export class HostAppControlProxy extends HostProxyBase<
91
175
  HostAppControlInput,
92
176
  HostAppControlResultPayload
93
177
  > {
94
- /** Conversation that owns this proxy instance. Used by `dispose()` to release the singleton lock only when this proxy is the holder. */
178
+ /** Conversation that owns this proxy instance. Used by `dispose()` to release the session lock only when this proxy is the holder. */
95
179
  private readonly conversationId: string;
96
180
 
97
181
  /** sha256 hex of the most recent observation's `pngBase64`, or undefined. */
@@ -143,21 +227,29 @@ export class HostAppControlProxy extends HostProxyBase<
143
227
  return { content: "Aborted", isError: true };
144
228
  }
145
229
 
146
- // Singleton-lock guard for `start`. Other tools assume a session
147
- // already exists and are not gated here.
148
- if (toolName === TOOL_START) {
230
+ // Authorization gate. `start` acquires the session lock (the user's
231
+ // medium-risk approval is the consent boundary); all other tools must
232
+ // belong to the active session and target the same `app`. Without this
233
+ // gate, prompt-injected calls would bypass the start-time approval and
234
+ // send raw input to arbitrary apps.
235
+ if (input.tool === "start") {
149
236
  if (
150
- activeAppControlConversationId != null &&
151
- activeAppControlConversationId !== conversationId
237
+ activeAppControlSession != null &&
238
+ activeAppControlSession.conversationId !== conversationId
152
239
  ) {
153
240
  return {
154
241
  content:
155
- `Another conversation (${activeAppControlConversationId}) currently holds the ` +
242
+ `Another conversation (${activeAppControlSession.conversationId}) currently holds the ` +
156
243
  `app-control session. Wait for it to finish, or call app_control_stop ` +
157
244
  `from that conversation first.`,
158
245
  isError: true,
159
246
  };
160
247
  }
248
+ } else {
249
+ const sessionError = checkNonStartAuthorization(input, conversationId);
250
+ if (sessionError != null) {
251
+ return sessionError;
252
+ }
161
253
  }
162
254
 
163
255
  try {
@@ -167,7 +259,7 @@ export class HostAppControlProxy extends HostProxyBase<
167
259
  conversationId,
168
260
  signal,
169
261
  );
170
- return this.handleSuccess(toolName, payload);
262
+ return this.handleSuccess(input, payload);
171
263
  } catch (err) {
172
264
  if (err instanceof HostProxyRequestError) {
173
265
  if (err.reason === "timeout") {
@@ -192,7 +284,7 @@ export class HostAppControlProxy extends HostProxyBase<
192
284
  // ---------------------------------------------------------------------------
193
285
 
194
286
  private handleSuccess(
195
- toolName: string,
287
+ input: HostAppControlInput,
196
288
  payload: HostAppControlResultPayload,
197
289
  ): ToolExecutionResult {
198
290
  // Update PNG-hash loop tracking only for the "running" state — other
@@ -212,9 +304,13 @@ export class HostAppControlProxy extends HostProxyBase<
212
304
  }
213
305
  }
214
306
 
215
- // Acquire the singleton lock on a successful `start`.
216
- if (toolName === TOOL_START && payload.state === "running") {
217
- activeAppControlConversationId = this.conversationId;
307
+ // Store the exact `app` form for validation against subsequent
308
+ // non-start tool calls.
309
+ if (input.tool === "start" && payload.state === "running") {
310
+ activeAppControlSession = {
311
+ conversationId: this.conversationId,
312
+ app: input.app,
313
+ };
218
314
  }
219
315
 
220
316
  return this.formatResult(payload, stuck);
@@ -281,13 +377,13 @@ export class HostAppControlProxy extends HostProxyBase<
281
377
  // ---------------------------------------------------------------------------
282
378
 
283
379
  /**
284
- * Reject pending requests via the base, then release the singleton lock
380
+ * Reject pending requests via the base, then release the session lock
285
381
  * if this proxy is the holder. Idempotent: safe to call multiple times.
286
382
  */
287
383
  override dispose(): void {
288
384
  super.dispose();
289
- if (activeAppControlConversationId === this.conversationId) {
290
- activeAppControlConversationId = undefined;
385
+ if (activeAppControlSession?.conversationId === this.conversationId) {
386
+ activeAppControlSession = undefined;
291
387
  }
292
388
  }
293
389
  }
@@ -5,6 +5,11 @@ import {
5
5
  assistantEventHub,
6
6
  broadcastMessage,
7
7
  } from "../runtime/assistant-event-hub.js";
8
+ import {
9
+ ambiguousSameUserError,
10
+ enforceSameActorOrErrorResult,
11
+ pickSameUserAutoResolve,
12
+ } from "../runtime/auth/same-actor.js";
8
13
  import * as pendingInteractions from "../runtime/pending-interactions.js";
9
14
  import { formatShellOutput } from "../tools/shared/shell-output.js";
10
15
  import type { ToolExecutionResult } from "../tools/types.js";
@@ -13,7 +18,6 @@ import { getLogger } from "../util/logger.js";
13
18
 
14
19
  const log = getLogger("host-bash-proxy");
15
20
 
16
-
17
21
  export class HostBashProxy {
18
22
  private static _instance: HostBashProxy | null = null;
19
23
 
@@ -62,14 +66,14 @@ export class HostBashProxy {
62
66
  },
63
67
  conversationId: string,
64
68
  signal?: AbortSignal,
69
+ // Principal ID of the actor on whose behalf this request is initiated.
70
+ sourceActorPrincipalId?: string,
65
71
  ): Promise<ToolExecutionResult> {
66
72
  if (signal?.aborted) {
67
73
  const result = formatShellOutput("", "Aborted", null, false, 0);
68
74
  return Promise.resolve(result);
69
75
  }
70
76
 
71
- const capableClients = assistantEventHub.listClientsByCapability("host_bash");
72
-
73
77
  let resolvedTargetClientId: string | undefined;
74
78
 
75
79
  if (input.targetClientId) {
@@ -81,14 +85,37 @@ export class HostBashProxy {
81
85
  });
82
86
  }
83
87
  resolvedTargetClientId = input.targetClientId;
84
- } else if (capableClients.length === 1) {
85
- // Auto-resolve when exactly one capable client is connected.
86
- resolvedTargetClientId = capableClients[0].clientId;
88
+ } else {
89
+ // Auto-resolve to the unique same-user client. Reject (rather than
90
+ // broadcast) when multiple same-user clients are connected so that
91
+ // a single targeted-style request cannot fan out across every one
92
+ // of the user's machines. Zero same-user matches falls through to
93
+ // the existing untargeted code path.
94
+ const resolved = pickSameUserAutoResolve({
95
+ hub: assistantEventHub,
96
+ capability: "host_bash",
97
+ sourceActorPrincipalId,
98
+ });
99
+ if (resolved.kind === "ambiguous") {
100
+ return Promise.resolve(ambiguousSameUserError("host_bash"));
101
+ }
102
+ resolvedTargetClientId =
103
+ resolved.kind === "match" ? resolved.clientId : undefined;
104
+ }
105
+
106
+ // Targeted requests must be bound to the same authenticated user as the
107
+ // target client. Fail closed at request time — before pendingInteractions
108
+ // registration and before broadcast — so a same-daemon caller cannot
109
+ // execute on another user's connected client.
110
+ if (resolvedTargetClientId != null) {
111
+ const rejection = enforceSameActorOrErrorResult({
112
+ hub: assistantEventHub,
113
+ sourceActorPrincipalId,
114
+ targetClientId: resolvedTargetClientId,
115
+ op: "host_bash",
116
+ });
117
+ if (rejection) return Promise.resolve(rejection);
87
118
  }
88
- // capableClients.length === 0 or > 1 without explicit target: resolvedTargetClientId
89
- // stays undefined and falls through to untargeted broadcast — the existing timeout/error
90
- // path handles the zero-client case, and multi-client ambiguity is enforced at the tool
91
- // executor layer (not here) once target_client_id is exposed in the tool schema.
92
119
 
93
120
  const requestId = uuid();
94
121
 
@@ -108,15 +135,7 @@ export class HostBashProxy {
108
135
  const timeoutMessage = resolvedTargetClientId
109
136
  ? `Host bash proxy timed out waiting for response from client ${resolvedTargetClientId}`
110
137
  : "Host bash proxy timed out waiting for client response";
111
- resolve(
112
- formatShellOutput(
113
- "",
114
- timeoutMessage,
115
- null,
116
- true,
117
- timeoutSec,
118
- ),
119
- );
138
+ resolve(formatShellOutput("", timeoutMessage, null, true, timeoutSec));
120
139
  }, proxyTimeoutSec * 1000);
121
140
 
122
141
  if (signal) {
@@ -147,11 +166,17 @@ export class HostBashProxy {
147
166
  pendingInteractions.register(requestId, {
148
167
  conversationId,
149
168
  kind: "host_bash",
150
- rpcResolve: resolve,
169
+ rpcResolve: resolve as (v: unknown) => void,
151
170
  rpcReject: reject,
152
171
  timer,
153
172
  detachAbort,
154
173
  targetClientId: resolvedTargetClientId,
174
+ targetActorPrincipalId:
175
+ resolvedTargetClientId != null
176
+ ? assistantEventHub.getActorPrincipalIdForClient(
177
+ resolvedTargetClientId,
178
+ )
179
+ : undefined,
155
180
  metadata: { timeoutSec },
156
181
  });
157
182
 
@@ -166,8 +191,8 @@ export class HostBashProxy {
166
191
  timeout_seconds: input.timeout_seconds,
167
192
  targetClientId: resolvedTargetClientId,
168
193
  ...(input.env && Object.keys(input.env).length > 0
169
- ? { env: input.env }
170
- : {}),
194
+ ? { env: input.env }
195
+ : {}),
171
196
  },
172
197
  conversationId,
173
198
  { targetClientId: resolvedTargetClientId },
@@ -135,7 +135,7 @@ export class HostBrowserProxy {
135
135
  pendingInteractions.register(requestId, {
136
136
  conversationId,
137
137
  kind: "host_browser",
138
- rpcResolve: resolve,
138
+ rpcResolve: resolve as (v: unknown) => void,
139
139
  rpcReject: reject,
140
140
  timer,
141
141
  detachAbort,