@vellumai/assistant 0.7.1 → 0.7.2

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 (535) hide show
  1. package/ARCHITECTURE.md +32 -49
  2. package/Dockerfile +1 -0
  3. package/README.md +1 -2
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +9 -3
  5. package/bun.lock +26 -26
  6. package/docs/architecture/security.md +20 -0
  7. package/docs/plugins.md +7 -9
  8. package/knip.json +1 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
  10. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +39 -1
  11. package/node_modules/@vellumai/gateway-client/src/types.ts +11 -0
  12. package/node_modules/@vellumai/service-contracts/package.json +2 -0
  13. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
  15. package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
  16. package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
  17. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
  18. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +9 -0
  19. package/node_modules/@vellumai/twilio-client/bun.lock +24 -0
  20. package/node_modules/@vellumai/twilio-client/package.json +18 -0
  21. package/node_modules/@vellumai/twilio-client/src/__tests__/twilio-client.test.ts +128 -0
  22. package/node_modules/@vellumai/twilio-client/src/index.ts +179 -0
  23. package/node_modules/@vellumai/twilio-client/tsconfig.json +20 -0
  24. package/openapi.yaml +565 -12
  25. package/package.json +6 -3
  26. package/src/__tests__/app-builder-tool-scripts.test.ts +3 -3
  27. package/src/__tests__/app-bundler.test.ts +170 -1
  28. package/src/__tests__/app-control-flow.test.ts +374 -0
  29. package/src/__tests__/app-control-no-global-cgevent.test.ts +98 -0
  30. package/src/__tests__/app-control-tool-schemas.test.ts +621 -0
  31. package/src/__tests__/app-executors.test.ts +30 -43
  32. package/src/__tests__/approval-routes-http.test.ts +23 -6
  33. package/src/__tests__/assistant-event-hub-machine-name.test.ts +146 -0
  34. package/src/__tests__/assistant-event-hub-targeted.test.ts +257 -0
  35. package/src/__tests__/assistant-event-hub.test.ts +109 -2
  36. package/src/__tests__/assistant-event.test.ts +10 -0
  37. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -2
  38. package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -7
  39. package/src/__tests__/background-shell-host-bash.test.ts +14 -15
  40. package/src/__tests__/bootstrap-turn-cleanup.test.ts +44 -0
  41. package/src/__tests__/btw-routes.test.ts +13 -4
  42. package/src/__tests__/call-controller.test.ts +49 -1
  43. package/src/__tests__/call-domain.test.ts +0 -2
  44. package/src/__tests__/call-routes-http.test.ts +0 -2
  45. package/src/__tests__/channel-readiness-service.test.ts +59 -1
  46. package/src/__tests__/checker.test.ts +3 -4
  47. package/src/__tests__/config-loader-backfill.test.ts +90 -155
  48. package/src/__tests__/config-loader-platform-defaults.test.ts +196 -0
  49. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  50. package/src/__tests__/config-set-platform-guard.test.ts +48 -4
  51. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +2 -2
  52. package/src/__tests__/config-watcher.test.ts +2 -2
  53. package/src/__tests__/conversation-app-control-instantiation.test.ts +392 -0
  54. package/src/__tests__/conversation-app-control-lifecycle.test.ts +237 -0
  55. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  56. package/src/__tests__/conversation-lifecycle.test.ts +36 -0
  57. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +283 -0
  58. package/src/__tests__/conversation-routes-disk-view.test.ts +6 -0
  59. package/src/__tests__/conversation-routes-guardian-reply.test.ts +120 -72
  60. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  61. package/src/__tests__/conversation-slash-commands.test.ts +0 -4
  62. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
  63. package/src/__tests__/conversation-surfaces-app-control.test.ts +317 -0
  64. package/src/__tests__/credential-execution-feature-gates.test.ts +5 -12
  65. package/src/__tests__/credential-execution-managed-contract.test.ts +3 -131
  66. package/src/__tests__/credentials-cli.test.ts +5 -12
  67. package/src/__tests__/cu-unified-flow.test.ts +185 -23
  68. package/src/__tests__/daemon-credential-client.test.ts +101 -19
  69. package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
  70. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  71. package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
  72. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -2
  73. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -2
  74. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -1
  75. package/src/__tests__/heartbeat-service.test.ts +718 -1
  76. package/src/__tests__/helpers/call-route-handler.ts +7 -1
  77. package/src/__tests__/host-app-control-proxy.test.ts +602 -0
  78. package/src/__tests__/host-app-control-routes.test.ts +263 -0
  79. package/src/__tests__/host-bash-proxy.test.ts +246 -47
  80. package/src/__tests__/host-bash-routes.test.ts +294 -0
  81. package/src/__tests__/host-browser-proxy.test.ts +24 -22
  82. package/src/__tests__/host-browser-routes.test.ts +39 -13
  83. package/src/__tests__/host-cu-proxy.test.ts +41 -52
  84. package/src/__tests__/host-cu-routes-targeted.test.ts +300 -0
  85. package/src/__tests__/host-file-edit-tool.test.ts +47 -1
  86. package/src/__tests__/host-file-proxy-targeted.test.ts +339 -0
  87. package/src/__tests__/host-file-proxy.test.ts +37 -43
  88. package/src/__tests__/host-file-read-tool.test.ts +17 -0
  89. package/src/__tests__/host-file-routes-targeted.test.ts +262 -0
  90. package/src/__tests__/host-file-write-tool.test.ts +42 -1
  91. package/src/__tests__/host-proxy-base.test.ts +312 -0
  92. package/src/__tests__/host-shell-tool.test.ts +22 -4
  93. package/src/__tests__/host-transfer-proxy-targeted.test.ts +583 -0
  94. package/src/__tests__/host-transfer-proxy.test.ts +121 -22
  95. package/src/__tests__/host-transfer-routes-targeted.test.ts +447 -0
  96. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  97. package/src/__tests__/identity-intro-cache.test.ts +29 -0
  98. package/src/__tests__/identity-routes.test.ts +103 -1
  99. package/src/__tests__/init-feature-flag-overrides.test.ts +26 -3
  100. package/src/__tests__/inline-command-runner.test.ts +0 -1
  101. package/src/__tests__/inline-skill-load-permissions.test.ts +5 -11
  102. package/src/__tests__/integration-status.test.ts +85 -5
  103. package/src/__tests__/intent-routing.test.ts +0 -1
  104. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +95 -5
  105. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +17 -0
  106. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
  107. package/src/__tests__/mcp-auth-routes.test.ts +197 -0
  108. package/src/__tests__/mcp-cli.test.ts +338 -2
  109. package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
  110. package/src/__tests__/migration-import-commit-http.test.ts +108 -2
  111. package/src/__tests__/mock-gateway-ipc.ts +1 -0
  112. package/src/__tests__/oauth-cli.test.ts +0 -2
  113. package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
  114. package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
  115. package/src/__tests__/platform-bash-auto-approve.test.ts +5 -9
  116. package/src/__tests__/prechat-onboarding-contract.test.ts +3 -1
  117. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  118. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  119. package/src/__tests__/public-ingress-urls.test.ts +97 -0
  120. package/src/__tests__/require-fresh-approval.test.ts +0 -1
  121. package/src/__tests__/retry-backoff.test.ts +87 -0
  122. package/src/__tests__/runtime-events-sse.test.ts +10 -6
  123. package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
  124. package/src/__tests__/schedule-retry.test.ts +715 -0
  125. package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
  126. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  127. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  128. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
  129. package/src/__tests__/skill-feature-flags.test.ts +43 -41
  130. package/src/__tests__/skill-load-feature-flag.test.ts +13 -14
  131. package/src/__tests__/skill-load-inline-command.test.ts +0 -51
  132. package/src/__tests__/skill-load-inline-includes.test.ts +0 -43
  133. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  134. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
  135. package/src/__tests__/slack-channel-config.test.ts +9 -14
  136. package/src/__tests__/system-prompt-ask-mode.test.ts +0 -1
  137. package/src/__tests__/system-prompt.test.ts +0 -1
  138. package/src/__tests__/telegram-config.test.ts +0 -1
  139. package/src/__tests__/test-preload.ts +8 -0
  140. package/src/__tests__/tool-approval-handler.test.ts +3 -4
  141. package/src/__tests__/tool-audit-listener.test.ts +48 -0
  142. package/src/__tests__/tool-execute-pipeline.test.ts +0 -1
  143. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  145. package/src/__tests__/tool-executor.test.ts +0 -1
  146. package/src/__tests__/twilio-config.test.ts +3 -16
  147. package/src/__tests__/twilio-routes.test.ts +3 -5
  148. package/src/__tests__/twilio-validation.test.ts +93 -0
  149. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -4
  150. package/src/__tests__/verification-control-plane-policy.test.ts +2 -4
  151. package/src/__tests__/voice-ingress-preflight.test.ts +19 -0
  152. package/src/__tests__/workspace-migration-006-services-config.test.ts +3 -2
  153. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -5
  154. package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
  155. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
  156. package/src/backup/__tests__/paths.test.ts +0 -22
  157. package/src/backup/__tests__/restore.test.ts +51 -151
  158. package/src/backup/paths.ts +2 -18
  159. package/src/backup/restore.ts +107 -231
  160. package/src/bundler/app-bundler.ts +51 -3
  161. package/src/calls/relay-server.ts +4 -44
  162. package/src/calls/twilio-config.ts +2 -17
  163. package/src/calls/twilio-rest.ts +33 -105
  164. package/src/calls/twilio-routes.ts +11 -12
  165. package/src/channels/types.ts +8 -7
  166. package/src/cli/commands/__tests__/backup.test.ts +6 -277
  167. package/src/cli/commands/__tests__/gateway.test.ts +288 -0
  168. package/src/cli/commands/__tests__/memory-v2.test.ts +4 -0
  169. package/src/cli/commands/__tests__/webhooks.test.ts +0 -1
  170. package/src/cli/commands/backup.ts +6 -331
  171. package/src/cli/commands/clients.ts +36 -37
  172. package/src/cli/commands/contacts.ts +73 -0
  173. package/src/cli/commands/conversations.ts +2 -5
  174. package/src/cli/commands/credentials.ts +15 -7
  175. package/src/cli/commands/domain.ts +66 -15
  176. package/src/cli/commands/gateway.ts +183 -0
  177. package/src/cli/commands/keys.ts +9 -6
  178. package/src/cli/commands/mcp.ts +116 -156
  179. package/src/cli/commands/memory-v2.ts +296 -1
  180. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  181. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -2
  182. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -2
  183. package/src/cli/commands/platform/__tests__/status.test.ts +13 -15
  184. package/src/cli/commands/platform/disconnect.ts +5 -4
  185. package/src/cli/commands/platform/index.ts +0 -18
  186. package/src/cli/lib/daemon-credential-client.ts +110 -28
  187. package/src/cli/program.ts +2 -0
  188. package/src/config/assistant-feature-flags.ts +67 -10
  189. package/src/config/bundled-skills/acp/SKILL.md +6 -0
  190. package/src/config/bundled-skills/acp/TOOLS.json +1 -22
  191. package/src/config/bundled-skills/app-builder/SKILL.md +14 -109
  192. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -28
  193. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -10
  194. package/src/config/bundled-skills/app-control/SKILL.md +75 -0
  195. package/src/config/bundled-skills/app-control/TOOLS.json +299 -0
  196. package/src/config/bundled-skills/app-control/tools/app-control-click.ts +12 -0
  197. package/src/config/bundled-skills/app-control/tools/app-control-combo.ts +12 -0
  198. package/src/config/bundled-skills/app-control/tools/app-control-drag.ts +12 -0
  199. package/src/config/bundled-skills/app-control/tools/app-control-observe.ts +12 -0
  200. package/src/config/bundled-skills/app-control/tools/app-control-press.ts +12 -0
  201. package/src/config/bundled-skills/app-control/tools/app-control-sequence.ts +12 -0
  202. package/src/config/bundled-skills/app-control/tools/app-control-start.ts +12 -0
  203. package/src/config/bundled-skills/app-control/tools/app-control-stop.ts +12 -0
  204. package/src/config/bundled-skills/app-control/tools/app-control-type.ts +12 -0
  205. package/src/config/bundled-skills/computer-use/SKILL.md +6 -0
  206. package/src/config/bundled-skills/computer-use/TOOLS.json +67 -43
  207. package/src/config/bundled-skills/contacts/TOOLS.json +0 -16
  208. package/src/config/bundled-skills/document/TOOLS.json +0 -8
  209. package/src/config/bundled-skills/followups/TOOLS.json +0 -12
  210. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  211. package/src/config/bundled-skills/image-studio/TOOLS.json +0 -4
  212. package/src/config/bundled-skills/media-processing/TOOLS.json +0 -24
  213. package/src/config/bundled-skills/messaging/TOOLS.json +0 -40
  214. package/src/config/bundled-skills/phone-calls/TOOLS.json +0 -12
  215. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +19 -4
  216. package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
  217. package/src/config/bundled-skills/schedule/TOOLS.json +14 -14
  218. package/src/config/bundled-skills/sequences/TOOLS.json +0 -36
  219. package/src/config/bundled-skills/settings/SKILL.md +4 -0
  220. package/src/config/bundled-skills/settings/TOOLS.json +0 -12
  221. package/src/config/bundled-skills/skill-management/SKILL.md +6 -0
  222. package/src/config/bundled-skills/skill-management/TOOLS.json +0 -8
  223. package/src/config/bundled-skills/subagent/SKILL.md +6 -2
  224. package/src/config/bundled-skills/subagent/TOOLS.json +0 -20
  225. package/src/config/bundled-skills/transcribe/SKILL.md +4 -0
  226. package/src/config/bundled-skills/transcribe/TOOLS.json +0 -4
  227. package/src/config/bundled-tool-registry.ts +21 -0
  228. package/src/config/env-registry.ts +0 -2
  229. package/src/config/env.ts +19 -12
  230. package/src/config/feature-flag-registry.json +21 -133
  231. package/src/config/loader.ts +73 -99
  232. package/src/config/sanitize-for-transfer.ts +2 -0
  233. package/src/config/schemas/__tests__/memory-lifecycle.test.ts +80 -0
  234. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -4
  235. package/src/config/schemas/calls.ts +0 -9
  236. package/src/config/schemas/heartbeat.ts +63 -0
  237. package/src/config/schemas/ingress.ts +10 -6
  238. package/src/config/schemas/llm.ts +5 -10
  239. package/src/config/schemas/memory-lifecycle.ts +77 -24
  240. package/src/config/schemas/memory-v2.ts +48 -4
  241. package/src/config/schemas/platform.ts +6 -0
  242. package/src/config/schemas/services.ts +1 -15
  243. package/src/config/schemas/skills.ts +0 -6
  244. package/src/config/seed-inference-profiles.ts +1 -1
  245. package/src/contacts/contact-store.ts +0 -30
  246. package/src/contacts/contacts-write.ts +0 -27
  247. package/src/context/window-manager.ts +1 -2
  248. package/src/credential-execution/feature-gates.ts +10 -10
  249. package/src/credential-execution/process-manager.ts +12 -41
  250. package/src/daemon/__tests__/conversation-tool-setup.test.ts +126 -5
  251. package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
  252. package/src/daemon/config-watcher.ts +4 -3
  253. package/src/daemon/conversation-agent-loop-handlers.ts +21 -3
  254. package/src/daemon/conversation-agent-loop.ts +32 -28
  255. package/src/daemon/conversation-lifecycle.ts +8 -1
  256. package/src/daemon/conversation-process.ts +16 -11
  257. package/src/daemon/conversation-runtime-assembly.ts +2 -2
  258. package/src/daemon/conversation-surfaces.ts +125 -4
  259. package/src/daemon/conversation-tool-setup.ts +16 -55
  260. package/src/daemon/conversation.ts +21 -2
  261. package/src/daemon/doordash-steps.ts +1 -1
  262. package/src/daemon/handlers/shared.ts +4 -1
  263. package/src/daemon/host-app-control-proxy.ts +293 -0
  264. package/src/daemon/host-bash-proxy.ts +84 -74
  265. package/src/daemon/host-browser-proxy.ts +67 -82
  266. package/src/daemon/host-cu-proxy.ts +81 -86
  267. package/src/daemon/host-file-proxy.ts +93 -69
  268. package/src/daemon/host-proxy-base.ts +294 -0
  269. package/src/daemon/host-proxy-preactivation.ts +82 -0
  270. package/src/daemon/host-transfer-proxy.ts +247 -129
  271. package/src/daemon/lifecycle.ts +115 -117
  272. package/src/daemon/message-protocol.ts +3 -8
  273. package/src/daemon/message-types/contacts.ts +23 -1
  274. package/src/daemon/message-types/conversations.ts +11 -8
  275. package/src/daemon/message-types/host-app-control.ts +150 -0
  276. package/src/daemon/message-types/host-bash.ts +4 -0
  277. package/src/daemon/message-types/host-cu.ts +2 -0
  278. package/src/daemon/message-types/host-file.ts +4 -0
  279. package/src/daemon/message-types/host-transfer.ts +3 -0
  280. package/src/daemon/message-types/schedules.ts +8 -3
  281. package/src/daemon/message-types/skills.ts +2 -2
  282. package/src/daemon/process-message.ts +18 -1
  283. package/src/daemon/shutdown-handlers.ts +0 -3
  284. package/src/daemon/tool-setup-types.ts +51 -0
  285. package/src/daemon/tool-side-effects.ts +1 -1
  286. package/src/events/tool-audit-listener.ts +2 -1
  287. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +15 -7
  288. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +216 -0
  289. package/src/heartbeat/heartbeat-run-store.ts +236 -0
  290. package/src/heartbeat/heartbeat-service.ts +280 -49
  291. package/src/home/__tests__/post-connect-feed.test.ts +99 -0
  292. package/src/home/__tests__/relationship-state-writer.test.ts +11 -9
  293. package/src/home/__tests__/suggested-prompts.test.ts +89 -0
  294. package/src/home/post-connect-feed.ts +68 -0
  295. package/src/home/relationship-state-writer.ts +17 -92
  296. package/src/home/suggested-prompts.ts +46 -10
  297. package/src/inbound/public-ingress-urls.ts +32 -34
  298. package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
  299. package/src/ipc/assistant-server.ts +14 -1
  300. package/src/ipc/cli-client.ts +32 -1
  301. package/src/live-voice/live-voice-metrics.ts +10 -10
  302. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +304 -0
  303. package/src/mcp/mcp-auth-orchestrator.ts +213 -0
  304. package/src/mcp/mcp-auth-state.ts +133 -0
  305. package/src/mcp/mcp-oauth-provider.ts +19 -0
  306. package/src/memory/__tests__/jobs-store-job-classes.test.ts +24 -0
  307. package/src/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
  308. package/src/memory/__tests__/sparse-tokenize.test.ts +66 -0
  309. package/src/memory/anisotropy.test.ts +247 -0
  310. package/src/memory/anisotropy.ts +443 -0
  311. package/src/memory/auto-analysis-constants.ts +17 -0
  312. package/src/memory/auto-analysis-guard.ts +5 -15
  313. package/src/memory/canonical-guardian-store.ts +7 -7
  314. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +122 -0
  315. package/src/memory/context-search/agent-protocol.ts +6 -6
  316. package/src/memory/context-search/agent-runner.ts +32 -7
  317. package/src/memory/context-search/sources/memory-v2.ts +17 -5
  318. package/src/memory/conversation-crud.ts +1 -1
  319. package/src/memory/conversation-key-store.ts +2 -15
  320. package/src/memory/db-init.ts +4 -0
  321. package/src/memory/embedding-backend.ts +9 -21
  322. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +49 -4
  323. package/src/memory/graph/conversation-graph-memory.ts +1 -24
  324. package/src/memory/graph/graph-search.ts +8 -0
  325. package/src/memory/graph/retriever.ts +28 -0
  326. package/src/memory/graph/tools.ts +1 -1
  327. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +8 -2
  328. package/src/memory/jobs/embed-concept-page.ts +28 -2
  329. package/src/memory/jobs/embed-pkb-file.test.ts +2 -2
  330. package/src/memory/jobs-store.ts +66 -22
  331. package/src/memory/jobs-worker.ts +112 -63
  332. package/src/memory/memory-v2-activation-log-store.ts +1 -1
  333. package/src/memory/migrations/237-heartbeat-runs.ts +45 -0
  334. package/src/memory/migrations/238-schedule-retry-policy.ts +20 -0
  335. package/src/memory/migrations/index.ts +5 -0
  336. package/src/memory/migrations/registry.ts +8 -0
  337. package/src/memory/pkb/pkb-search.ts +7 -0
  338. package/src/memory/qdrant-client.ts +50 -20
  339. package/src/memory/schema/infrastructure.ts +15 -0
  340. package/src/memory/search/semantic.ts +7 -0
  341. package/src/memory/sparse-tokenize.ts +49 -0
  342. package/src/memory/v2/__tests__/activation.test.ts +77 -95
  343. package/src/memory/v2/__tests__/injection.test.ts +43 -21
  344. package/src/memory/v2/__tests__/sim.test.ts +166 -6
  345. package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
  346. package/src/memory/v2/__tests__/static-context.test.ts +0 -1
  347. package/src/memory/v2/activation.ts +69 -88
  348. package/src/memory/v2/consolidation-job.ts +3 -5
  349. package/src/memory/v2/constants.ts +7 -0
  350. package/src/memory/v2/injection.ts +86 -53
  351. package/src/memory/v2/prompts/consolidation.ts +312 -91
  352. package/src/memory/v2/qdrant.ts +99 -1
  353. package/src/memory/v2/sim.ts +126 -16
  354. package/src/memory/v2/skill-qdrant.ts +12 -3
  355. package/src/memory/v2/skill-store.ts +16 -1
  356. package/src/memory/v2/sparse-bm25.ts +245 -0
  357. package/src/memory/v2/static-context.ts +6 -5
  358. package/src/messaging/providers/gmail/types.ts +0 -49
  359. package/src/messaging/providers/slack/adapter.ts +1 -31
  360. package/src/messaging/providers/slack/types.ts +0 -32
  361. package/src/notifications/README.md +10 -10
  362. package/src/notifications/broadcaster.ts +1 -1
  363. package/src/notifications/guardian-question-mode.ts +5 -5
  364. package/src/oauth/connect-orchestrator.ts +4 -0
  365. package/src/oauth/credential-token-resolver.ts +1 -3
  366. package/src/oauth/manual-token-connection.ts +0 -4
  367. package/src/outbound-proxy/index.ts +1 -37
  368. package/src/outbound-proxy/logging.ts +1 -1
  369. package/src/outbound-proxy/policy.ts +6 -5
  370. package/src/outbound-proxy/router.ts +2 -1
  371. package/src/permissions/approval-policy.test.ts +6 -275
  372. package/src/permissions/approval-policy.ts +0 -51
  373. package/src/permissions/checker.test.ts +0 -1
  374. package/src/permissions/checker.ts +3 -17
  375. package/src/permissions/gateway-threshold-reader.ts +2 -0
  376. package/src/permissions/prompter.ts +34 -1
  377. package/src/permissions/secret-prompter.ts +6 -2
  378. package/src/prompts/bootstrap-cleanup.ts +27 -0
  379. package/src/prompts/system-prompt.ts +3 -18
  380. package/src/prompts/templates/SOUL.md +13 -1
  381. package/src/providers/speech-to-text/provider-catalog.ts +7 -8
  382. package/src/runtime/assistant-event-hub.ts +118 -96
  383. package/src/runtime/assistant-event.ts +1 -0
  384. package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
  385. package/src/runtime/auth/middleware.ts +0 -96
  386. package/src/runtime/auth/route-policy.ts +19 -0
  387. package/src/runtime/btw-sidechain.ts +2 -3
  388. package/src/runtime/channel-invite-transport.ts +2 -48
  389. package/src/runtime/channel-invite-transports/email.ts +1 -1
  390. package/src/runtime/channel-invite-transports/slack.ts +1 -1
  391. package/src/runtime/channel-invite-transports/telegram.ts +1 -1
  392. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  393. package/src/runtime/channel-invite-transports/whatsapp.ts +1 -1
  394. package/src/runtime/channel-invite-types.ts +54 -0
  395. package/src/runtime/channel-readiness-service.ts +32 -13
  396. package/src/runtime/http-server.ts +3 -329
  397. package/src/runtime/http-types.ts +0 -5
  398. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +413 -0
  399. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +260 -0
  400. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +189 -0
  401. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +153 -1
  402. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +451 -0
  403. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  404. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +515 -0
  405. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +437 -0
  406. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +319 -0
  407. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +51 -1
  408. package/src/runtime/migrations/migration-transport.ts +7 -7
  409. package/src/runtime/migrations/vbundle-builder.ts +327 -60
  410. package/src/runtime/migrations/vbundle-import-analyzer.ts +4 -4
  411. package/src/runtime/migrations/vbundle-import-policy.ts +172 -0
  412. package/src/runtime/migrations/vbundle-importer.ts +245 -68
  413. package/src/runtime/migrations/vbundle-streaming-importer.ts +326 -35
  414. package/src/runtime/migrations/vbundle-streaming-validator.ts +157 -4
  415. package/src/runtime/migrations/vbundle-tar-stream.ts +15 -6
  416. package/src/runtime/migrations/vbundle-validator.ts +114 -0
  417. package/src/runtime/pending-interactions.ts +35 -9
  418. package/src/runtime/routes/__tests__/backup-routes.test.ts +22 -150
  419. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
  420. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +242 -0
  421. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +112 -0
  422. package/src/runtime/routes/approval-interception-types.ts +13 -0
  423. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +1 -1
  424. package/src/runtime/routes/backup-routes.ts +15 -38
  425. package/src/runtime/routes/btw-routes.ts +14 -37
  426. package/src/runtime/routes/client-routes.ts +1 -0
  427. package/src/runtime/routes/contact-prompt-routes.ts +183 -0
  428. package/src/runtime/routes/conversation-query-routes.ts +36 -1
  429. package/src/runtime/routes/conversation-routes.ts +30 -13
  430. package/src/runtime/routes/document-pdf-renderer.ts +165 -0
  431. package/src/runtime/routes/documents-routes.ts +30 -0
  432. package/src/runtime/routes/errors.ts +19 -4
  433. package/src/runtime/routes/events-routes.ts +12 -6
  434. package/src/runtime/routes/gateway-log-routes.ts +79 -0
  435. package/src/runtime/routes/guardian-approval-interception.ts +2 -8
  436. package/src/runtime/routes/heartbeat-routes.ts +103 -38
  437. package/src/runtime/routes/host-app-control-routes.ts +134 -0
  438. package/src/runtime/routes/host-bash-routes.ts +36 -6
  439. package/src/runtime/routes/host-browser-routes.ts +108 -13
  440. package/src/runtime/routes/host-cu-routes.ts +44 -14
  441. package/src/runtime/routes/host-file-routes.ts +33 -10
  442. package/src/runtime/routes/host-transfer-routes.ts +64 -24
  443. package/src/runtime/routes/http-adapter.ts +1 -0
  444. package/src/runtime/routes/identity-intro-cache.ts +30 -0
  445. package/src/runtime/routes/identity-routes.ts +15 -43
  446. package/src/runtime/routes/inbound-message-handler.ts +1 -9
  447. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -7
  448. package/src/runtime/routes/inbound-stages/edit-intercept.ts +0 -8
  449. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +0 -20
  450. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
  451. package/src/runtime/routes/index.ts +8 -0
  452. package/src/runtime/routes/mcp-auth-routes.ts +132 -0
  453. package/src/runtime/routes/memory-item-routes.ts +10 -12
  454. package/src/runtime/routes/memory-v2-routes.ts +441 -1
  455. package/src/runtime/routes/migration-routes.ts +96 -0
  456. package/src/runtime/routes/schedule-routes.ts +7 -0
  457. package/src/runtime/verification-templates.ts +4 -7
  458. package/src/schedule/integration-status.ts +66 -2
  459. package/src/schedule/recurrence-engine.ts +4 -1
  460. package/src/schedule/retry-backoff.ts +18 -0
  461. package/src/schedule/retry-policy.ts +82 -0
  462. package/src/schedule/schedule-recovery.ts +64 -0
  463. package/src/schedule/schedule-store.ts +106 -2
  464. package/src/schedule/scheduler-types.ts +25 -0
  465. package/src/schedule/scheduler.ts +63 -38
  466. package/src/security/oauth-callback-registry.ts +8 -0
  467. package/src/sequence/analytics.ts +5 -5
  468. package/src/sequence/engine.ts +1 -1
  469. package/src/skills/catalog-files.ts +2 -8
  470. package/src/skills/include-graph.ts +5 -5
  471. package/src/skills/remote-skill-policy.ts +5 -5
  472. package/src/skills/skill-file-provider.ts +1 -1
  473. package/src/skills/skill-file-types.ts +13 -0
  474. package/src/skills/skillssh-audit-types.ts +28 -0
  475. package/src/skills/skillssh-registry.ts +8 -21
  476. package/src/telemetry/types.ts +2 -0
  477. package/src/telemetry/usage-telemetry-reporter.test.ts +21 -0
  478. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  479. package/src/tools/app-control/skill-proxy-bridge.ts +28 -0
  480. package/src/tools/apps/executors.ts +56 -69
  481. package/src/tools/browser/__tests__/browser-status.test.ts +21 -18
  482. package/src/tools/browser/browser-execution.ts +2 -2
  483. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +55 -4
  484. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +12 -6
  485. package/src/tools/browser/cdp-client/factory.ts +23 -24
  486. package/src/tools/browser/cdp-client/index.ts +1 -14
  487. package/src/tools/computer-use/definitions.ts +42 -20
  488. package/src/tools/executor.ts +2 -0
  489. package/src/tools/host-filesystem/edit.ts +26 -0
  490. package/src/tools/host-filesystem/read.ts +26 -0
  491. package/src/tools/host-filesystem/transfer.ts +31 -1
  492. package/src/tools/host-filesystem/write.ts +26 -0
  493. package/src/tools/host-terminal/host-shell.ts +58 -0
  494. package/src/tools/schedule/create.ts +6 -0
  495. package/src/tools/schedule/list.ts +2 -0
  496. package/src/tools/schedule/update.ts +10 -0
  497. package/src/tools/shared/filesystem/file-ops-service.ts +2 -0
  498. package/src/tools/shared/filesystem/path-policy.ts +25 -1
  499. package/src/tools/skills/load.ts +0 -32
  500. package/src/tools/tool-approval-handler.ts +1 -5
  501. package/src/tools/types.ts +4 -0
  502. package/src/usage/pricing.ts +1 -1
  503. package/src/workspace/hatched-date.ts +86 -0
  504. package/src/workspace/migrations/003-seed-device-id.ts +1 -1
  505. package/src/workspace/migrations/006-services-config.ts +8 -5
  506. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +3 -9
  507. package/src/workspace/migrations/021-move-signals-to-workspace.ts +4 -10
  508. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +4 -10
  509. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +4 -11
  510. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +3 -10
  511. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +3 -2
  512. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +2 -1
  513. package/src/workspace/migrations/059-move-pid-to-workspace.ts +3 -8
  514. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +3 -8
  515. package/src/workspace/migrations/AGENTS.md +1 -1
  516. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
  517. package/src/workspace/migrations/utils.ts +21 -0
  518. package/src/__tests__/host-browser-e2e-cloud.test.ts +0 -443
  519. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +0 -226
  520. package/src/__tests__/host-browser-ws-events-e2e.test.ts +0 -427
  521. package/src/__tests__/twilio-rest.test.ts +0 -34
  522. package/src/backup/__tests__/backup-key.test.ts +0 -152
  523. package/src/backup/__tests__/backup-worker.test.ts +0 -782
  524. package/src/backup/__tests__/offsite-writer.test.ts +0 -641
  525. package/src/backup/__tests__/stream-crypt.test.ts +0 -228
  526. package/src/backup/backup-key.ts +0 -137
  527. package/src/backup/backup-worker.ts +0 -472
  528. package/src/backup/offsite-writer.ts +0 -222
  529. package/src/backup/stream-crypt.ts +0 -263
  530. package/src/daemon/message-types/pairing.ts +0 -58
  531. package/src/outbound-proxy/config.ts +0 -20
  532. package/src/outbound-proxy/health.ts +0 -18
  533. package/src/outbound-proxy/types.ts +0 -150
  534. package/src/runtime/capability-tokens.ts +0 -190
  535. package/src/signals/mcp-reload.ts +0 -18
