@vellumai/assistant 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (463) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docker-entrypoint.sh +12 -2
  4. package/docs/architecture/memory.md +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  7. package/openapi.yaml +184 -69
  8. package/package.json +41 -41
  9. package/scripts/generate-openapi.ts +1 -2
  10. package/src/__tests__/acp-session.test.ts +43 -0
  11. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +1 -0
  13. package/src/__tests__/app-source-watcher.test.ts +37 -11
  14. package/src/__tests__/approval-routes-http.test.ts +178 -1
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  17. package/src/__tests__/browser-manager.test.ts +40 -27
  18. package/src/__tests__/catalog-files.test.ts +862 -0
  19. package/src/__tests__/channel-approvals.test.ts +53 -0
  20. package/src/__tests__/checker.test.ts +104 -170
  21. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  22. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  23. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  24. package/src/__tests__/config-schema.test.ts +125 -48
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  26. package/src/__tests__/context-overflow-approval.test.ts +21 -6
  27. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  28. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  29. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  30. package/src/__tests__/conversation-attachments.test.ts +80 -4
  31. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  32. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  33. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  34. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  35. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  36. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  37. package/src/__tests__/conversation-queue.test.ts +45 -2
  38. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  39. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  40. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  42. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  43. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  44. package/src/__tests__/conversation-store.test.ts +195 -0
  45. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  46. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -3
  47. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  48. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  49. package/src/__tests__/credential-vault.test.ts +152 -13
  50. package/src/__tests__/credentials-cli.test.ts +2 -2
  51. package/src/__tests__/date-context.test.ts +4 -4
  52. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  53. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  54. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  55. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  56. package/src/__tests__/gemini-provider.test.ts +2 -2
  57. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  58. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  59. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  60. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  61. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  62. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  63. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  64. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  65. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  66. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  67. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  68. package/src/__tests__/host-browser-routes.test.ts +198 -0
  69. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  70. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  71. package/src/__tests__/host-file-proxy.test.ts +185 -1
  72. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  73. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  74. package/src/__tests__/host-shell-tool.test.ts +1 -11
  75. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  76. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  77. package/src/__tests__/inline-command-runner.test.ts +7 -5
  78. package/src/__tests__/integration-status.test.ts +6 -7
  79. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  80. package/src/__tests__/log-export-workspace.test.ts +190 -0
  81. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  82. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  83. package/src/__tests__/mcp-health-check.test.ts +10 -3
  84. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  85. package/src/__tests__/migration-export-http.test.ts +61 -2
  86. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  87. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  88. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  89. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  90. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  91. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  92. package/src/__tests__/oauth-cli.test.ts +707 -60
  93. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  94. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  95. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  96. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  97. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  98. package/src/__tests__/oauth-store.test.ts +1386 -182
  99. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  100. package/src/__tests__/onboarding-template-contract.test.ts +74 -55
  101. package/src/__tests__/openai-provider.test.ts +2 -2
  102. package/src/__tests__/outlook-categories.test.ts +1 -1
  103. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  104. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  105. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  106. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  107. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  108. package/src/__tests__/outlook-trash.test.ts +1 -1
  109. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  110. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  111. package/src/__tests__/permission-mode.test.ts +28 -56
  112. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  113. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  114. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  115. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  116. package/src/__tests__/require-fresh-approval.test.ts +40 -3
  117. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  118. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  119. package/src/__tests__/schedule-routes.test.ts +162 -0
  120. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  121. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  122. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  123. package/src/__tests__/set-permission-mode.test.ts +13 -250
  124. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  125. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  126. package/src/__tests__/slack-channel-config.test.ts +12 -15
  127. package/src/__tests__/subagent-detail.test.ts +44 -2
  128. package/src/__tests__/subagent-disposal.test.ts +1 -0
  129. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  130. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  131. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  132. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  133. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  134. package/src/__tests__/subagent-tools.test.ts +1 -0
  135. package/src/__tests__/subagent-types.test.ts +1 -0
  136. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  137. package/src/__tests__/system-prompt.test.ts +72 -1
  138. package/src/__tests__/task-scheduler.test.ts +32 -6
  139. package/src/__tests__/telegram-config.test.ts +10 -13
  140. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  141. package/src/__tests__/terminal-tools.test.ts +11 -5
  142. package/src/__tests__/test-preload.ts +14 -0
  143. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  144. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  145. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  146. package/src/__tests__/tool-executor.test.ts +0 -1
  147. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  148. package/src/__tests__/top-level-renderer.test.ts +73 -1
  149. package/src/__tests__/transport-hints-queue.test.ts +62 -0
  150. package/src/__tests__/trust-store.test.ts +4 -4
  151. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  152. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  153. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  154. package/src/__tests__/workspace-policy.test.ts +2 -7
  155. package/src/acp/client-handler.ts +30 -4
  156. package/src/agent/loop.ts +12 -35
  157. package/src/approvals/guardian-request-resolvers.ts +21 -15
  158. package/src/browser-session/__tests__/manager.test.ts +297 -0
  159. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  160. package/src/browser-session/backends/extension.ts +26 -0
  161. package/src/browser-session/backends/local.ts +24 -0
  162. package/src/browser-session/events.ts +164 -0
  163. package/src/browser-session/index.ts +27 -0
  164. package/src/browser-session/manager.ts +159 -0
  165. package/src/browser-session/types.ts +28 -0
  166. package/src/channels/__tests__/types.test.ts +134 -0
  167. package/src/channels/types.ts +55 -0
  168. package/src/cli/__tests__/run-assistant-command.ts +34 -7
  169. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  170. package/src/cli/commands/browser-relay.ts +339 -409
  171. package/src/cli/commands/credentials.ts +3 -3
  172. package/src/cli/commands/default-action.ts +68 -1
  173. package/src/cli/commands/email.ts +18 -13
  174. package/src/cli/commands/mcp.ts +16 -4
  175. package/src/cli/commands/oauth/__tests__/connect.test.ts +68 -41
  176. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  177. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  178. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  179. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  180. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  181. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  182. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  183. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  184. package/src/cli/commands/oauth/apps.ts +7 -4
  185. package/src/cli/commands/oauth/connect.ts +16 -2
  186. package/src/cli/commands/oauth/disconnect.ts +1 -1
  187. package/src/cli/commands/oauth/providers.ts +200 -36
  188. package/src/cli/commands/oauth/shared.ts +5 -5
  189. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  190. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  191. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  192. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  193. package/src/cli/commands/platform/index.ts +107 -10
  194. package/src/cli/commands/usage.ts +10 -9
  195. package/src/cli/lib/daemon-credential-client.ts +4 -0
  196. package/src/cli/program.ts +10 -3
  197. package/src/config/assistant-feature-flags.ts +59 -55
  198. package/src/config/bundled-skills/app-builder/SKILL.md +33 -173
  199. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  200. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  201. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  202. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  203. package/src/config/bundled-skills/document/SKILL.md +4 -0
  204. package/src/config/bundled-skills/gmail/SKILL.md +12 -7
  205. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  206. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  207. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  208. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  209. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  210. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  211. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  212. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  213. package/src/config/env-registry.ts +14 -0
  214. package/src/config/env.ts +21 -0
  215. package/src/config/feature-flag-registry.json +46 -7
  216. package/src/config/loader.ts +56 -1
  217. package/src/config/sanitize-for-transfer.ts +47 -0
  218. package/src/config/schema.ts +46 -5
  219. package/src/config/schemas/host-browser.ts +66 -0
  220. package/src/config/schemas/memory-lifecycle.ts +1 -1
  221. package/src/config/schemas/memory-retrieval.ts +103 -0
  222. package/src/config/schemas/security.ts +0 -6
  223. package/src/config/schemas/services.ts +16 -0
  224. package/src/config/types.ts +0 -1
  225. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  226. package/src/context/window-manager.ts +19 -1
  227. package/src/credential-execution/approval-bridge.ts +49 -16
  228. package/src/credential-execution/managed-catalog.ts +3 -7
  229. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  230. package/src/daemon/app-source-watcher.ts +35 -0
  231. package/src/daemon/config-watcher.ts +6 -2
  232. package/src/daemon/context-overflow-approval.ts +5 -1
  233. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  234. package/src/daemon/conversation-agent-loop.ts +74 -19
  235. package/src/daemon/conversation-attachments.ts +40 -1
  236. package/src/daemon/conversation-messaging.ts +3 -0
  237. package/src/daemon/conversation-process.ts +66 -3
  238. package/src/daemon/conversation-queue-manager.ts +8 -0
  239. package/src/daemon/conversation-runtime-assembly.ts +159 -20
  240. package/src/daemon/conversation-surfaces.ts +78 -12
  241. package/src/daemon/conversation-tool-setup.ts +74 -11
  242. package/src/daemon/conversation-workspace.ts +12 -0
  243. package/src/daemon/conversation.ts +227 -11
  244. package/src/daemon/date-context.ts +10 -10
  245. package/src/daemon/first-greeting.ts +3 -2
  246. package/src/daemon/handlers/conversations.ts +9 -139
  247. package/src/daemon/handlers/shared.ts +65 -0
  248. package/src/daemon/handlers/skills.ts +232 -37
  249. package/src/daemon/host-bash-proxy.ts +48 -13
  250. package/src/daemon/host-browser-proxy.ts +191 -0
  251. package/src/daemon/host-cu-proxy.ts +36 -11
  252. package/src/daemon/host-file-proxy.ts +57 -9
  253. package/src/daemon/lifecycle.ts +86 -12
  254. package/src/daemon/message-protocol.ts +7 -0
  255. package/src/daemon/message-types/conversations.ts +59 -13
  256. package/src/daemon/message-types/host-browser.ts +100 -0
  257. package/src/daemon/message-types/messages.ts +5 -6
  258. package/src/daemon/message-types/notifications.ts +12 -0
  259. package/src/daemon/message-types/settings.ts +12 -0
  260. package/src/daemon/message-types/skills.ts +10 -0
  261. package/src/daemon/message-types/subagents.ts +2 -0
  262. package/src/daemon/server.ts +112 -35
  263. package/src/daemon/tool-side-effects.ts +6 -0
  264. package/src/daemon/transport-hints.ts +14 -0
  265. package/src/inbound/platform-callback-registration.ts +18 -17
  266. package/src/index.ts +1 -1
  267. package/src/mcp/client.ts +59 -24
  268. package/src/memory/app-store.ts +31 -1
  269. package/src/memory/conversation-crud.ts +38 -10
  270. package/src/memory/conversation-directories.ts +39 -0
  271. package/src/memory/conversation-group-migration.ts +65 -5
  272. package/src/memory/conversation-starters-cadence.ts +76 -0
  273. package/src/memory/conversation-title-service.ts +5 -2
  274. package/src/memory/db-init.ts +12 -0
  275. package/src/memory/embedding-backend.test.ts +75 -0
  276. package/src/memory/embedding-backend.ts +131 -5
  277. package/src/memory/embedding-gemini.test.ts +54 -0
  278. package/src/memory/embedding-gemini.ts +20 -9
  279. package/src/memory/embedding-local.ts +177 -18
  280. package/src/memory/graph/capability-seed.ts +3 -5
  281. package/src/memory/graph/consolidation.ts +10 -23
  282. package/src/memory/graph/extraction-job.ts +15 -0
  283. package/src/memory/graph/retriever.ts +40 -22
  284. package/src/memory/graph/store.test.ts +7 -3
  285. package/src/memory/graph/store.ts +47 -12
  286. package/src/memory/group-crud.ts +25 -9
  287. package/src/memory/llm-usage-store.ts +45 -4
  288. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  289. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  290. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  291. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  292. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  293. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  294. package/src/memory/migrations/index.ts +6 -0
  295. package/src/memory/migrations/registry.ts +8 -0
  296. package/src/memory/schema/conversations.ts +1 -0
  297. package/src/memory/schema/oauth.ts +18 -13
  298. package/src/messaging/provider.ts +1 -1
  299. package/src/notifications/broadcaster.ts +6 -0
  300. package/src/notifications/conversation-pairing.ts +12 -4
  301. package/src/notifications/emit-signal.ts +14 -0
  302. package/src/notifications/signal.ts +11 -0
  303. package/src/oauth/AGENTS.md +76 -0
  304. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  305. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  306. package/src/oauth/byo-connection.test.ts +8 -8
  307. package/src/oauth/byo-connection.ts +7 -7
  308. package/src/oauth/connect-orchestrator.ts +23 -21
  309. package/src/oauth/connect-types.ts +3 -3
  310. package/src/oauth/connection-resolver.test.ts +17 -4
  311. package/src/oauth/connection-resolver.ts +16 -16
  312. package/src/oauth/connection.ts +1 -1
  313. package/src/oauth/manual-token-connection.ts +13 -13
  314. package/src/oauth/oauth-store.ts +214 -100
  315. package/src/oauth/platform-connection.test.ts +5 -5
  316. package/src/oauth/platform-connection.ts +4 -4
  317. package/src/oauth/provider-serializer.ts +31 -5
  318. package/src/oauth/revoke.ts +76 -0
  319. package/src/oauth/seed-providers.ts +127 -87
  320. package/src/oauth/token-persistence.ts +1 -1
  321. package/src/permissions/checker.ts +3 -3
  322. package/src/permissions/defaults.ts +7 -8
  323. package/src/permissions/permission-mode.ts +4 -11
  324. package/src/permissions/prompter.ts +13 -3
  325. package/src/permissions/v2-consent-policy.ts +87 -0
  326. package/src/platform/client.ts +1 -1
  327. package/src/prompts/system-prompt.ts +18 -21
  328. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  329. package/src/prompts/templates/BOOTSTRAP.md +59 -96
  330. package/src/prompts/templates/SOUL.md +11 -11
  331. package/src/providers/anthropic/client.ts +1 -0
  332. package/src/providers/types.ts +1 -1
  333. package/src/runtime/AGENTS.md +23 -0
  334. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  335. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  336. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  337. package/src/runtime/assistant-event-hub.ts +24 -2
  338. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  339. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  341. package/src/runtime/auth/middleware.ts +98 -0
  342. package/src/runtime/auth/route-policy.ts +6 -7
  343. package/src/runtime/auth/token-service.ts +8 -0
  344. package/src/runtime/capability-tokens.ts +414 -0
  345. package/src/runtime/channel-approvals.ts +18 -5
  346. package/src/runtime/chrome-extension-registry.ts +332 -0
  347. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  348. package/src/runtime/guardian-decision-types.ts +7 -0
  349. package/src/runtime/http-server.ts +425 -70
  350. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  351. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  352. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  353. package/src/runtime/migrations/migration-transport.ts +6 -0
  354. package/src/runtime/migrations/migration-wizard.ts +22 -2
  355. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  356. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  357. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  358. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  359. package/src/runtime/pending-interactions.ts +29 -13
  360. package/src/runtime/routes/approval-routes.ts +90 -16
  361. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  362. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  363. package/src/runtime/routes/conversation-analysis-routes.ts +18 -5
  364. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  365. package/src/runtime/routes/conversation-routes.ts +308 -28
  366. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  367. package/src/runtime/routes/group-routes.ts +22 -8
  368. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  369. package/src/runtime/routes/host-browser-routes.ts +279 -0
  370. package/src/runtime/routes/host-file-routes.ts +9 -1
  371. package/src/runtime/routes/identity-routes.ts +259 -16
  372. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  373. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  374. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  375. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  376. package/src/runtime/routes/log-export-routes.ts +60 -25
  377. package/src/runtime/routes/memory-item-routes.ts +1 -7
  378. package/src/runtime/routes/migration-routes.ts +87 -2
  379. package/src/runtime/routes/oauth-apps.ts +15 -17
  380. package/src/runtime/routes/oauth-providers.ts +4 -0
  381. package/src/runtime/routes/schedule-routes.ts +24 -11
  382. package/src/runtime/routes/settings-routes.ts +9 -97
  383. package/src/runtime/routes/skills-routes.ts +52 -2
  384. package/src/runtime/routes/subagents-routes.ts +14 -10
  385. package/src/runtime/routes/usage-routes.ts +8 -7
  386. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  387. package/src/runtime/routes/workspace-routes.ts +8 -1
  388. package/src/runtime/routes/workspace-utils.ts +2 -0
  389. package/src/schedule/scheduler.ts +7 -5
  390. package/src/security/ces-credential-client.ts +20 -0
  391. package/src/security/ces-rpc-credential-backend.ts +17 -0
  392. package/src/security/credential-backend.ts +5 -0
  393. package/src/security/oauth2.ts +42 -25
  394. package/src/security/secure-keys.ts +118 -25
  395. package/src/security/token-manager.ts +23 -10
  396. package/src/skills/catalog-files.ts +492 -0
  397. package/src/skills/inline-command-runner.ts +12 -14
  398. package/src/subagent/manager.ts +131 -26
  399. package/src/subagent/types.ts +19 -0
  400. package/src/tools/apps/executors.ts +11 -2
  401. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  402. package/src/tools/browser/auth-detector.ts +43 -12
  403. package/src/tools/browser/browser-execution.ts +645 -340
  404. package/src/tools/browser/browser-manager.ts +36 -12
  405. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  406. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  407. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  408. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  409. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  410. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  411. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  412. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  413. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  414. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  415. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  416. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  417. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  418. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  419. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  420. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  421. package/src/tools/browser/cdp-client/errors.ts +34 -0
  422. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  423. package/src/tools/browser/cdp-client/factory.ts +204 -0
  424. package/src/tools/browser/cdp-client/index.ts +14 -0
  425. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  426. package/src/tools/browser/cdp-client/types.ts +52 -0
  427. package/src/tools/filesystem/edit.ts +1 -1
  428. package/src/tools/filesystem/list.ts +1 -1
  429. package/src/tools/filesystem/read.ts +1 -1
  430. package/src/tools/filesystem/write.ts +2 -1
  431. package/src/tools/host-filesystem/edit.ts +1 -1
  432. package/src/tools/host-filesystem/read.ts +12 -15
  433. package/src/tools/host-filesystem/write.ts +1 -1
  434. package/src/tools/host-terminal/host-shell.ts +21 -16
  435. package/src/tools/permission-checker.ts +77 -100
  436. package/src/tools/registry.ts +0 -2
  437. package/src/tools/secret-detection-handler.ts +34 -1
  438. package/src/tools/shared/filesystem/image-read.ts +61 -40
  439. package/src/tools/skills/sandbox-runner.ts +3 -6
  440. package/src/tools/subagent/spawn.ts +47 -3
  441. package/src/tools/subagent/status.ts +2 -0
  442. package/src/tools/system/register.ts +2 -16
  443. package/src/tools/terminal/safe-env.ts +7 -0
  444. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  445. package/src/tools/terminal/sandbox.ts +4 -1
  446. package/src/tools/terminal/shell.ts +24 -21
  447. package/src/tools/tool-approval-handler.ts +48 -2
  448. package/src/tools/types.ts +2 -3
  449. package/src/util/platform.ts +14 -19
  450. package/src/watcher/provider-types.ts +1 -1
  451. package/src/workspace/migrations/029-seed-pkb.ts +1 -0
  452. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  453. package/src/workspace/migrations/registry.ts +2 -0
  454. package/src/workspace/top-level-renderer.ts +19 -1
  455. package/src/__tests__/chrome-cdp.test.ts +0 -419
  456. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  457. package/src/__tests__/permission-mode-store.test.ts +0 -277
  458. package/src/browser-extension-relay/protocol.ts +0 -63
  459. package/src/browser-extension-relay/server.ts +0 -203
  460. package/src/config/schemas/sandbox.ts +0 -14
  461. package/src/permissions/permission-mode-store.ts +0 -180
  462. package/src/tools/browser/chrome-cdp.ts +0 -239
  463. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -4,6 +4,8 @@
