@vellumai/assistant 0.6.2 → 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 (396) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docs/architecture/memory.md +1 -1
  4. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  5. package/openapi.yaml +184 -69
  6. package/package.json +41 -41
  7. package/scripts/generate-openapi.ts +1 -2
  8. package/src/__tests__/acp-session.test.ts +43 -0
  9. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  10. package/src/__tests__/app-executors.test.ts +1 -0
  11. package/src/__tests__/app-source-watcher.test.ts +37 -11
  12. package/src/__tests__/approval-routes-http.test.ts +178 -1
  13. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  14. package/src/__tests__/browser-manager.test.ts +40 -27
  15. package/src/__tests__/catalog-files.test.ts +862 -0
  16. package/src/__tests__/channel-approvals.test.ts +53 -0
  17. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +125 -48
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  21. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  22. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  24. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  25. package/src/__tests__/conversation-attachments.test.ts +80 -4
  26. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  27. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  28. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  29. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  30. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  31. package/src/__tests__/conversation-queue.test.ts +45 -2
  32. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  33. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  34. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  35. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  36. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  37. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  38. package/src/__tests__/conversation-store.test.ts +195 -0
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  40. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  41. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  42. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  43. package/src/__tests__/credential-vault.test.ts +152 -13
  44. package/src/__tests__/credentials-cli.test.ts +2 -2
  45. package/src/__tests__/date-context.test.ts +4 -4
  46. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  47. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  48. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  49. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  50. package/src/__tests__/gemini-provider.test.ts +2 -2
  51. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  52. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  53. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  54. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  55. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  56. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  57. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  58. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  59. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  60. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  61. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  62. package/src/__tests__/host-browser-routes.test.ts +198 -0
  63. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  64. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  65. package/src/__tests__/host-file-proxy.test.ts +185 -1
  66. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  67. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  68. package/src/__tests__/host-shell-tool.test.ts +1 -11
  69. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  70. package/src/__tests__/integration-status.test.ts +6 -7
  71. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  72. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  73. package/src/__tests__/mcp-health-check.test.ts +10 -3
  74. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  75. package/src/__tests__/migration-export-http.test.ts +61 -2
  76. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  77. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  78. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  80. package/src/__tests__/oauth-cli.test.ts +707 -60
  81. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  82. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  83. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  84. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  85. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  86. package/src/__tests__/oauth-store.test.ts +1386 -182
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  88. package/src/__tests__/onboarding-template-contract.test.ts +75 -57
  89. package/src/__tests__/openai-provider.test.ts +2 -2
  90. package/src/__tests__/outlook-categories.test.ts +1 -1
  91. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  92. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  93. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  94. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  95. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  96. package/src/__tests__/outlook-trash.test.ts +1 -1
  97. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  98. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  99. package/src/__tests__/permission-mode.test.ts +28 -56
  100. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  101. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  102. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  103. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  104. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  105. package/src/__tests__/schedule-routes.test.ts +162 -0
  106. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  107. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  108. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  109. package/src/__tests__/set-permission-mode.test.ts +13 -250
  110. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  111. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  112. package/src/__tests__/slack-channel-config.test.ts +12 -15
  113. package/src/__tests__/subagent-detail.test.ts +44 -2
  114. package/src/__tests__/subagent-disposal.test.ts +1 -0
  115. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  116. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  117. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  118. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  119. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  120. package/src/__tests__/subagent-tools.test.ts +1 -0
  121. package/src/__tests__/subagent-types.test.ts +1 -0
  122. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  123. package/src/__tests__/system-prompt.test.ts +72 -1
  124. package/src/__tests__/task-scheduler.test.ts +32 -6
  125. package/src/__tests__/telegram-config.test.ts +10 -13
  126. package/src/__tests__/terminal-tools.test.ts +9 -0
  127. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  128. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  129. package/src/__tests__/top-level-renderer.test.ts +73 -1
  130. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  131. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  132. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  133. package/src/acp/client-handler.ts +30 -4
  134. package/src/agent/loop.ts +12 -6
  135. package/src/approvals/guardian-request-resolvers.ts +21 -15
  136. package/src/browser-session/__tests__/manager.test.ts +297 -0
  137. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  138. package/src/browser-session/backends/extension.ts +26 -0
  139. package/src/browser-session/backends/local.ts +24 -0
  140. package/src/browser-session/events.ts +164 -0
  141. package/src/browser-session/index.ts +27 -0
  142. package/src/browser-session/manager.ts +159 -0
  143. package/src/browser-session/types.ts +28 -0
  144. package/src/channels/__tests__/types.test.ts +134 -0
  145. package/src/channels/types.ts +53 -3
  146. package/src/cli/commands/browser-relay.ts +339 -409
  147. package/src/cli/commands/credentials.ts +3 -3
  148. package/src/cli/commands/email.ts +18 -13
  149. package/src/cli/commands/mcp.ts +16 -4
  150. package/src/cli/commands/oauth/__tests__/connect.test.ts +44 -44
  151. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  152. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  153. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  154. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  155. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  156. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  157. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  158. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  159. package/src/cli/commands/oauth/apps.ts +7 -4
  160. package/src/cli/commands/oauth/connect.ts +6 -3
  161. package/src/cli/commands/oauth/disconnect.ts +1 -1
  162. package/src/cli/commands/oauth/providers.ts +200 -36
  163. package/src/cli/commands/oauth/shared.ts +5 -5
  164. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  165. package/src/cli/commands/platform/index.ts +107 -10
  166. package/src/cli/commands/usage.ts +10 -9
  167. package/src/cli/lib/daemon-credential-client.ts +4 -0
  168. package/src/cli/program.ts +1 -1
  169. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  170. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  171. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  172. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  173. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  174. package/src/config/bundled-skills/document/SKILL.md +4 -0
  175. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  176. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  177. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  178. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  179. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  180. package/src/config/env-registry.ts +14 -0
  181. package/src/config/env.ts +21 -0
  182. package/src/config/feature-flag-registry.json +44 -5
  183. package/src/config/loader.ts +56 -1
  184. package/src/config/sanitize-for-transfer.ts +47 -0
  185. package/src/config/schema.ts +46 -5
  186. package/src/config/schemas/host-browser.ts +66 -0
  187. package/src/config/schemas/memory-lifecycle.ts +1 -1
  188. package/src/config/schemas/memory-retrieval.ts +103 -0
  189. package/src/config/schemas/security.ts +0 -6
  190. package/src/config/schemas/services.ts +8 -0
  191. package/src/config/types.ts +0 -1
  192. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  193. package/src/context/window-manager.ts +19 -1
  194. package/src/credential-execution/approval-bridge.ts +49 -15
  195. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  196. package/src/daemon/app-source-watcher.ts +35 -0
  197. package/src/daemon/context-overflow-approval.ts +5 -0
  198. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  199. package/src/daemon/conversation-agent-loop.ts +58 -24
  200. package/src/daemon/conversation-attachments.ts +40 -0
  201. package/src/daemon/conversation-process.ts +48 -1
  202. package/src/daemon/conversation-runtime-assembly.ts +118 -36
  203. package/src/daemon/conversation-surfaces.ts +37 -36
  204. package/src/daemon/conversation-tool-setup.ts +74 -8
  205. package/src/daemon/conversation-workspace.ts +12 -0
  206. package/src/daemon/conversation.ts +226 -8
  207. package/src/daemon/date-context.ts +10 -10
  208. package/src/daemon/first-greeting.ts +3 -2
  209. package/src/daemon/handlers/conversations.ts +9 -140
  210. package/src/daemon/handlers/shared.ts +58 -0
  211. package/src/daemon/handlers/skills.ts +232 -37
  212. package/src/daemon/host-bash-proxy.ts +48 -13
  213. package/src/daemon/host-browser-proxy.ts +191 -0
  214. package/src/daemon/host-cu-proxy.ts +36 -11
  215. package/src/daemon/host-file-proxy.ts +57 -9
  216. package/src/daemon/lifecycle.ts +65 -11
  217. package/src/daemon/message-protocol.ts +7 -0
  218. package/src/daemon/message-types/conversations.ts +55 -13
  219. package/src/daemon/message-types/host-browser.ts +100 -0
  220. package/src/daemon/message-types/messages.ts +5 -5
  221. package/src/daemon/message-types/skills.ts +10 -0
  222. package/src/daemon/message-types/subagents.ts +2 -0
  223. package/src/daemon/server.ts +92 -12
  224. package/src/daemon/tool-side-effects.ts +6 -0
  225. package/src/daemon/transport-hints.ts +5 -24
  226. package/src/inbound/platform-callback-registration.ts +18 -17
  227. package/src/mcp/client.ts +59 -24
  228. package/src/memory/app-store.ts +31 -1
  229. package/src/memory/conversation-crud.ts +23 -0
  230. package/src/memory/conversation-starters-cadence.ts +76 -0
  231. package/src/memory/conversation-title-service.ts +5 -2
  232. package/src/memory/db-init.ts +12 -0
  233. package/src/memory/embedding-backend.test.ts +75 -0
  234. package/src/memory/embedding-backend.ts +131 -5
  235. package/src/memory/embedding-gemini.test.ts +54 -0
  236. package/src/memory/embedding-gemini.ts +20 -9
  237. package/src/memory/embedding-local.ts +176 -17
  238. package/src/memory/graph/consolidation.ts +10 -23
  239. package/src/memory/graph/extraction-job.ts +15 -0
  240. package/src/memory/graph/retriever.ts +40 -22
  241. package/src/memory/graph/store.test.ts +7 -3
  242. package/src/memory/graph/store.ts +47 -12
  243. package/src/memory/llm-usage-store.ts +45 -4
  244. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  245. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  246. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  247. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  248. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  249. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  250. package/src/memory/migrations/index.ts +6 -0
  251. package/src/memory/migrations/registry.ts +8 -0
  252. package/src/memory/schema/conversations.ts +1 -0
  253. package/src/memory/schema/oauth.ts +18 -13
  254. package/src/oauth/AGENTS.md +76 -0
  255. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  256. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  257. package/src/oauth/byo-connection.test.ts +8 -8
  258. package/src/oauth/byo-connection.ts +7 -7
  259. package/src/oauth/connect-orchestrator.ts +23 -21
  260. package/src/oauth/connect-types.ts +3 -3
  261. package/src/oauth/connection-resolver.test.ts +17 -4
  262. package/src/oauth/connection-resolver.ts +16 -16
  263. package/src/oauth/connection.ts +1 -1
  264. package/src/oauth/manual-token-connection.ts +13 -13
  265. package/src/oauth/oauth-store.ts +214 -100
  266. package/src/oauth/platform-connection.test.ts +3 -3
  267. package/src/oauth/platform-connection.ts +4 -4
  268. package/src/oauth/provider-serializer.ts +31 -5
  269. package/src/oauth/revoke.ts +76 -0
  270. package/src/oauth/seed-providers.ts +126 -87
  271. package/src/oauth/token-persistence.ts +1 -1
  272. package/src/permissions/permission-mode.ts +4 -11
  273. package/src/permissions/prompter.ts +13 -1
  274. package/src/permissions/v2-consent-policy.ts +87 -0
  275. package/src/prompts/system-prompt.ts +18 -21
  276. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  277. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  278. package/src/providers/anthropic/client.ts +1 -0
  279. package/src/providers/types.ts +1 -1
  280. package/src/runtime/AGENTS.md +23 -0
  281. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  282. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  283. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  284. package/src/runtime/assistant-event-hub.ts +2 -2
  285. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  286. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  287. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  288. package/src/runtime/auth/middleware.ts +98 -0
  289. package/src/runtime/auth/route-policy.ts +6 -7
  290. package/src/runtime/capability-tokens.ts +414 -0
  291. package/src/runtime/channel-approvals.ts +18 -5
  292. package/src/runtime/chrome-extension-registry.ts +332 -0
  293. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  294. package/src/runtime/guardian-decision-types.ts +7 -0
  295. package/src/runtime/http-server.ts +425 -70
  296. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  297. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  298. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  299. package/src/runtime/migrations/migration-transport.ts +6 -0
  300. package/src/runtime/migrations/migration-wizard.ts +22 -2
  301. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  302. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  303. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  304. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  305. package/src/runtime/pending-interactions.ts +29 -13
  306. package/src/runtime/routes/approval-routes.ts +90 -16
  307. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  308. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  309. package/src/runtime/routes/conversation-analysis-routes.ts +2 -1
  310. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  311. package/src/runtime/routes/conversation-routes.ts +301 -27
  312. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  313. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  314. package/src/runtime/routes/host-browser-routes.ts +279 -0
  315. package/src/runtime/routes/host-file-routes.ts +9 -1
  316. package/src/runtime/routes/identity-routes.ts +259 -16
  317. package/src/runtime/routes/log-export-routes.ts +42 -22
  318. package/src/runtime/routes/memory-item-routes.ts +1 -7
  319. package/src/runtime/routes/migration-routes.ts +87 -2
  320. package/src/runtime/routes/oauth-apps.ts +15 -17
  321. package/src/runtime/routes/oauth-providers.ts +4 -0
  322. package/src/runtime/routes/schedule-routes.ts +24 -11
  323. package/src/runtime/routes/settings-routes.ts +9 -97
  324. package/src/runtime/routes/skills-routes.ts +52 -2
  325. package/src/runtime/routes/subagents-routes.ts +14 -10
  326. package/src/runtime/routes/usage-routes.ts +8 -7
  327. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  328. package/src/runtime/routes/workspace-routes.ts +8 -1
  329. package/src/runtime/routes/workspace-utils.ts +2 -0
  330. package/src/schedule/scheduler.ts +7 -5
  331. package/src/security/ces-credential-client.ts +20 -0
  332. package/src/security/ces-rpc-credential-backend.ts +17 -0
  333. package/src/security/credential-backend.ts +5 -0
  334. package/src/security/oauth2.ts +42 -25
  335. package/src/security/secure-keys.ts +118 -25
  336. package/src/security/token-manager.ts +23 -10
  337. package/src/skills/catalog-files.ts +492 -0
  338. package/src/subagent/manager.ts +131 -26
  339. package/src/subagent/types.ts +19 -0
  340. package/src/tools/apps/executors.ts +11 -2
  341. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  342. package/src/tools/browser/auth-detector.ts +43 -12
  343. package/src/tools/browser/browser-execution.ts +645 -340
  344. package/src/tools/browser/browser-manager.ts +36 -12
  345. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  346. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  347. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  348. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  349. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  350. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  351. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  352. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  353. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  354. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  355. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  356. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  357. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  358. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  359. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  360. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  361. package/src/tools/browser/cdp-client/errors.ts +34 -0
  362. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  363. package/src/tools/browser/cdp-client/factory.ts +204 -0
  364. package/src/tools/browser/cdp-client/index.ts +14 -0
  365. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  366. package/src/tools/browser/cdp-client/types.ts +52 -0
  367. package/src/tools/filesystem/edit.ts +1 -1
  368. package/src/tools/filesystem/list.ts +1 -1
  369. package/src/tools/filesystem/read.ts +1 -1
  370. package/src/tools/filesystem/write.ts +2 -1
  371. package/src/tools/host-filesystem/edit.ts +1 -1
  372. package/src/tools/host-filesystem/read.ts +12 -15
  373. package/src/tools/host-filesystem/write.ts +1 -1
  374. package/src/tools/host-terminal/host-shell.ts +21 -16
  375. package/src/tools/permission-checker.ts +77 -82
  376. package/src/tools/registry.ts +0 -2
  377. package/src/tools/secret-detection-handler.ts +34 -0
  378. package/src/tools/shared/filesystem/image-read.ts +61 -40
  379. package/src/tools/subagent/spawn.ts +47 -3
  380. package/src/tools/subagent/status.ts +2 -0
  381. package/src/tools/system/register.ts +2 -16
  382. package/src/tools/terminal/safe-env.ts +7 -0
  383. package/src/tools/terminal/shell.ts +21 -16
  384. package/src/tools/tool-approval-handler.ts +48 -2
  385. package/src/tools/types.ts +2 -0
  386. package/src/util/platform.ts +14 -19
  387. package/src/workspace/top-level-renderer.ts +19 -1
  388. package/src/__tests__/chrome-cdp.test.ts +0 -419
  389. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  390. package/src/__tests__/permission-mode-store.test.ts +0 -277
  391. package/src/browser-extension-relay/protocol.ts +0 -63
  392. package/src/browser-extension-relay/server.ts +0 -203
  393. package/src/config/schemas/sandbox.ts +0 -14
  394. package/src/permissions/permission-mode-store.ts +0 -180
  395. package/src/tools/browser/chrome-cdp.ts +0 -239
  396. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -5,13 +5,18 @@