@@ -5,7 +5,6 @@ import { join } from "node:path";
5
5
  import { afterAll, afterEach, describe, expect, mock, test } from "bun:test";
6
6
 
7
7
  const sentMessages: unknown[] = [];
8
- const resolvedInteractionIds: string[] = [];
9
8
  let mockHasClient = false;
10
9
 
11
10
  mock.module("../runtime/assistant-event-hub.js", () => ({
@@ -13,20 +12,14 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
13
12
  assistantEventHub: {
14
13
  getMostRecentClientByCapability: (cap: string) =>
15
14
  cap === "host_file" && mockHasClient ? { id: "mock-client" } : null,
15
+ listClientsByCapability: (_cap: string) =>
16
+ mockHasClient ? [{ clientId: "mock-client", capabilities: ["host_file"] }] : [],
17
+ getClientById: (_id: string) => null,
16
18
  },
17
19
  }));
18
20
 
19
- mock.module("../runtime/pending-interactions.js", () => ({
20
- resolve: (requestId: string) => {
21
- resolvedInteractionIds.push(requestId);
22
- return undefined;
23
- },
24
- get: () => undefined,
25
- getByKind: () => [],
26
- getByConversation: () => [],
27
- removeByConversation: () => {},
28
- }));
29
-
21
+ // Use the REAL pending-interactions module the proxy self-registers here.
22
+ const pendingInteractions = await import("../runtime/pending-interactions.js");
30
23
  const { HostTransferProxy } = await import("../daemon/host-transfer-proxy.js");