4
4
  * POST /v1/conversations — create a new conversation
5
5
  * POST /v1/conversations/switch — switch to an existing conversation
6
6
  * POST /v1/conversations/fork — fork an existing conversation
7
+ * GET /v1/conversations/:id/host-access — read host access for one conversation
8
+ * PATCH /v1/conversations/:id/host-access — update host access for one conversation
7
9
  * PATCH /v1/conversations/:id/name — rename a conversation
8
10
  * DELETE /v1/conversations — clear all conversations
9
11
  * POST /v1/conversations/:id/wipe — wipe conversation and revert memory
@@ -21,7 +23,9 @@ import {
21
23
  countConversationsByScheduleJobId,
22
24
  deleteConversation,
23
25
  getConversation,
26
+ getConversationHostAccess,
24
27
  PRIVATE_CONVERSATION_FORK_ERROR,
28
+ updateConversationHostAccess,
25
29
  wipeConversation,
26
30
  } from "../../memory/conversation-crud.js";
27
31
  import { updateConversationTitle } from "../../memory/conversation-crud.js";
@@ -34,6 +38,10 @@ import { enqueueMemoryJob } from "../../memory/jobs-store.js";
34
38
  import { deleteSchedule } from "../../schedule/schedule-store.js";
35
39
  import { UserError } from "../../util/errors.js";