5
5
  * configured port (default: 7821).
6
6
  */
7
7
 
8
- import { existsSync, readFileSync } from "node:fs";
9
- import { resolve } from "node:path";
8
+ import {
9
+ existsSync,
10
+ mkdirSync,
11
+ readFileSync,
12
+ renameSync,
13
+ unlinkSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import { dirname, resolve } from "node:path";
10
17
 
11
18
  import type { ServerWebSocket } from "bun";
12
19
 
13
- import type { BrowserRelayWebSocketData } from "../browser-extension-relay/server.js";
14
- import { extensionRelayServer } from "../browser-extension-relay/server.js";
15
20
  import {
16
21
  startGuardianActionSweep,
17
22
  stopGuardianActionSweep,
@@ -63,17 +68,24 @@ import {
63
68
  } from "../security/oauth-callback-registry.js";
64
69
  import { UserError } from "../util/errors.js";
65
70
  import { getLogger } from "../util/logger.js";
71
+ import { getRuntimePortFilePath } from "../util/platform.js";
66
72
  import { buildAssistantEvent } from "./assistant-event.js";
67
73
  import { assistantEventHub } from "./assistant-event-hub.js";
68
74
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
69
75
  // Auth
70
- import { authenticateRequest } from "./auth/middleware.js";
76
+ import {
77
+ authenticateHostBrowserResultRequest,
78
+ authenticateRequest,
79
+ } from "./auth/middleware.js";
80
+ import { parseSub } from "./auth/subject.js";
71
81
  import {
72
82
  mintDaemonDeliveryToken,
73
83
  mintUiPageToken,
74
84
  verifyToken,
75
85
  } from "./auth/token-service.js";
86
+ import { verifyHostBrowserCapability } from "./capability-tokens.js";
76
87
  import { sweepFailedEvents } from "./channel-retry-sweep.js";
88
+ import { getChromeExtensionRegistry } from "./chrome-extension-registry.js";
77
89
  import { httpError } from "./http-errors.js";
78
90
  import type { RouteDefinition } from "./http-router.js";
79
91
  import { HttpRouter } from "./http-router.js";
@@ -110,6 +122,8 @@ import { attachmentRouteDefinitions } from "./routes/attachment-routes.js";
110
122
  import { handleGetAudio } from "./routes/audio-routes.js";
111
123
  import { avatarRouteDefinitions } from "./routes/avatar-routes.js";
112
124
  import { brainGraphRouteDefinitions } from "./routes/brain-graph-routes.js";
125
+ import { browserCdpRouteDefinitions } from "./routes/browser-cdp-routes.js";
126
+ import { handleBrowserExtensionPair } from "./routes/browser-extension-pair-routes.js";
113
127
  import { btwRouteDefinitions } from "./routes/btw-routes.js";
114
128
  import { callRouteDefinitions } from "./routes/call-routes.js";
115
129
  import {
@@ -147,6 +161,12 @@ import { handleGuardianBootstrap } from "./routes/guardian-bootstrap-routes.js";
147
161
  import { handleGuardianRefresh } from "./routes/guardian-refresh-routes.js";
148
162
  import { heartbeatRouteDefinitions } from "./routes/heartbeat-routes.js";
149
163
  import { hostBashRouteDefinitions } from "./routes/host-bash-routes.js";
164
+ import {
165
+ hostBrowserRouteDefinitions,
166
+ resolveHostBrowserEvent,
167
+ resolveHostBrowserResultByRequestId,
168
+ resolveHostBrowserSessionInvalidated,
169
+ } from "./routes/host-browser-routes.js";
150
170
  import { hostCuRouteDefinitions } from "./routes/host-cu-routes.js";
151
171
  import { hostFileRouteDefinitions } from "./routes/host-file-routes.js";
152
172
  import {
@@ -228,6 +248,40 @@ const DEFAULT_HOSTNAME = "127.0.0.1";
228
248
  /** Global hard cap on request body size (512 MB — accommodates large .vbundle backup imports). */
229
249
  const MAX_REQUEST_BODY_BYTES = 512 * 1024 * 1024;
230
250
 
251
+ /**
252
+ * WebSocket data attached to `/v1/browser-relay` connections. The route
253
+ * is used exclusively by the chrome-extension CDP proxy — outbound
254
+ * `host_browser_request` frames are pushed through the
255
+ * {@link ChromeExtensionRegistry}, and inbound `host_browser_result`
256
+ * frames are dispatched through
257
+ * `resolveHostBrowserResultByRequestId`. The extension may also submit
258
+ * results via `POST /v1/host-browser-result` (both transports resolve
259
+ * through the same core function).
260
+ */
261
+ interface BrowserRelayWebSocketData {
262
+ wsType: "browser-relay";
263
+ connectionId: string;
264
+ /**
265
+ * Guardian identity derived from the JWT claims at WebSocket upgrade
266
+ * time. Used by the ChromeExtensionRegistry to route
267
+ * host_browser_request frames to the correct extension. Undefined when
268
+ * HTTP auth is disabled (dev bypass) or when the token's sub cannot be
269
+ * parsed into an actor principal.
270
+ */
271
+ guardianId?: string;
272
+ /**
273
+ * Stable per-extension-install identifier supplied by the client on
274
+ * the WebSocket handshake (via the `clientInstanceId` query param or
275
+ * the `x-client-instance-id` header). Plumbed into the
276
+ * ChromeExtensionRegistry so multiple parallel installs for the same
277
+ * guardian (e.g. two Chrome profiles, two desktops) don't evict each
278
+ * other on register/unregister. Undefined on older extension builds
279
+ * — the registry synthesizes a connection-scoped fallback key in
280
+ * that case for backwards-compatible single-instance semantics.
281
+ */
282
+ clientInstanceId?: string;
283
+ }
284
+
231
285
  export class RuntimeHttpServer {
232
286
  private server: ReturnType<typeof Bun.serve> | null = null;
233
287
  private port: number;
@@ -329,9 +383,20 @@ export class RuntimeHttpServer {
329
383
  open(ws) {
330
384
  const data = ws.data as AllWebSocketData;
331
385
  if ("wsType" in data && data.wsType === "browser-relay") {
332
- extensionRelayServer.handleOpen(
333
- ws as ServerWebSocket<BrowserRelayWebSocketData>,
334
- );
386
+ // When the JWT sub resolved to a guardian principal at upgrade
387
+ // time, register this connection with the chrome-extension
388
+ // registry so host_browser_request frames can be routed to it.
389
+ if (data.guardianId) {
390
+ const now = Date.now();
391
+ getChromeExtensionRegistry().register({
392
+ id: data.connectionId,
393
+ guardianId: data.guardianId,
394
+ clientInstanceId: data.clientInstanceId,
395
+ ws,
396
+ connectedAt: now,
397
+ lastActiveAt: now,
398
+ });
399
+ }
335
400
  return;
336
401
  }
337
402
  const callSessionId = (data as RelayWebSocketData).callSessionId;
@@ -351,11 +416,119 @@ export class RuntimeHttpServer {
351
416
  ? message
352
417
  : new TextDecoder().decode(message);
353
418
  if ("wsType" in data && data.wsType === "browser-relay") {
354
- extensionRelayServer.handleMessage(
355
- ws as ServerWebSocket<BrowserRelayWebSocketData>,
356
- raw,
357
- );
358
- return;
419
+ // Inbound frames on `/v1/browser-relay` carry one of:
420
+ // - `host_browser_result` — paired response to an outbound
421
+ // `host_browser_request` (see PR2).
422
+ // - `host_browser_event` — unsolicited CDP event forwarded
423
+ // from the extension's `chrome.debugger.onEvent`
424
+ // subscription (PR10).
425
+ // - `host_browser_session_invalidated` — detach
426
+ // notification forwarded from the extension's
427
+ // `chrome.debugger.onDetach` subscription (PR10).
428
+ //
429
+ // Every supported frame type delegates into a shared
430
+ // resolver exported from `host-browser-routes.ts` so the
431
+ // validation and resolution semantics stay in lockstep
432
+ // with the HTTP path. Malformed or unsupported frames are
433
+ // logged at debug and swallowed — we never throw out of a
434
+ // WebSocket `message` handler because an uncaught
435
+ // exception would tear down the whole socket for an
436
+ // attacker-controlled payload.
437
+ let parsed: unknown;
438
+ try {
439
+ parsed = JSON.parse(raw);
440
+ } catch (err) {
441
+ log.debug(
442
+ {
443
+ connectionId: data.connectionId,
444
+ error: err instanceof Error ? err.message : String(err),
445
+ },
446
+ "browser-relay: dropped non-JSON inbound frame",
447
+ );
448
+ return;
449
+ }
450
+ if (!parsed || typeof parsed !== "object") {
451
+ log.debug(
452
+ { connectionId: data.connectionId },
453
+ "browser-relay: dropped non-object inbound frame",
454
+ );
455
+ return;
456
+ }
457
+ const frame = parsed as Record<string, unknown>;
458
+ switch (frame.type) {
459
+ case "host_browser_result": {
460
+ const resolution = resolveHostBrowserResultByRequestId({
461
+ requestId: frame.requestId,
462
+ content: frame.content,
463
+ isError: frame.isError,
464
+ });
465
+ if (!resolution.ok) {
466
+ log.warn(
467
+ {
468
+ connectionId: data.connectionId,
469
+ requestId:
470
+ typeof frame.requestId === "string"
471
+ ? frame.requestId
472
+ : undefined,
473
+ code: resolution.code,
474
+ message: resolution.message,
475
+ },
476
+ "browser-relay: host_browser_result frame rejected",
477
+ );
478
+ }
479
+ return;
480
+ }
481
+ case "host_browser_event": {
482
+ const resolution = resolveHostBrowserEvent({
483
+ method: frame.method,
484
+ params: frame.params,
485
+ cdpSessionId: frame.cdpSessionId,
486
+ });
487
+ if (!resolution.ok) {
488
+ log.warn(
489
+ {
490
+ connectionId: data.connectionId,
491
+ method:
492
+ typeof frame.method === "string"
493
+ ? frame.method
494
+ : undefined,
495
+ code: resolution.code,
496
+ message: resolution.message,
497
+ },
498
+ "browser-relay: host_browser_event frame rejected",
499
+ );
500
+ }
501
+ return;
502
+ }
503
+ case "host_browser_session_invalidated": {
504
+ const resolution = resolveHostBrowserSessionInvalidated({
505
+ targetId: frame.targetId,
506
+ reason: frame.reason,
507
+ });
508
+ if (!resolution.ok) {
509
+ log.warn(
510
+ {
511
+ connectionId: data.connectionId,
512
+ targetId:
513
+ typeof frame.targetId === "string"
514
+ ? frame.targetId
515
+ : undefined,
516
+ code: resolution.code,
517
+ message: resolution.message,
518
+ },
519
+ "browser-relay: host_browser_session_invalidated frame rejected",
520
+ );
521
+ }
522
+ return;
523
+ }
524
+ default: {
525
+ log.debug(
526
+ { connectionId: data.connectionId, type: frame.type },
527
+ "browser-relay: dropped unsupported inbound frame type",
528
+ );
529
+ return;
530
+ }
531
+ }
359
532
  }
360
533
  const callSessionId = (data as RelayWebSocketData).callSessionId;
361
534
  if (callSessionId) {
@@ -366,11 +539,12 @@ export class RuntimeHttpServer {
366
539
  close(ws, code, reason) {
367
540
  const data = ws.data as AllWebSocketData;
368
541
  if ("wsType" in data && data.wsType === "browser-relay") {
369
- extensionRelayServer.handleClose(
370
- ws as ServerWebSocket<BrowserRelayWebSocketData>,
371
- code,
372
- reason?.toString(),
373
- );
542
+ // Always attempt to unregister — the registry uses connectionId
543
+ // as the key and no-ops if the entry is absent (e.g. when the
544
+ // connection was never registered because guardianId was
545
+ // undefined, or when it was superseded by a newer registration
546
+ // for the same guardian).
547
+ getChromeExtensionRegistry().unregister(data.connectionId);
374
548
  return;
375
549
  }
376
550
  const callSessionId = (data as RelayWebSocketData).callSessionId;
@@ -388,7 +562,51 @@ export class RuntimeHttpServer {
388
562
  },
389
563
  });
390
564
 
391
- if (this.processMessage) {
565
+ this.startBackgroundSweeps();
566
+
567
+ log.info(
568
+ "Running in gateway-only ingress mode. Direct webhook routes disabled.",
569
+ );
570
+ if (!isLoopbackHost(this.hostname)) {
571
+ log.warn(
572
+ "RUNTIME_HTTP_HOST is not bound to loopback. This may expose the runtime to direct public access.",
573
+ );
574
+ }
575
+
576
+ this.pairingStore.start();
577
+
578
+ if (hasUngatedHttpAuthDisabled()) {
579
+ log.warn(
580
+ "DISABLE_HTTP_AUTH is set but VELLUM_UNSAFE_AUTH_BYPASS=1 is not — auth bypass is IGNORED and HTTP authentication remains enabled. Set VELLUM_UNSAFE_AUTH_BYPASS=1 to confirm the bypass.",
581
+ );
582
+ } else if (isHttpAuthDisabled()) {
583
+ log.warn(
584
+ "DISABLE_HTTP_AUTH is set — HTTP API authentication is DISABLED. All API endpoints are accessible without a bearer token. Do not use in production.",
585
+ );
586
+ }
587
+
588
+ log.info(
589
+ {
590
+ port: this.actualPort,
591
+ hostname: this.hostname,
592
+ auth: !!this.bearerToken,
593
+ },
594
+ "Runtime HTTP server listening",
595
+ );
596
+
597
+ // Advertise the actual port to thin helpers that need to reach the
598
+ // runtime without inheriting the daemon's environment (e.g. the
599
+ // chrome-extension native messaging helper, spawned by Chrome).
600
+ this.writeRuntimePortFile(this.actualPort);
601
+ }
602
+
603
+ /**
604
+ * Start background sweep timers: retry sweep for failed channel events,
605
+ * guardian approval/action expiry sweeps, and canonical guardian expiry.
606
+ * Extracted from start() to allow future callers to defer sweep startup.
607
+ */
608
+ private startBackgroundSweeps(): void {
609
+ if (this.processMessage && !this.retrySweepTimer) {
392
610
  const pm = this.processMessage;
393
611
  const mintBt = () => mintDaemonDeliveryToken();
394
612
  this.retrySweepTimer = setInterval(() => {
@@ -416,36 +634,76 @@ export class RuntimeHttpServer {
416
634
 
417
635
  startCanonicalGuardianExpirySweep();
418
636
  log.info("Canonical guardian request expiry sweep started");
637
+ }
419
638
 
420
- log.info(
421
- "Running in gateway-only ingress mode. Direct webhook routes disabled.",
422
- );
423
- if (!isLoopbackHost(this.hostname)) {
639
+ /**
640
+ * Atomically publish the runtime HTTP port to ~/.vellum/runtime-port so
641
+ * external helpers can locate a non-default `RUNTIME_HTTP_PORT` without
642
+ * any manifest changes. Best-effort — write failures never block
643
+ * daemon startup (see assistant/AGENTS.md "Daemon startup philosophy").
644
+ */
645
+ private writeRuntimePortFile(actualPort: number): void {
646
+ try {
647
+ const portFile = getRuntimePortFilePath();
648
+ const dir = dirname(portFile);
649
+ if (!existsSync(dir)) {
650
+ mkdirSync(dir, { recursive: true });
651
+ }
652
+ const tmpPath = `${portFile}.tmp.${process.pid}`;
653
+ writeFileSync(tmpPath, String(actualPort), { mode: 0o644 });
654
+ renameSync(tmpPath, portFile);
655
+ log.info({ portFile, actualPort }, "Wrote runtime port file");
656
+ } catch (err) {
424
657
  log.warn(
425
- "RUNTIME_HTTP_HOST is not bound to loopback. This may expose the runtime to direct public access.",
658
+ { err },
659
+ "Failed to write runtime port file; non-default assistant ports may require --assistant-port on thin helpers",
426
660
  );
427
661
  }
662
+ }
428
663
 
429
- this.pairingStore.start();
430
-
431
- if (hasUngatedHttpAuthDisabled()) {
432
- log.warn(
433
- "DISABLE_HTTP_AUTH is set but VELLUM_UNSAFE_AUTH_BYPASS=1 is not auth bypass is IGNORED and HTTP authentication remains enabled. Set VELLUM_UNSAFE_AUTH_BYPASS=1 to confirm the bypass.",
434
- );
435
- } else if (isHttpAuthDisabled()) {
436
- log.warn(
437
- "DISABLE_HTTP_AUTH is set HTTP API authentication is DISABLED. All API endpoints are accessible without a bearer token. Do not use in production.",
438
- );
664
+ /**
665
+ * Remove the runtime port file written by `writeRuntimePortFile`.
666
+ * Called from `stop()` on clean shutdown so a stale file does not
667
+ * point thin helpers (e.g. the chrome-extension native messaging
668
+ * helper) at a dead port until the next daemon start overwrites it.
669
+ * Best-effort — unlink failures never block shutdown.
670
+ *
671
+ * The unlink is conditional: we only remove the file if its current
672
+ * contents still match this server's port. The runtime-port file
673
+ * lives at the user-home level (`~/.vellum/runtime-port`) and is
674
+ * therefore shared across multiple daemon instances running on
675
+ * different `RUNTIME_HTTP_PORT`s. If a sibling instance has already
676
+ * rewritten the file with its own port, deleting it would strand
677
+ * thin helpers on the default port `7821` and break their ability
678
+ * to reach the still-running sibling.
679
+ *
680
+ * Note: this only runs on graceful shutdown. A crash leaves the
681
+ * file in place; the next successful startup overwrites it.
682
+ */
683
+ private removeRuntimePortFile(): void {
684
+ try {
685
+ const portFile = getRuntimePortFilePath();
686
+ if (!existsSync(portFile)) return;
687
+ // Read-then-compare-then-unlink. Race-safe enough: the worst case
688
+ // is that another instance writes the file between our read and
689
+ // our unlink, in which case we erroneously delete its mapping.
690
+ // That window is short (a few microseconds) and a sibling startup
691
+ // will rewrite the file on its next port-publish call. The much
692
+ // more common multi-instance race — sibling already overwrote
693
+ // before our stop() runs — is correctly handled here as a no-op.
694
+ const current = readFileSync(portFile, "utf-8").trim();
695
+ if (current !== String(this.actualPort)) {
696
+ log.info(
697
+ { portFile, current, actualPort: this.actualPort },
698
+ "Leaving runtime port file alone — owned by another instance",
699
+ );
700
+ return;
701
+ }
702
+ unlinkSync(portFile);
703
+ log.info({ portFile }, "Removed runtime port file");
704
+ } catch (err) {
705
+ log.warn({ err }, "Failed to remove runtime port file");
439
706
  }
440
-
441
- log.info(
442
- {
443
- port: this.actualPort,
444
- hostname: this.hostname,
445
- auth: !!this.bearerToken,
446
- },
447
- "Runtime HTTP server listening",
448
- );
449
707
  }
450
708
 
451
709
  async stop(): Promise<void> {
@@ -462,6 +720,7 @@ export class RuntimeHttpServer {
462
720
  this.server = null;
463
721
  log.info("Runtime HTTP server stopped");
464
722
  }
723
+ this.removeRuntimePortFile();
465
724
  }
466
725
 
467
726
  private async handleRequest(
@@ -532,6 +791,13 @@ export class RuntimeHttpServer {
532
791
  return handlePairingStatus(url, this.pairingContext);
533
792
  }
534
793
 
794
+ // Chrome extension capability-token pair endpoint — unauthenticated but
795
+ // restricted to loopback peers + an extension-id allowlist. Used by the
796
+ // native messaging helper to bootstrap a scoped token.
797
+ if (path === "/v1/browser-extension-pair") {
798
+ return await handleBrowserExtensionPair(req, server);
799
+ }
800
+
535
801
  // Guardian bootstrap and refresh endpoints — before JWT auth because
536
802
  // bootstrap is the first endpoint called to obtain a JWT, and refresh
537
803
  // needs to work when the access token is expired. Bootstrap has its
@@ -546,7 +812,19 @@ export class RuntimeHttpServer {
546
812
 
547
813
  // JWT bearer authentication — replaces the old shared-secret comparison.
548
814
  // authenticateRequest handles dev bypass (DISABLE_HTTP_AUTH) internally.
549
- const authResult = authenticateRequest(req);
815
+ //
816
+ // Special-case: /v1/host-browser-result POST accepts either a
817
+ // daemon-minted JWT (legacy/cloud) or a host_browser capability
818
+ // token (self-hosted chrome extension). The chrome extension's
819
+ // HTTP fallback (`postHostBrowserResult`) hands over the same
820
+ // capability token it presented to `/v1/browser-relay`, so the
821
+ // POST route must understand both auth shapes. Every other route
822
+ // keeps the JWT-only flow via `authenticateRequest`.
823
+ const normalizedPath = path.endsWith("/") ? path.slice(0, -1) : path;
824
+ const authResult =
825
+ normalizedPath === "/v1/host-browser-result" && req.method === "POST"
826
+ ? authenticateHostBrowserResultRequest(req)
827
+ : authenticateRequest(req);
550
828
  if (!authResult.ok) {
551
829
  return authResult.response;
552
830
  }
@@ -634,15 +912,110 @@ export class RuntimeHttpServer {
634
912
  );
635
913
  }
636
914
 
915
+ // When auth is enabled we accept two different kinds of token on the
916
+ // `/v1/browser-relay` handshake:
917
+ //
918
+ // 1. **Capability token** — a signed `host_browser_command`
919
+ // capability minted by `mintHostBrowserCapability()` and handed
920
+ // to the chrome extension by the native-messaging pair flow
921
+ // (`/v1/browser-extension-pair`). This is the preferred,
922
+ // self-hosted default: the extension never has to touch a
923
+ // gateway JWT.
924
+ // 2. **JWT** (audience `vellum-daemon`) — the legacy path used by
925
+ // the gateway-proxied cloud flow and by any compatibility
926
+ // callers that still hold a daemon-bound JWT. In that case we
927
+ // parse the JWT `sub` to extract the actor principal id and
928
+ // fall back to the explicit `x-guardian-id` / `guardianId`
929
+ // query param for service-token paths (see below).
930
+ //
931
+ // When auth is disabled (dev bypass), guardianId remains undefined
932
+ // and the registration is skipped — host_browser_request routing
933
+ // requires an authenticated guardian.
934
+ //
935
+ // Gateway path: when the WebSocket upgrade is proxied through the
936
+ // gateway, the upstream token minted by `mintServiceToken()` has
937
+ // `sub=svc:gateway:self` with no actor principal id. The gateway
938
+ // parses the downstream edge token's `actorPrincipalId` and forwards
939
+ // it as an explicit `guardianId` query parameter (and/or header) so
940
+ // we can register the connection under the real guardian. Missing
941
+ // guardian context on this path is rejected (fail closed).
942
+ // Read the client-supplied stable instance id off the handshake.
943
+ // The extension generates this on first run and persists it in
944
+ // chrome.storage so it survives service-worker restarts and
945
+ // browser restarts. The header form is preferred so gateway
946
+ // forwarding and proxy logs don't surface instance ids in the
947
+ // URL, but we also accept a query param for fetch-based clients
948
+ // that can't mutate headers. An empty string is treated as absent
949
+ // so sparse clients don't end up all sharing the same legacy key.
950
+ const rawInstanceHeader = req.headers.get("x-client-instance-id")?.trim();
951
+ const rawInstanceQuery = new URL(req.url).searchParams
952
+ .get("clientInstanceId")
953
+ ?.trim();
954
+ const clientInstanceId =
955
+ (rawInstanceHeader ?? "") || (rawInstanceQuery ?? "") || undefined;
956
+
957
+ let guardianId: string | undefined;
637
958
  if (!isHttpAuthDisabled()) {
638
959
  const wsUrl = new URL(req.url);
639
960
  const token = wsUrl.searchParams.get("token");
640
961
  if (!token) {
641
962
  return httpError("UNAUTHORIZED", "Unauthorized", 401);
642
963
  }
643
- const jwtResult = verifyToken(token, "vellum-daemon");
644
- if (!jwtResult.ok) {
645
- return httpError("UNAUTHORIZED", "Unauthorized", 401);
964
+ // 1) Capability-token path (self-hosted default). The chrome
965
+ // extension presents the token it received from the native
966
+ // messaging pair flow. We derive `guardianId` from the
967
+ // capability claims directly — the claims are HMAC-signed by
968
+ // the same daemon so there is no cross-tenant risk.
969
+ const capabilityClaims = verifyHostBrowserCapability(token);
970
+ if (capabilityClaims) {
971
+ guardianId = capabilityClaims.guardianId;
972
+ } else {
973
+ // 2) JWT compatibility path (gateway / legacy). Fall back to the
974
+ // existing verifyToken+parseSub flow so cloud callers and any
975
+ // old self-hosted clients still holding a daemon JWT
976
+ // continue to work during the cutover.
977
+ const jwtResult = verifyToken(token, "vellum-daemon");
978
+ if (!jwtResult.ok) {
979
+ return httpError("UNAUTHORIZED", "Unauthorized", 401);
980
+ }
981
+ const subResult = parseSub(jwtResult.claims.sub);
982
+ if (subResult.ok && subResult.actorPrincipalId) {
983
+ // Direct actor principal — this is the loopback / desktop path.
984
+ guardianId = subResult.actorPrincipalId;
985
+ } else {
986
+ // Service-token path (gateway-forwarded). The gateway must plumb
987
+ // the resolved actor principal as an explicit `x-guardian-id`
988
+ // header or `guardianId` query param. Header takes precedence
989
+ // because headers are easier for the gateway to forward without
990
+ // rewriting the URL.
991
+ const headerGuardianId =
992
+ req.headers.get("x-guardian-id")?.trim() ?? "";
993
+ const queryGuardianId =
994
+ wsUrl.searchParams.get("guardianId")?.trim() ?? "";
995
+ const fallbackGuardianId = headerGuardianId || queryGuardianId;
996
+ if (fallbackGuardianId) {
997
+ guardianId = fallbackGuardianId;
998
+ } else {
999
+ // Fail closed: a service-token relay upgrade without a
1000
+ // guardian context cannot be routed safely. Allowing the
1001
+ // upgrade to proceed creates an unscoped socket that never
1002
+ // registers in the ChromeExtensionRegistry.
1003
+ log.warn(
1004
+ {
1005
+ principalType: subResult.ok
1006
+ ? subResult.principalType
1007
+ : "unknown",
1008
+ sub: jwtResult.claims.sub,
1009
+ },
1010
+ "Browser relay upgrade denied: missing guardian context on service-token path",
1011
+ );
1012
+ return httpError(
1013
+ "UNAUTHORIZED",
1014
+ "Browser relay requires guardian context",
1015
+ 401,
1016
+ );
1017
+ }
1018
+ }
646
1019
  }
647
1020
  }
648
1021
 
@@ -651,6 +1024,8 @@ export class RuntimeHttpServer {
651
1024
  data: {
652
1025
  wsType: "browser-relay",
653
1026
  connectionId,
1027
+ guardianId,
1028
+ clientInstanceId,
654
1029
  } satisfies BrowserRelayWebSocketData,
655
1030
  });
656
1031
  if (!upgraded) {
@@ -833,6 +1208,7 @@ export class RuntimeHttpServer {
833
1208
  lastMessageAt: conversation.lastMessageAt,
834
1209
  conversationType: conversation.conversationType ?? "standard",
835
1210
  source: conversation.source ?? "user",
1211
+ hostAccess: conversation.hostAccess === 1,
836
1212
  ...(conversation.scheduleJobId
837
1213
  ? { scheduleJobId: conversation.scheduleJobId }
838
1214
  : {}),
@@ -997,29 +1373,6 @@ export class RuntimeHttpServer {
997
1373
  }),
998
1374
  ...ttsRouteDefinitions(),
999
1375
 
1000
- // Browser relay — not extracted into a domain module because
1001
- // these two routes depend on the in-process extensionRelayServer
1002
- // singleton which is only available here.
1003
- {
1004
- endpoint: "browser-relay/status",
1005
- method: "GET",
1006
- handler: () => Response.json(extensionRelayServer.getStatus()),
1007
- },
1008
- {
1009
- endpoint: "browser-relay/command",
1010
- method: "POST",
1011
- handler: async ({ req }) => {
1012
- const body = (await req.json()) as Record<string, unknown>;
1013
- const resp = await extensionRelayServer.sendCommand(
1014
- body as Omit<
1015
- import("../browser-extension-relay/protocol.js").ExtensionCommand,
1016
- "id"
1017
- >,
1018
- );
1019
- return Response.json(resp);
1020
- },
1021
- },
1022
-
1023
1376
  // Conversation list and seen signal — kept inline because they
1024
1377
  // depend on multiple cross-cutting stores that aren't grouped
1025
1378
  // into a single domain module.
@@ -1209,6 +1562,8 @@ export class RuntimeHttpServer {
1209
1562
  ...globalSearchRouteDefinitions(),
1210
1563
  ...approvalRouteDefinitions(),
1211
1564
  ...hostBashRouteDefinitions(),
1565
+ ...hostBrowserRouteDefinitions(),
1566
+ ...browserCdpRouteDefinitions(),
1212
1567
  ...hostCuRouteDefinitions(),
1213
1568
  ...hostFileRouteDefinitions(),
1214
1569
  ...(this.getSkillContext