31
24
 
32
25
  /**
@@ -56,14 +49,15 @@ describe("HostTransferProxy", () => {
56
49
 
57
50
  function setup() {
58
51
  sentMessages.length = 0;
59
- resolvedInteractionIds.length = 0;
60
52
  mockHasClient = false;
53
+ pendingInteractions.clear();
61
54
  proxy = new (HostTransferProxy as any)();
62
55
  }
63
56
 
64
57
  afterEach(async () => {
65
58
  proxy?.dispose();
66
59
  HostTransferProxy.reset();
60
+ pendingInteractions.clear();
67
61
  if (tempDir) {
68
62
  await rm(tempDir, { recursive: true, force: true }).catch(() => {});
69
63
  }
@@ -347,6 +341,110 @@ describe("HostTransferProxy", () => {
347
341
  });
348
342
  });
349
343
 
344
+ describe("takeJustConsumedTransferMetadata", () => {
345
+ test("returns size+sha256 immediately after getTransferContent", async () => {
346
+ setup();
347
+ tempDir = await mkdtemp(join(tmpdir(), "htp-test-"));
348
+ const srcPath = join(tempDir, "source.txt");
349
+ const fileContent = "header metadata test";
350
+ await globalThis.Bun.write(srcPath, fileContent);
351
+
352
+ const resultPromise = proxy.requestToHost({
353
+ sourcePath: srcPath,
354
+ destPath: "/host/dest.txt",
355
+ overwrite: true,
356
+ conversationId: "conv-meta-1",
357
+ });
358
+
359
+ await waitForMessages(sentMessages, 1);
360
+ const sent = sentMessages[0] as Record<string, unknown>;
361
+ const transferId = sent.transferId as string;
362
+
363
+ const expectedSize = Buffer.from(fileContent).length;
364
+ const expectedSha = createHash("sha256")
365
+ .update(Buffer.from(fileContent))
366
+ .digest("hex");
367
+
368
+ // Handler consumes content; metadata should now be available for the
369
+ // GET-content route's responseHeaders resolver.
370
+ const content = proxy.getTransferContent(transferId);
371
+ expect(content).not.toBeNull();
372
+
373
+ const meta = proxy.takeJustConsumedTransferMetadata(transferId);
374
+ expect(meta).toEqual({ sizeBytes: expectedSize, sha256: expectedSha });
375
+
376
+ // Resolve the transfer to avoid hanging
377
+ const requestId = sent.requestId as string;
378
+ proxy.resolveTransferResult(requestId, {
379
+ isError: false,
380
+ bytesWritten: expectedSize,
381
+ });
382
+ await resultPromise;
383
+ });
384
+
385
+ test("single-use: second take returns null", async () => {
386
+ setup();
387
+ tempDir = await mkdtemp(join(tmpdir(), "htp-test-"));
388
+ const srcPath = join(tempDir, "source.txt");
389
+ await globalThis.Bun.write(srcPath, "x");
390
+
391
+ const resultPromise = proxy.requestToHost({
392
+ sourcePath: srcPath,
393
+ destPath: "/host/dest.txt",
394
+ overwrite: true,
395
+ conversationId: "conv-meta-2",
396
+ });
397
+
398
+ await waitForMessages(sentMessages, 1);
399
+ const sent = sentMessages[0] as Record<string, unknown>;
400
+ const transferId = sent.transferId as string;
401
+
402
+ proxy.getTransferContent(transferId);
403
+ expect(proxy.takeJustConsumedTransferMetadata(transferId)).not.toBeNull();
404
+ expect(proxy.takeJustConsumedTransferMetadata(transferId)).toBeNull();
405
+
406
+ const requestId = sent.requestId as string;
407
+ proxy.resolveTransferResult(requestId, { isError: false });
408
+ await resultPromise;
409
+ });
410
+
411
+ test("returns null when getTransferContent was never called", () => {
412
+ setup();
413
+ expect(
414
+ proxy.takeJustConsumedTransferMetadata("never-consumed-id"),
415
+ ).toBeNull();
416
+ });
417
+
418
+ test("returns null for unknown transfer ID even after a different transfer was consumed", async () => {
419
+ setup();
420
+ tempDir = await mkdtemp(join(tmpdir(), "htp-test-"));
421
+ const srcPath = join(tempDir, "source.txt");
422
+ await globalThis.Bun.write(srcPath, "x");
423
+
424
+ const resultPromise = proxy.requestToHost({
425
+ sourcePath: srcPath,
426
+ destPath: "/host/dest.txt",
427
+ overwrite: true,
428
+ conversationId: "conv-meta-3",
429
+ });
430
+
431
+ await waitForMessages(sentMessages, 1);
432
+ const sent = sentMessages[0] as Record<string, unknown>;
433
+ const transferId = sent.transferId as string;
434
+
435
+ proxy.getTransferContent(transferId);
436
+
437
+ // Different transferId should still return null
438
+ expect(
439
+ proxy.takeJustConsumedTransferMetadata("other-id"),
440
+ ).toBeNull();
441
+
442
+ const requestId = sent.requestId as string;
443
+ proxy.resolveTransferResult(requestId, { isError: false });
444
+ await resultPromise;
445
+ });
446
+ });
447
+
350
448
  describe("timeout behavior", () => {
351
449
  /** Use a very short real timeout instead of fake timers to avoid deadlocks in Bun. */