36
40
  import { getLogger } from "../../util/logger.js";
41
+ import { buildAssistantEvent } from "../assistant-event.js";
42
+ import { assistantEventHub } from "../assistant-event-hub.js";
43
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
44
+ import { requireBoundGuardian } from "../auth/require-bound-guardian.js";
37
45
  import { httpError } from "../http-errors.js";
38
46
  import type { RouteDefinition } from "../http-router.js";
39
47
 
@@ -52,6 +60,7 @@ export interface ConversationManagementDeps {
52
60
  conversationId: string;
53
61
  title: string;
54
62
  conversationType: string;
63
+ hostAccess: boolean;
55
64
  } | null>;
56
65
  renameConversation: (conversationId: string, name: string) => boolean;
57
66
  clearAllConversations: () => number;
@@ -218,6 +227,7 @@ export function conversationManagementRouteDefinitions(
218
227
  conversationId: z.string(),
219
228
  title: z.string(),
220
229
  conversationType: z.string(),
230
+ hostAccess: z.boolean(),
221
231
  }),
222
232
  handler: async ({ req }) => {
223
233
  const body = (await req.json()) as {
@@ -246,6 +256,104 @@ export function conversationManagementRouteDefinitions(
246
256
  title: result.title,
247
257
  conversationType:
248
258
  result.conversationType === "private" ? "private" : "standard",
259
+ hostAccess: result.hostAccess,
260
+ });
261
+ },
262
+ },
263
+ {
264
+ endpoint: "conversations/:id/host-access",
265
+ method: "GET",
266
+ policyKey: "conversations/host-access:GET",
267
+ summary: "Get conversation host access",
268
+ description: "Return whether the conversation can use host tools.",
269
+ tags: ["conversations"],
270
+ responseBody: z.object({
271
+ conversationId: z.string(),
272
+ hostAccess: z.boolean(),
273
+ }),
274
+ handler: ({ params }) => {
275
+ const resolvedId = resolveConversationId(params.id) ?? params.id;
276
+ const conversation = getConversation(resolvedId);
277
+ if (!conversation) {
278
+ return httpError(
279
+ "NOT_FOUND",
280
+ `Conversation ${params.id} not found`,
281
+ 404,
282
+ );
283
+ }
284
+ return Response.json({
285
+ conversationId: conversation.id,
286
+ hostAccess: getConversationHostAccess(conversation.id),
287
+ });
288
+ },
289
+ },
290
+ {
291
+ endpoint: "conversations/:id/host-access",
292
+ method: "PATCH",
293
+ policyKey: "conversations/host-access",
294
+ summary: "Update conversation host access",
295
+ description: "Enable or disable host access for a conversation.",
296
+ tags: ["conversations"],
297
+ requestBody: z.object({
298
+ hostAccess: z.boolean(),
299
+ }),
300
+ responseBody: z.object({
301
+ conversationId: z.string(),
302
+ hostAccess: z.boolean(),
303
+ }),
304
+ handler: async ({ req, params, authContext }) => {
305
+ const guardianError = requireBoundGuardian(authContext);
306
+ if (guardianError) return guardianError;
307
+
308
+ const rawBody = (await req.json()) as unknown;
309
+ if (
310
+ rawBody == null ||
311
+ typeof rawBody !== "object" ||
312
+ Array.isArray(rawBody)
313
+ ) {
314
+ return httpError("BAD_REQUEST", "Invalid request body", 400);
315
+ }
316
+ const body = rawBody as { hostAccess?: unknown };
317
+ if (typeof body.hostAccess !== "boolean") {
318
+ return httpError("BAD_REQUEST", "Missing hostAccess boolean", 400);
319
+ }
320
+
321
+ const resolvedId = resolveConversationId(params.id) ?? params.id;
322
+ const conversation = getConversation(resolvedId);
323
+ if (!conversation) {
324
+ return httpError(
325
+ "NOT_FOUND",
326
+ `Conversation ${params.id} not found`,
327
+ 404,
328
+ );
329
+ }
330
+
331
+ const nextHostAccess = body.hostAccess;
332
+ if (conversation.hostAccess !== (nextHostAccess ? 1 : 0)) {
333
+ updateConversationHostAccess(resolvedId, nextHostAccess);
334
+ assistantEventHub
335
+ .publish(
336
+ buildAssistantEvent(
337
+ DAEMON_INTERNAL_ASSISTANT_ID,
338
+ {
339
+ type: "conversation_host_access_updated",
340
+ conversationId: resolvedId,
341
+ hostAccess: nextHostAccess,
342
+ },
343
+ resolvedId,
344
+ ),
345
+ )
346
+ .catch((err) => {
347
+ log.warn(
348
+ { err, conversationId: resolvedId },
349
+ "Failed to publish conversation_host_access_updated event",
350
+ );
351
+ });
352
+ }
353
+
354
+ return Response.json({
355
+ conversationId: resolvedId,
356
+ hostAccess: nextHostAccess,
249
357
  });
250
358
  },
251
359
  },
@@ -17,6 +17,7 @@ import {
17
17
  isInteractiveInterface,
18
18
  parseChannelId,
19
19
  parseInterfaceId,
20
+ supportsHostProxy,
20
21
  } from "../../channels/types.js";
21
22
  import { isHttpAuthDisabled } from "../../config/env.js";
22
23
  import { getConfig } from "../../config/loader.js";
@@ -35,12 +36,13 @@ import {
35
36
  } from "../../daemon/first-greeting.js";
36
37
  import { renderHistoryContent } from "../../daemon/handlers/shared.js";
37
38
  import { HostBashProxy } from "../../daemon/host-bash-proxy.js";
39
+ import { HostBrowserProxy } from "../../daemon/host-browser-proxy.js";
38
40
  import { HostCuProxy } from "../../daemon/host-cu-proxy.js";
39
41
  import { HostFileProxy } from "../../daemon/host-file-proxy.js";
40
42
  import type { ServerMessage } from "../../daemon/message-protocol.js";
41
43
  import type {
42
- MacosTransportMetadata,
43
- NonMacosTransportMetadata,
44
+ HostProxyTransportMetadata,
45
+ NonHostProxyTransportMetadata,
44
46
  } from "../../daemon/message-types/conversations.js";
45
47
  import type { HeartbeatService } from "../../heartbeat/heartbeat-service.js";