352
450
  const SHORT_TIMEOUT_MS = 150;
@@ -642,8 +740,8 @@ describe("HostTransferProxy", () => {
642
740
  });
643
741
  });
644
742
 
645
- describe("pendingInteractions.resolve callback", () => {
646
- test("fires on abort", async () => {
743
+ describe("pendingInteractions cleanup", () => {
744
+ test("cleans up on abort", async () => {
647
745
  setup();
648
746
 
649
747
  const controller = new AbortController();
@@ -658,14 +756,15 @@ describe("HostTransferProxy", () => {
658
756
 
659
757
  const sent = sentMessages[0] as Record<string, unknown>;
660
758
  const requestId = sent.requestId as string;
759
+ expect(pendingInteractions.get(requestId)).toBeDefined();
661
760
 
662
761
  controller.abort();
663
762
  await resultPromise;
664
763
 
665
- expect(resolvedInteractionIds).toContain(requestId);
764
+ expect(pendingInteractions.get(requestId)).toBeUndefined();
666
765
  });
667
766
 
668
- test("fires for each pending request on dispose", () => {
767
+ test("cleans up for each pending request on dispose", () => {
669
768
  setup();
670
769
 
671
770
  const p1 = proxy.requestToSandbox({
@@ -688,12 +787,11 @@ describe("HostTransferProxy", () => {
688
787
 
689
788
  proxy.dispose();
690
789
 
691
- expect(resolvedInteractionIds).toHaveLength(2);
692
- expect(resolvedInteractionIds).toContain(ids[0]);
693
- expect(resolvedInteractionIds).toContain(ids[1]);
790
+ expect(pendingInteractions.get(ids[0])).toBeUndefined();
791
+ expect(pendingInteractions.get(ids[1])).toBeUndefined();
694
792
  });
695
793
 
696
- test("does not fire on normal resolveTransferResult", async () => {
794
+ test("cleans up on normal resolveTransferResult", async () => {
697
795
  setup();
698
796
  tempDir = await mkdtemp(join(tmpdir(), "htp-test-"));
699
797
  const srcPath = join(tempDir, "source.txt");
@@ -710,6 +808,7 @@ describe("HostTransferProxy", () => {
710
808
 
711
809
  const sent = sentMessages[0] as Record<string, unknown>;
712
810
  const requestId = sent.requestId as string;
811
+ expect(pendingInteractions.get(requestId)).toBeDefined();
713
812
 
714
813
  proxy.resolveTransferResult(requestId, {
715
814
  isError: false,
@@ -717,7 +816,7 @@ describe("HostTransferProxy", () => {
717
816
  });
718
817
 
719
818
  await resultPromise;
720
- expect(resolvedInteractionIds).toEqual([]);
819
+ expect(pendingInteractions.get(requestId)).toBeUndefined();
721
820
  });
722
821
  });
723
822
  });
@@ -0,0 +1,447 @@
1
+ /**
2
+ * Tests for the host-transfer route 403 guard introduced in Phase 3.
3
+ *
4
+ * Covers GET /transfers/:transferId/content, PUT /transfers/:transferId/content,
5
+ * and POST /host-transfer-result ownership checks.
6
+ *
7
+ * 1. Targeted + correct x-vellum-client-id header → success
8
+ * 2. Targeted + missing header → 400 BadRequestError
9
+ * 3. Targeted + wrong header → 403 ForbiddenError, operation NOT performed
10
+ * 4. Untargeted (no targetClientId, no header) → success (regression)
11
+ */
12
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
13
+
14
+ // ── Module mocks ────────────────────────────────────────────────────────────
15
+
16
+ mock.module("../config/env.js", () => ({
17
+ isHttpAuthDisabled: () => true,
18
+ hasUngatedHttpAuthDisabled: () => false,
19
+ }));
20
+
21
+ import type { PendingInteraction } from "../runtime/pending-interactions.js";
22
+
23
+ const pendingStore = new Map<string, PendingInteraction>();
24
+
25
+ mock.module("../runtime/pending-interactions.js", () => ({
26
+ get: (requestId: string) => pendingStore.get(requestId),
27
+ resolve: (requestId: string) => {
28
+ const entry = pendingStore.get(requestId);
29
+ if (entry) pendingStore.delete(requestId);
30
+ return entry;
31
+ },
32
+ }));
33
+
34
+ // Per-test controls for the proxy stub
35
+ let stubTargetClientId: string | null = null;
36
+ const getTransferContentCalls: string[] = [];
37
+ const receiveTransferContentCalls: string[] = [];
38
+ const resolveTransferResultCalls: string[] = [];
39
+
40
+ mock.module("../daemon/host-transfer-proxy.js", () => ({
41
+ HostTransferProxy: {
42
+ get instance() {
43
+ return {
44
+ getRequestIdForTransfer(_transferId: string) {
45
+ return "req-1";
46
+ },
47
+ getTargetClientIdForTransfer(_transferId: string) {
48
+ return stubTargetClientId;
49
+ },
50
+ getTransferContent(transferId: string) {
51
+ getTransferContentCalls.push(transferId);
52
+ return { buffer: Buffer.from("data"), sizeBytes: 4, sha256: "abc123" };
53
+ },
54
+ async receiveTransferContent(transferId: string, _data: Buffer, _sha256: string) {
55
+ receiveTransferContentCalls.push(transferId);
56
+ return { accepted: true };
57
+ },
58
+ resolveTransferResult(requestId: string, _result: unknown) {
59
+ resolveTransferResultCalls.push(requestId);
60
+ },
61
+ };
62
+ },
63
+ },
64
+ }));
65
+
66
+ // ── Real imports (after mocks) ──────────────────────────────────────────────
67
+
68
+ import {
69
+ BadRequestError,
70
+ ForbiddenError,
71
+ } from "../runtime/routes/errors.js";
72
+ import { ROUTES } from "../runtime/routes/host-transfer-routes.js";
73
+
74
+ afterAll(() => {
75
+ mock.restore();
76
+ });
77
+
78
+ const handleTransferContentGet = ROUTES.find(
79
+ (r) => r.endpoint === "transfers/:transferId/content" && r.method === "GET",
80
+ )!.handler;
81
+
82
+ const handleTransferContentPut = ROUTES.find(
83
+ (r) => r.endpoint === "transfers/:transferId/content" && r.method === "PUT",
84
+ )!.handler;
85
+
86
+ const handleTransferResult = ROUTES.find(
87
+ (r) => r.endpoint === "host-transfer-result",
88
+ )!.handler;
89
+
90
+ // ── Helpers ─────────────────────────────────────────────────────────────────
91
+
92
+ const TEST_TRANSFER_ID = "transfer-abc";
93
+ const TEST_REQUEST_ID = "req-1";
94
+
95
+ function registerPending(overrides: Partial<PendingInteraction> = {}): void {
96
+ pendingStore.set(TEST_REQUEST_ID, {
97
+ conversationId: "conv-1",
98
+ kind: "host_transfer",
99
+ ...overrides,
100
+ });
101
+ }
102
+
103
+ // ── Tests ────────────────────────────────────────────────────────────────────
104
+
105
+ describe("handleTransferContentGet — Phase 3 targetClientId guard", () => {
106
+ beforeEach(() => {
107
+ pendingStore.clear();
108
+ stubTargetClientId = null;
109
+ getTransferContentCalls.length = 0;
110
+ });
111
+
112
+ // ── 1. Targeted + correct header → success ────────────────────────────────
113
+
114
+ describe("targeted + correct x-vellum-client-id header", () => {
115
+ test("returns Uint8Array and calls getTransferContent", async () => {
116
+ stubTargetClientId = "client-A";
117
+ const result = await handleTransferContentGet({
118
+ pathParams: { transferId: TEST_TRANSFER_ID },
119
+ headers: { "x-vellum-client-id": "client-A" },
120
+ });
121
+
122
+ expect(result).toBeInstanceOf(Uint8Array);
123
+ expect(getTransferContentCalls).toContain(TEST_TRANSFER_ID);
124
+ });
125
+
126
+ test("trims whitespace from header before comparing", async () => {
127
+ stubTargetClientId = "client-A";
128
+ const result = await handleTransferContentGet({
129
+ pathParams: { transferId: TEST_TRANSFER_ID },
130
+ headers: { "x-vellum-client-id": " client-A " },
131
+ });
132
+
133
+ expect(result).toBeInstanceOf(Uint8Array);
134
+ });
135
+ });
136
+
137
+ // ── 2. Targeted + missing header → 400 ───────────────────────────────────
138
+
139
+ describe("targeted + missing x-vellum-client-id header", () => {
140
+ test("throws BadRequestError when header is absent", () => {
141
+ stubTargetClientId = "client-A";
142
+ expect(() =>
143
+ handleTransferContentGet({
144
+ pathParams: { transferId: TEST_TRANSFER_ID },
145
+ }),
146
+ ).toThrow(BadRequestError);
147
+ });
148
+
149
+ test("getTransferContent NOT called on 400", () => {
150
+ stubTargetClientId = "client-A";
151
+ try {
152
+ handleTransferContentGet({
153
+ pathParams: { transferId: TEST_TRANSFER_ID },
154
+ });
155
+ } catch {
156
+ // expected
157
+ }
158
+ expect(getTransferContentCalls).toHaveLength(0);
159
+ });
160
+ });
161
+
162
+ // ── 3. Targeted + wrong header → 403 ─────────────────────────────────────
163
+
164
+ describe("targeted + wrong x-vellum-client-id header", () => {
165
+ test("throws ForbiddenError when client ID does not match", () => {
166
+ stubTargetClientId = "client-A";
167
+ expect(() =>
168
+ handleTransferContentGet({
169
+ pathParams: { transferId: TEST_TRANSFER_ID },
170
+ headers: { "x-vellum-client-id": "client-B" },
171
+ }),
172
+ ).toThrow(ForbiddenError);
173
+ });
174
+
175
+ test("getTransferContent NOT called on 403", () => {
176
+ stubTargetClientId = "client-A";
177
+ try {
178
+ handleTransferContentGet({
179
+ pathParams: { transferId: TEST_TRANSFER_ID },
180
+ headers: { "x-vellum-client-id": "client-B" },
181
+ });
182
+ } catch {
183
+ // expected
184
+ }
185
+ expect(getTransferContentCalls).toHaveLength(0);
186
+ });
187
+ });
188
+
189
+ // ── 4. Untargeted — regression ────────────────────────────────────────────
190
+
191
+ describe("untargeted request (no targetClientId)", () => {
192
+ test("returns Uint8Array without a header", async () => {
193
+ stubTargetClientId = null;
194
+ const result = await handleTransferContentGet({
195
+ pathParams: { transferId: TEST_TRANSFER_ID },
196
+ });
197
+
198
+ expect(result).toBeInstanceOf(Uint8Array);
199
+ expect(getTransferContentCalls).toContain(TEST_TRANSFER_ID);
200
+ });
201
+ });
202
+ });
203
+
204
+ describe("handleTransferContentPut — Phase 3 targetClientId guard", () => {
205
+ beforeEach(() => {
206
+ pendingStore.clear();
207
+ stubTargetClientId = null;
208
+ receiveTransferContentCalls.length = 0;
209
+ });
210
+
211
+ // ── 1. Targeted + correct header → success ────────────────────────────────
212
+
213
+ describe("targeted + correct x-vellum-client-id header", () => {
214
+ test("returns { accepted: true } and calls receiveTransferContent", async () => {
215
+ stubTargetClientId = "client-A";
216
+ const result = await handleTransferContentPut({
217
+ pathParams: { transferId: TEST_TRANSFER_ID },
218
+ headers: { "x-vellum-client-id": "client-A", "x-transfer-sha256": "abc" },
219
+ rawBody: new Uint8Array(Buffer.from("data")),
220
+ });
221
+
222
+ expect(result).toEqual({ accepted: true });
223
+ expect(receiveTransferContentCalls).toContain(TEST_TRANSFER_ID);
224
+ });
225
+
226
+ test("trims whitespace from header before comparing", async () => {
227
+ stubTargetClientId = "client-A";
228
+ const result = await handleTransferContentPut({
229
+ pathParams: { transferId: TEST_TRANSFER_ID },
230
+ headers: { "x-vellum-client-id": " client-A ", "x-transfer-sha256": "abc" },
231
+ rawBody: new Uint8Array(Buffer.from("data")),
232
+ });
233
+
234
+ expect(result).toEqual({ accepted: true });
235
+ });
236
+ });
237
+
238
+ // ── 2. Targeted + missing header → 400 ───────────────────────────────────
239
+
240
+ describe("targeted + missing x-vellum-client-id header", () => {
241
+ test("throws BadRequestError when header is absent", async () => {
242
+ stubTargetClientId = "client-A";
243
+ await expect(
244
+ handleTransferContentPut({
245
+ pathParams: { transferId: TEST_TRANSFER_ID },
246
+ headers: { "x-transfer-sha256": "abc" },
247
+ rawBody: new Uint8Array(Buffer.from("data")),
248
+ }),
249
+ ).rejects.toBeInstanceOf(BadRequestError);
250
+ });
251
+
252
+ test("receiveTransferContent NOT called on 400", async () => {
253
+ stubTargetClientId = "client-A";
254
+ try {
255
+ await handleTransferContentPut({
256
+ pathParams: { transferId: TEST_TRANSFER_ID },
257
+ headers: { "x-transfer-sha256": "abc" },
258
+ rawBody: new Uint8Array(Buffer.from("data")),
259
+ });
260
+ } catch {
261
+ // expected
262
+ }
263
+ expect(receiveTransferContentCalls).toHaveLength(0);
264
+ });
265
+ });
266
+
267
+ // ── 3. Targeted + wrong header → 403 ─────────────────────────────────────
268
+
269
+ describe("targeted + wrong x-vellum-client-id header", () => {
270
+ test("throws ForbiddenError when client ID does not match", async () => {
271
+ stubTargetClientId = "client-A";
272
+ await expect(
273
+ handleTransferContentPut({
274
+ pathParams: { transferId: TEST_TRANSFER_ID },
275
+ headers: { "x-vellum-client-id": "client-B", "x-transfer-sha256": "abc" },
276
+ rawBody: new Uint8Array(Buffer.from("data")),
277
+ }),
278
+ ).rejects.toBeInstanceOf(ForbiddenError);
279
+ });
280
+
281
+ test("receiveTransferContent NOT called on 403", async () => {
282
+ stubTargetClientId = "client-A";
283
+ try {
284
+ await handleTransferContentPut({
285
+ pathParams: { transferId: TEST_TRANSFER_ID },
286
+ headers: { "x-vellum-client-id": "client-B", "x-transfer-sha256": "abc" },
287
+ rawBody: new Uint8Array(Buffer.from("data")),
288
+ });
289
+ } catch {
290
+ // expected
291
+ }
292
+ expect(receiveTransferContentCalls).toHaveLength(0);
293
+ });
294
+ });
295
+
296
+ // ── 4. Untargeted — regression ────────────────────────────────────────────
297
+
298
+ describe("untargeted request (no targetClientId)", () => {
299
+ test("returns { accepted: true } without a header", async () => {
300
+ stubTargetClientId = null;
301
+ const result = await handleTransferContentPut({
302
+ pathParams: { transferId: TEST_TRANSFER_ID },
303
+ headers: { "x-transfer-sha256": "abc" },
304
+ rawBody: new Uint8Array(Buffer.from("data")),
305
+ });
306
+
307
+ expect(result).toEqual({ accepted: true });
308
+ expect(receiveTransferContentCalls).toContain(TEST_TRANSFER_ID);
309
+ });
310
+ });
311
+ });
312
+
313
+ describe("handleTransferResult — Phase 3 targetClientId guard", () => {
314
+ beforeEach(() => {
315
+ pendingStore.clear();
316
+ stubTargetClientId = null;
317
+ resolveTransferResultCalls.length = 0;
318
+ });
319
+
320
+ function registerHostTransferPending(targetClientId?: string): void {
321
+ registerPending({ targetClientId });
322
+ }
323
+
324
+ function resultBody(): Record<string, unknown> {
325
+ return { requestId: TEST_REQUEST_ID };
326
+ }
327
+
328
+ // ── 1. Targeted + correct header → success ────────────────────────────────
329
+
330
+ describe("targeted + correct x-vellum-client-id header", () => {
331
+ test("returns { accepted: true } and calls resolveTransferResult", async () => {
332
+ registerHostTransferPending("client-A");
333
+ const result = await handleTransferResult({
334
+ body: resultBody(),
335
+ headers: { "x-vellum-client-id": "client-A" },
336
+ });
337
+
338
+ expect(result).toEqual({ accepted: true });
339
+ expect(resolveTransferResultCalls).toContain(TEST_REQUEST_ID);
340
+ });
341
+
342
+ test("trims whitespace from header before comparing", async () => {
343
+ registerHostTransferPending("client-A");
344
+ const result = await handleTransferResult({
345
+ body: resultBody(),
346
+ headers: { "x-vellum-client-id": " client-A " },
347
+ });
348
+
349
+ expect(result).toEqual({ accepted: true });
350
+ });
351
+ });
352
+
353
+ // ── 2. Targeted + missing header → 400 ───────────────────────────────────
354
+
355
+ describe("targeted + missing x-vellum-client-id header", () => {
356
+ test("throws BadRequestError when header is absent", () => {
357
+ registerHostTransferPending("client-A");
358
+ expect(() =>
359
+ handleTransferResult({ body: resultBody() }),
360
+ ).toThrow(BadRequestError);
361
+ });
362
+
363
+ test("resolveTransferResult NOT called on 400", () => {
364
+ registerHostTransferPending("client-A");
365
+ try {
366
+ handleTransferResult({ body: resultBody() });
367
+ } catch {
368
+ // expected
369
+ }
370
+ expect(resolveTransferResultCalls).toHaveLength(0);
371
+ });
372
+
373
+ test("pending interaction still present after 400", () => {
374
+ registerHostTransferPending("client-A");
375
+ try {
376
+ handleTransferResult({ body: resultBody() });
377
+ } catch {
378
+ // expected
379
+ }
380
+ expect(pendingStore.has(TEST_REQUEST_ID)).toBe(true);
381
+ });
382
+ });
383
+
384
+ // ── 3. Targeted + wrong header → 403 ─────────────────────────────────────
385
+
386
+ describe("targeted + wrong x-vellum-client-id header", () => {
387
+ test("throws ForbiddenError when client ID does not match", () => {
388
+ registerHostTransferPending("client-A");
389
+ expect(() =>
390
+ handleTransferResult({
391
+ body: resultBody(),
392
+ headers: { "x-vellum-client-id": "client-B" },
393
+ }),
394
+ ).toThrow(ForbiddenError);
395
+ });
396
+
397
+ test("resolveTransferResult NOT called on 403", () => {
398
+ registerHostTransferPending("client-A");
399
+ try {
400
+ handleTransferResult({
401
+ body: resultBody(),
402
+ headers: { "x-vellum-client-id": "client-B" },
403
+ });
404
+ } catch {
405
+ // expected
406
+ }
407
+ expect(resolveTransferResultCalls).toHaveLength(0);
408
+ });
409
+
410
+ test("pending interaction still present after 403", () => {
411
+ registerHostTransferPending("client-A");
412
+ try {
413
+ handleTransferResult({
414
+ body: resultBody(),
415
+ headers: { "x-vellum-client-id": "client-B" },
416
+ });
417
+ } catch {
418
+ // expected
419
+ }
420
+ expect(pendingStore.has(TEST_REQUEST_ID)).toBe(true);
421
+ });
422
+ });
423
+
424
+ // ── 4. Untargeted — regression ────────────────────────────────────────────
425
+
426
+ describe("untargeted request (no targetClientId)", () => {
427
+ test("accepts when no header is provided", async () => {
428
+ registerHostTransferPending();
429
+ const result = await handleTransferResult({
430
+ body: resultBody(),
431
+ });
432
+
433
+ expect(result).toEqual({ accepted: true });
434
+ expect(resolveTransferResultCalls).toContain(TEST_REQUEST_ID);
435
+ });
436
+
437
+ test("accepts when header is present (header ignored for untargeted)", async () => {
438
+ registerHostTransferPending();
439
+ const result = await handleTransferResult({
440
+ body: resultBody(),
441
+ headers: { "x-vellum-client-id": "client-whatever" },
442
+ });
443
+
444
+ expect(result).toEqual({ accepted: true });
445
+ });
446
+ });
447
+ });
@@ -206,6 +206,7 @@ function makeConversation(overrides: Record<string, unknown> = {}) {
206
206
  updateClient: () => {},
207
207
  setHostBrowserProxy: () => {},
208
208
  setHostCuProxy: () => {},
209
+ setHostAppControlProxy: () => {},
209
210
  addPreactivatedSkillId: () => {},
210
211
  emitConfirmationStateChanged: () => {},
211
212
  emitActivityState: () => {},