46
48
  import * as attachmentsStore from "../../memory/attachments-store.js";
@@ -76,6 +78,7 @@ import { silentlyWithLog } from "../../util/silently.js";
76
78
  import { buildAssistantEvent } from "../assistant-event.js";
77
79
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
78
80
  import type { AuthContext } from "../auth/types.js";
81
+ import { getChromeExtensionRegistry } from "../chrome-extension-registry.js";
79
82
  import { bridgeConfirmationRequestToGuardian } from "../confirmation-request-guardian-bridge.js";
80
83
  import { routeGuardianReply } from "../guardian-reply-router.js";
81
84
  import { healGuardianBindingDrift } from "../guardian-vellum-migration.js";
@@ -421,8 +424,18 @@ export function handleListMessages(
421
424
  // pendingToolUses map — otherwise they render as "Unknown" tool calls.
422
425
  const mergedMessages = mergeToolResultsIntoAssistantMessages(rawMessages);
423
426
 
427
+ // During streaming, all assistant turns within one agent loop accumulate
428
+ // on a single client-side ChatMessage (via currentAssistantMessageId).
429
+ // In the DB, each API turn is a separate assistant row because
430
+ // consolidation is deferred to compaction for prefix-cache stability.
431
+ // Merge consecutive assistant messages here at query time so
432
+ // renderHistoryContent produces the same contentOrder shape as streaming
433
+ // (consecutive tool refs grouped together).
434
+ const { messages: consolidatedMessages, mergedIdMap } =
435
+ mergeConsecutiveAssistantMessages(mergedMessages);
436
+
424
437
  // Parse content blocks and extract text + tool calls
425
- const parsed = mergedMessages.map((msg) => {
438
+ const parsed = consolidatedMessages.map((msg) => {
426
439
  let content: unknown;
427
440
  try {
428
441
  content = JSON.parse(msg.content);
@@ -547,7 +560,13 @@ export function handleListMessages(
547
560
  // blobs for non-image attachments (documents, audio). Then
548
561
  // selectively fetch full data only for images so the client can
549
562
  // generate thumbnails for inline display on history restore.
550
- const linked = attachmentsStore.getAttachmentMetadataForMessage(m.id);
563
+ // Also query attachments for any messages that were merged into
564
+ // this one (consecutive assistant merge), so their attachments
565
+ // aren't lost before DB compaction relinks them.
566
+ const idsToQuery = [m.id, ...(mergedIdMap.get(m.id) ?? [])];
567
+ const linked = idsToQuery.flatMap((id) =>
568
+ attachmentsStore.getAttachmentMetadataForMessage(id),
569
+ );
551
570
  if (linked.length > 0) {
552
571
  msgAttachments = linked.map((a) => {
553
572
  if (a.mimeType.startsWith("image/")) {
@@ -721,7 +740,10 @@ function mergeToolResultsIntoAssistantMessages(
721
740
  }
722
741
  }
723
742
 
724
- // No tool results → pass through unchanged.
743
+ // No tool results → pass through unchanged. System notices are only
744
+ // injected alongside tool results in the agent loop, so a pure user
745
+ // message (no tool_result blocks) should never be filtered — even if
746
+ // the user's text happens to look like a system_notice tag.
725
747
  if (toolResultBlocks.length === 0) {
726
748
  result.push(msg);
727
749
  continue;
@@ -789,14 +811,158 @@ function mergeToolResultsIntoAssistantMessages(
789
811
  return result;
790
812
  }
791
813
 
814
+ // ── Consecutive assistant message merging ────────────────────────────
815
+
816
+ /** Parse a message's JSON content into an array of content blocks. */
817
+ function parseContentBlocks(content: string): unknown[] {
818
+ try {
819
+ const parsed = JSON.parse(content);
820
+ return Array.isArray(parsed) ? parsed : [parsed];
821
+ } catch (err) {
822
+ log.warn(
823
+ { err },
824
+ "Failed to parse content blocks during assistant message merge",
825
+ );
826
+ return [];
827
+ }
828
+ }
829
+
830
+ /**
831
+ * Append content blocks from a donor message onto a target block array.
832
+ * Parses the donor's JSON content and pushes each block into `target`.
833
+ */
834
+ function appendContentBlocks(target: unknown[], donorContent: string): void {
835
+ try {
836
+ const parsed = JSON.parse(donorContent);
837
+ if (Array.isArray(parsed)) {
838
+ target.push(...parsed);
839
+ } else {
840
+ target.push(parsed);
841
+ }
842
+ } catch (err) {
843
+ log.warn(
844
+ { err },
845
+ "Failed to parse donor content blocks during assistant message merge",
846
+ );
847
+ }
848
+ }
849
+
850
+ /**
851
+ * Promote metadata fields from a donor message to the surviving message
852
+ * when the survivor lacks them. Currently promotes `subagentNotification`.
853
+ * Returns a new MessageRow if promotion occurred, otherwise the original.
854
+ */
855
+ function promoteMetadata(survivor: MessageRow, donor: MessageRow): MessageRow {
856
+ if (donor.metadata && survivor.metadata) {
857
+ try {
858
+ const survivorMeta = JSON.parse(survivor.metadata);
859
+ const donorMeta = JSON.parse(donor.metadata);
860
+ if (
861
+ !survivorMeta.subagentNotification &&
862
+ donorMeta.subagentNotification
863
+ ) {
864
+ survivorMeta.subagentNotification = donorMeta.subagentNotification;
865
+ return { ...survivor, metadata: JSON.stringify(survivorMeta) };
866
+ }
867
+ } catch (err) {
868
+ log.warn(
869
+ { err },
870
+ "Failed to parse metadata during assistant message merge",
871
+ );
872
+ }
873
+ } else if (donor.metadata && !survivor.metadata) {
874
+ return { ...survivor, metadata: donor.metadata };
875
+ }
876
+ return survivor;
877
+ }
878
+
879
+ /**
880
+ * Merge consecutive assistant messages into a single message at query time.
881
+ *
882
+ * During streaming, all assistant turns within one agent loop accumulate on
883
+ * a single client-side ChatMessage. In the DB, each API turn is stored as a
884
+ * separate assistant row (consolidation is deferred to compaction for
885
+ * prefix-cache stability). This produces N separate assistant messages that
886
+ * the client renders as N individual bubbles — each showing "Completed 1
887
+ * step" instead of one grouped "Completed N steps" accordion.
888
+ *
889
+ * This function concatenates the content block arrays of consecutive
890
+ * assistant messages (no intervening user messages after tool-result
891
+ * merging) into the first message of each run. The merged messages are
892
+ * removed from the output. This is query-time only — the DB is not
893
+ * modified.
894
+ *
895
+ * The first message in each run keeps its id, createdAt, and metadata so
896
+ * that attachment lookups, display timestamps, and subagent notifications
897
+ * continue to work. Metadata from later messages in the run (e.g.
898
+ * subagentNotification) is preserved by promoting it to the surviving
899
+ * message when the surviving message has no metadata of its own for that
900
+ * field.
901
+ */
902
+ function mergeConsecutiveAssistantMessages(messages: MessageRow[]): {
903
+ messages: MessageRow[];
904
+ /** Maps each surviving message ID → all original message IDs merged into it. */
905
+ mergedIdMap: Map<string, string[]>;
906
+ } {
907
+ const result: MessageRow[] = [];
908
+ // Key = index in `result`, value = accumulated content blocks.
909
+ const pendingMerges = new Map<number, unknown[]>();
910
+ // Key = index in `result`, value = IDs of messages merged into the target.
911
+ const mergedIds = new Map<number, string[]>();
912
+
913
+ for (const msg of messages) {
914
+ const lastIdx = result.length - 1;
915
+ const isConsecutiveAssistant =
916
+ msg.role === "assistant" &&
917
+ lastIdx >= 0 &&
918
+ result[lastIdx].role === "assistant";
919
+
920
+ if (!isConsecutiveAssistant) {
921
+ result.push(msg);
922
+ continue;
923
+ }
924
+
925
+ // Track the donor message ID.
926
+ let ids = mergedIds.get(lastIdx);
927
+ if (!ids) {
928
+ ids = [];
929
+ mergedIds.set(lastIdx, ids);
930
+ }
931
+ ids.push(msg.id);
932
+
933
+ // Lazily parse the target's content on first merge.
934
+ let targetContent = pendingMerges.get(lastIdx);
935
+ if (!targetContent) {
936
+ targetContent = parseContentBlocks(result[lastIdx].content);
937
+ pendingMerges.set(lastIdx, targetContent);
938
+ }
939
+
940
+ appendContentBlocks(targetContent, msg.content);
941
+ result[lastIdx] = promoteMetadata(result[lastIdx], msg);
942
+ }
943
+
944
+ // Write back merged content for any messages that were targets.
945
+ for (const [idx, content] of pendingMerges) {
946
+ result[idx] = { ...result[idx], content: JSON.stringify(content) };
947
+ }
948
+
949
+ // Build the merged ID map keyed by surviving message ID.
950
+ const mergedIdMap = new Map<string, string[]>();
951
+ for (const [idx, ids] of mergedIds) {
952
+ mergedIdMap.set(result[idx].id, ids);
953
+ }
954
+
955
+ return { messages: result, mergedIdMap };
956
+ }
957
+
792
958
  /**
793
959
  * Build an `onEvent` callback that publishes every outbound event to the
794
960
  * assistant event hub, maintaining ordered delivery through a serial chain.
795
961
  *
796
962
  * Also registers pending interactions when confirmation_request,
797
- * secret_request, host_bash_request, or host_file_request events flow
798
- * through, so standalone approval/result endpoints can look up the conversation
799
- * by requestId.
963
+ * secret_request, host_bash_request, host_browser_request, host_file_request,
964
+ * or host_cu_request events flow through, so standalone approval/result
965
+ * endpoints can look up the conversation by requestId.
800
966
  */
801
967
  function makeHubPublisher(
802
968
  deps: SendMessageDeps,
@@ -888,6 +1054,12 @@ function makeHubPublisher(
888
1054
  conversationId,
889
1055
  kind: "host_bash",
890
1056
  });
1057
+ } else if (msg.type === "host_browser_request") {
1058
+ pendingInteractions.register(msg.requestId, {
1059
+ conversation,
1060
+ conversationId,
1061
+ kind: "host_browser",
1062
+ });
891
1063
  } else if (msg.type === "host_file_request") {
892
1064
  pendingInteractions.register(msg.requestId, {
893
1065
  conversation,
@@ -1054,18 +1226,21 @@ export async function handleSendMessage(
1054
1226
 
1055
1227
  // Build transport metadata from the request so the daemon can inject
1056
1228
  // host environment hints (home directory, username) into the LLM context.
1057
- const transport =
1058
- sourceInterface === "macos"
1059
- ? ({
1060
- channelId: sourceChannel,
1061
- interfaceId: "macos" as const,
1062
- hostHomeDir: body.hostHomeDir,
1063
- hostUsername: body.hostUsername,
1064
- } satisfies MacosTransportMetadata)
1065
- : ({
1066
- channelId: sourceChannel,
1067
- interfaceId: sourceInterface,
1068
- } satisfies NonMacosTransportMetadata);
1229
+ // The `supportsHostProxy` type predicate narrows `sourceInterface` to
1230
+ // `HostProxyInterfaceId` in the truthy branch, which is exactly the
1231
+ // discriminant the `HostProxyTransportMetadata` variant expects — so the
1232
+ // construction site stays in lock-step with the runtime capability gate.
1233
+ const transport = supportsHostProxy(sourceInterface)
1234
+ ? ({
1235
+ channelId: sourceChannel,
1236
+ interfaceId: sourceInterface,
1237
+ hostHomeDir: body.hostHomeDir,
1238
+ hostUsername: body.hostUsername,
1239
+ } satisfies HostProxyTransportMetadata)
1240
+ : ({
1241
+ channelId: sourceChannel,
1242
+ interfaceId: sourceInterface,
1243
+ } satisfies NonHostProxyTransportMetadata);
1069
1244
 
1070
1245
  const conversation = await smDeps.getOrCreateConversation(
1071
1246
  mapping.conversationId,
@@ -1135,12 +1310,13 @@ export async function handleSendMessage(
1135
1310
  conversation,
1136
1311
  );
1137
1312
  const isInteractive = isInteractiveInterface(sourceInterface);
1138
- // Only create the host bash proxy for desktop client interfaces that can
1139
- // execute commands on the user's machine. Non-desktop conversations (CLI,
1140
- // channels, headless) fall back to local execution.
1313
+ // Only create each host proxy for interfaces that support the matching
1314
+ // capability. macOS supports all four; the chrome-extension interface only
1315
+ // supports host_browser. Non-desktop conversations (CLI, channels, headless)
1316
+ // fall back to local execution.
1141
1317
  // Set the proxy BEFORE updateClient so updateClient's call to
1142
1318
  // hostBashProxy.updateSender targets the correct (new) proxy.
1143
- if (sourceInterface === "macos") {
1319
+ if (supportsHostProxy(sourceInterface, "host_bash")) {
1144
1320
  // Reuse the existing proxy if the conversation is actively processing a
1145
1321
  // host bash request to avoid orphaning in-flight requests.
1146
1322
  if (!conversation.isProcessing() || !conversation.hostBashProxy) {
@@ -1149,12 +1325,92 @@ export async function handleSendMessage(
1149
1325
  });
1150
1326
  conversation.setHostBashProxy(proxy);
1151
1327
  }
1328
+ } else if (!conversation.isProcessing()) {
1329
+ conversation.setHostBashProxy(undefined);
1330
+ }
1331
+ // For the chrome-extension interface we route host_browser_request /
1332
+ // host_browser_cancel frames through the in-process ChromeExtensionRegistry
1333
+ // to the WebSocket opened against /v1/browser-relay by the connected
1334
+ // extension, instead of the SSE/onEvent hub used by macOS. The registry
1335
+ // lookup is keyed by the JWT-derived actor principal id, which the
1336
+ // runtime captured at WebSocket upgrade time.
1337
+ //
1338
+ // A single guardian may have multiple parallel extension installs
1339
+ // connected at once (two Chrome profiles, two desktops). The registry
1340
+ // tracks them under (guardianId, clientInstanceId) pairs and the
1341
+ // default `send(guardianId, msg)` path routes to whichever instance
1342
+ // has the most recent activity — typically the one the user is
1343
+ // currently driving. Pinning to a specific instance can be done via
1344
+ // `sendToInstance` if a caller ever needs it.
1345
+ //
1346
+ // macOS (and any other interface that supports host_browser in the
1347
+ // future via the SSE hub) keeps using `onEvent` — see the else branch.
1348
+ const browserProxySendToClient: (msg: ServerMessage) => void =
1349
+ sourceInterface === "chrome-extension"
1350
+ ? (msg) => {
1351
+ // Resolve the guardian principal id at send time rather than
1352
+ // capturing it from the POST-time authContext. This closure can be
1353
+ // re-fired on queue drain — if a different actor's POST lands while
1354
+ // the queue is still draining an earlier turn, a captured
1355
+ // authContext.actorPrincipalId would mis-route the earlier turn's
1356
+ // host_browser frames to the *new* actor. Preferring
1357
+ // conversation.trustContext?.guardianPrincipalId makes the routing
1358
+ // follow the conversation's bound guardian, which is stable across
1359
+ // subsequent POSTs. Falls back to the per-POST authContext for
1360
+ // turns that haven't been bound to a trust context yet.
1361
+ const gid =
1362
+ conversation.trustContext?.guardianPrincipalId ??
1363
+ authContext.actorPrincipalId;
1364
+ if (!gid) {
1365
+ // No guardian identity on this turn — nothing to route to.
1366
+ // The proxy will observe this via its try/catch and surface a
1367
+ // transport error back to the caller.
1368
+ throw new Error(
1369
+ "chrome-extension host_browser send skipped: no guardianId on AuthContext",
1370
+ );
1371
+ }
1372
+ const ok = getChromeExtensionRegistry().send(gid, msg);
1373
+ if (!ok) {
1374
+ throw new Error(
1375
+ `chrome-extension host_browser send failed: no active connection for guardian ${gid}`,
1376
+ );
1377
+ }
1378
+ }
1379
+ : onEvent;
1380
+ // Stash the registry-routed sender on the conversation so queue-drain
1381
+ // restores (which run outside of conversation-routes.ts and only have
1382
+ // access to `sendToClient`) can preserve it when calling
1383
+ // `restoreBrowserProxyAvailability()`. For non-chrome-extension
1384
+ // interfaces the override is cleared so the SSE hub sender is used.
1385
+ if (sourceInterface === "chrome-extension") {
1386
+ conversation.hostBrowserSenderOverride = browserProxySendToClient;
1387
+ } else {
1388
+ conversation.hostBrowserSenderOverride = undefined;
1389
+ }
1390
+ if (supportsHostProxy(sourceInterface, "host_browser")) {
1391
+ if (!conversation.isProcessing() || !conversation.hostBrowserProxy) {
1392
+ const browserProxy = new HostBrowserProxy(
1393
+ browserProxySendToClient,
1394
+ (requestId) => {
1395
+ pendingInteractions.resolve(requestId);
1396
+ },
1397
+ );
1398
+ conversation.setHostBrowserProxy(browserProxy);
1399
+ }
1400
+ } else if (!conversation.isProcessing()) {
1401
+ conversation.setHostBrowserProxy(undefined);
1402
+ }
1403
+ if (supportsHostProxy(sourceInterface, "host_file")) {
1152
1404
  if (!conversation.isProcessing() || !conversation.hostFileProxy) {
1153
1405
  const fileProxy = new HostFileProxy(onEvent, (requestId) => {
1154
1406
  pendingInteractions.resolve(requestId);
1155
1407
  });
1156
1408
  conversation.setHostFileProxy(fileProxy);
1157
1409
  }
1410
+ } else if (!conversation.isProcessing()) {
1411
+ conversation.setHostFileProxy(undefined);
1412
+ }
1413
+ if (supportsHostProxy(sourceInterface, "host_cu")) {
1158
1414
  if (!conversation.isProcessing() || !conversation.hostCuProxy) {
1159
1415
  const cuProxy = new HostCuProxy(onEvent, (requestId) => {
1160
1416
  pendingInteractions.resolve(requestId);
@@ -1168,19 +1424,41 @@ export async function handleSendMessage(
1168
1424
  conversation.addPreactivatedSkillId("computer-use");
1169
1425
  }
1170
1426
  } else if (!conversation.isProcessing()) {
1171
- conversation.setHostBashProxy(undefined);
1172
- conversation.setHostFileProxy(undefined);
1173
1427
  conversation.setHostCuProxy(undefined);
1174
1428
  }
1175
1429
  // Wire sendToClient to the SSE hub so all subsystems can reach the HTTP client.
1176
1430
  // Called after setHostBashProxy so updateSender targets the current proxy.
1177
1431
  // When proxies are preserved during an active turn (non-desktop request while
1178
- // processing), skip updating proxy senders to avoid degrading them.
1432
+ // processing), skip updating proxy senders to avoid degrading them. The gate
1433
+ // matches the host_bash capability because the legacy "reject send during
1434
+ // host bash" flow is what this is really protecting.
1179
1435
  const preservingProxies =
1180
- conversation.isProcessing() && sourceInterface !== "macos";
1436
+ conversation.isProcessing() &&
1437
+ !supportsHostProxy(sourceInterface, "host_bash");
1438
+ // hasNoClient must remain `!isInteractive` so downstream tool gating
1439
+ // (`isToolActiveForContext` for HOST_TOOL_NAMES, `createToolExecutor`'s
1440
+ // `isInteractive: !ctx.hasNoClient`) keeps host_bash/host_file/host_cu
1441
+ // tools gated for non-desktop interfaces. The chrome-extension interface
1442
+ // is non-interactive (no SSE prompter UI) but still has a connected client
1443
+ // that can service host_browser_request events; we restore that single
1444
+ // proxy explicitly below without relaxing `hasNoClient`.
1181
1445
  conversation.updateClient(onEvent, !isInteractive, {
1182
1446
  skipProxySenderUpdate: preservingProxies,
1183
1447
  });
1448
+ // For non-interactive interfaces that DO support host_browser
1449
+ // (chrome-extension), explicitly re-enable just the browser proxy. The
1450
+ // helper bypasses the `hasNoClient` gate so the single-capability
1451
+ // chrome-extension turn can drive the browser via CDP without leaking
1452
+ // host_bash/host_file tool availability into tool gating.
1453
+ //
1454
+ // `restoreBrowserProxyAvailability()` reads `hostBrowserSenderOverride`
1455
+ // (set above for chrome-extension) and applies the registry-routed
1456
+ // sender, so the chrome-extension path gets the correct sender here
1457
+ // — including after queue-drain restores run from conversation-process.ts,
1458
+ // which only have access to the conversation instance.
1459
+ if (supportsHostProxy(sourceInterface, "host_browser")) {
1460
+ conversation.restoreBrowserProxyAvailability?.();
1461
+ }
1184
1462
 
1185
1463
  // ── Canned first-greeting fast path ──
1186
1464
  // On a completely fresh workspace, skip LLM inference for the macOS
@@ -1338,6 +1616,8 @@ export async function handleSendMessage(
1338
1616
  ...(body.automated === true ? { automated: true } : {}),
1339
1617
  },
1340
1618
  { isInteractive },
1619
+ undefined, // displayContent
1620
+ transport,
1341
1621
  );
1342
1622
  if (enqueueResult.rejected) {
1343
1623
  return Response.json(