@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
@@ -0,0 +1,518 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import type { ServerMessage } from "../../daemon/message-protocol.js";
4
+ import {
5
+ __resetChromeExtensionRegistryForTests,
6
+ type ChromeExtensionConnection,
7
+ ChromeExtensionRegistry,
8
+ getChromeExtensionRegistry,
9
+ } from "../chrome-extension-registry.js";
10
+
11
+ // Minimal structural stand-in for Bun's ServerWebSocket. Only the methods
12
+ // the registry touches (`send`, `close`) are modeled; the rest of the Bun
13
+ // ServerWebSocket API is out of scope for these unit tests.
14
+ interface FakeWs {
15
+ send: (data: string) => number;
16
+ close: (code?: number, reason?: string) => void;
17
+ sent: string[];
18
+ closed: { code?: number; reason?: string }[];
19
+ sendShouldThrow?: boolean;
20
+ }
21
+
22
+ function makeFakeWs(): FakeWs {
23
+ const sent: string[] = [];
24
+ const closed: { code?: number; reason?: string }[] = [];
25
+ const ws: FakeWs = {
26
+ sent,
27
+ closed,
28
+ send(data: string) {
29
+ if (ws.sendShouldThrow) {
30
+ throw new Error("simulated ws.send failure");
31
+ }
32
+ sent.push(data);
33
+ return data.length;
34
+ },
35
+ close(code?: number, reason?: string) {
36
+ closed.push({ code, reason });
37
+ },
38
+ };
39
+ return ws;
40
+ }
41
+
42
+ function makeConnection(
43
+ guardianId: string,
44
+ id?: string,
45
+ clientInstanceId?: string,
46
+ ): { conn: ChromeExtensionConnection; fakeWs: FakeWs } {
47
+ const fakeWs = makeFakeWs();
48
+ const now = Date.now();
49
+ const conn: ChromeExtensionConnection = {
50
+ id: id ?? crypto.randomUUID(),
51
+ guardianId,
52
+ clientInstanceId,
53
+ ws: fakeWs as unknown as ChromeExtensionConnection["ws"],
54
+ connectedAt: now,
55
+ lastActiveAt: now,
56
+ };
57
+ return { conn, fakeWs };
58
+ }
59
+
60
+ describe("ChromeExtensionRegistry", () => {
61
+ beforeEach(() => {
62
+ __resetChromeExtensionRegistryForTests();
63
+ });
64
+
65
+ test("register stores the connection under the guardianId", () => {
66
+ const registry = new ChromeExtensionRegistry();
67
+ const { conn } = makeConnection("guardian-alpha");
68
+ registry.register(conn);
69
+ expect(registry.get("guardian-alpha")).toBe(conn);
70
+ });
71
+
72
+ test("unregister removes the connection", () => {
73
+ const registry = new ChromeExtensionRegistry();
74
+ const { conn } = makeConnection("guardian-alpha");
75
+ registry.register(conn);
76
+ registry.unregister(conn.id);
77
+ expect(registry.get("guardian-alpha")).toBeUndefined();
78
+ });
79
+
80
+ test("unregister is a no-op when the connectionId is unknown", () => {
81
+ const registry = new ChromeExtensionRegistry();
82
+ // Should not throw even though nothing is registered.
83
+ expect(() => registry.unregister("unknown-connection")).not.toThrow();
84
+ });
85
+
86
+ test("registering a second connection for the same guardianId + instance closes the prior one", () => {
87
+ const registry = new ChromeExtensionRegistry();
88
+ const { conn: conn1, fakeWs: fakeWs1 } = makeConnection(
89
+ "guardian-alpha",
90
+ "conn-1",
91
+ "install-A",
92
+ );
93
+ const { conn: conn2 } = makeConnection(
94
+ "guardian-alpha",
95
+ "conn-2",
96
+ "install-A",
97
+ );
98
+ registry.register(conn1);
99
+ registry.register(conn2);
100
+ // Prior connection (same instance) should have been closed with code 1000.
101
+ expect(fakeWs1.closed).toHaveLength(1);
102
+ expect(fakeWs1.closed[0].code).toBe(1000);
103
+ // Registry should hold the new connection for that instance.
104
+ expect(registry.getInstance("guardian-alpha", "install-A")).toBe(conn2);
105
+ });
106
+
107
+ test("registering the same connection id twice is idempotent and does not close itself", () => {
108
+ const registry = new ChromeExtensionRegistry();
109
+ const { conn, fakeWs } = makeConnection("guardian-alpha", "conn-1");
110
+ registry.register(conn);
111
+ registry.register(conn);
112
+ expect(fakeWs.closed).toHaveLength(0);
113
+ expect(registry.get("guardian-alpha")).toBe(conn);
114
+ });
115
+
116
+ test("send returns false when no connection exists for the guardian", () => {
117
+ const registry = new ChromeExtensionRegistry();
118
+ const msg: ServerMessage = {
119
+ type: "host_browser_cancel",
120
+ requestId: "req-1",
121
+ } as ServerMessage;
122
+ expect(registry.send("missing-guardian", msg)).toBe(false);
123
+ });
124
+
125
+ test("send returns true and forwards the JSON-serialized message when a connection exists", () => {
126
+ const registry = new ChromeExtensionRegistry();
127
+ const { conn, fakeWs } = makeConnection("guardian-alpha");
128
+ registry.register(conn);
129
+ const msg: ServerMessage = {
130
+ type: "host_browser_cancel",
131
+ requestId: "req-1",
132
+ } as ServerMessage;
133
+ const ok = registry.send("guardian-alpha", msg);
134
+ expect(ok).toBe(true);
135
+ expect(fakeWs.sent).toHaveLength(1);
136
+ const parsed = JSON.parse(fakeWs.sent[0]);
137
+ expect(parsed.type).toBe("host_browser_cancel");
138
+ expect(parsed.requestId).toBe("req-1");
139
+ });
140
+
141
+ test("send returns false when ws.send throws (best-effort delivery)", () => {
142
+ const registry = new ChromeExtensionRegistry();
143
+ const { conn, fakeWs } = makeConnection("guardian-alpha");
144
+ fakeWs.sendShouldThrow = true;
145
+ registry.register(conn);
146
+ const msg: ServerMessage = {
147
+ type: "host_browser_cancel",
148
+ requestId: "req-1",
149
+ } as ServerMessage;
150
+ expect(registry.send("guardian-alpha", msg)).toBe(false);
151
+ });
152
+
153
+ test("getChromeExtensionRegistry returns a module-level singleton", () => {
154
+ const first = getChromeExtensionRegistry();
155
+ const second = getChromeExtensionRegistry();
156
+ expect(first).toBe(second);
157
+ });
158
+
159
+ test("unregister after supersession does not remove the new connection", () => {
160
+ // When a new connection supersedes an older one, the close handler for
161
+ // the older socket will fire later and call unregister with the OLD id.
162
+ // That must not clobber the newer registration.
163
+ const registry = new ChromeExtensionRegistry();
164
+ const { conn: old } = makeConnection(
165
+ "guardian-alpha",
166
+ "old-id",
167
+ "install-A",
168
+ );
169
+ const { conn: fresh } = makeConnection(
170
+ "guardian-alpha",
171
+ "fresh-id",
172
+ "install-A",
173
+ );
174
+ registry.register(old);
175
+ registry.register(fresh);
176
+ registry.unregister("old-id");
177
+ expect(registry.getInstance("guardian-alpha", "install-A")).toBe(fresh);
178
+ });
179
+
180
+ // ── Multi-instance routing ──────────────────────────────────────────
181
+ //
182
+ // A single guardian may have multiple parallel extension installs
183
+ // connected at once (two Chrome profiles, two desktops sharing a sync
184
+ // identity). The registry keys inner entries by (guardianId,
185
+ // clientInstanceId) so sibling installs don't evict each other on
186
+ // register/unregister, and the default `send()` path routes to
187
+ // whichever instance has the most recent activity.
188
+ describe("multi-instance routing", () => {
189
+ test("two concurrent instances under the same guardian coexist", () => {
190
+ const registry = new ChromeExtensionRegistry();
191
+ const { conn: connA } = makeConnection(
192
+ "guardian-alpha",
193
+ "conn-A",
194
+ "install-A",
195
+ );
196
+ const { conn: connB } = makeConnection(
197
+ "guardian-alpha",
198
+ "conn-B",
199
+ "install-B",
200
+ );
201
+ registry.register(connA);
202
+ registry.register(connB);
203
+ // Both instances remain registered — neither should have been
204
+ // closed by the other's registration.
205
+ expect(registry.getInstance("guardian-alpha", "install-A")).toBe(connA);
206
+ expect(registry.getInstance("guardian-alpha", "install-B")).toBe(connB);
207
+ expect(registry.listInstances("guardian-alpha")).toHaveLength(2);
208
+ });
209
+
210
+ test("registering a new instance does not close sibling instances", () => {
211
+ const registry = new ChromeExtensionRegistry();
212
+ const { conn: connA, fakeWs: fakeWsA } = makeConnection(
213
+ "guardian-alpha",
214
+ "conn-A",
215
+ "install-A",
216
+ );
217
+ const { conn: connB, fakeWs: fakeWsB } = makeConnection(
218
+ "guardian-alpha",
219
+ "conn-B",
220
+ "install-B",
221
+ );
222
+ registry.register(connA);
223
+ registry.register(connB);
224
+ // Neither socket should have been closed by the sibling's
225
+ // registration.
226
+ expect(fakeWsA.closed).toHaveLength(0);
227
+ expect(fakeWsB.closed).toHaveLength(0);
228
+ });
229
+
230
+ test("unregister of one instance leaves the sibling in place", () => {
231
+ const registry = new ChromeExtensionRegistry();
232
+ const { conn: connA } = makeConnection(
233
+ "guardian-alpha",
234
+ "conn-A",
235
+ "install-A",
236
+ );
237
+ const { conn: connB } = makeConnection(
238
+ "guardian-alpha",
239
+ "conn-B",
240
+ "install-B",
241
+ );
242
+ registry.register(connA);
243
+ registry.register(connB);
244
+ registry.unregister("conn-A");
245
+ expect(
246
+ registry.getInstance("guardian-alpha", "install-A"),
247
+ ).toBeUndefined();
248
+ expect(registry.getInstance("guardian-alpha", "install-B")).toBe(connB);
249
+ // Guardian bucket should still exist because install-B is active.
250
+ expect(registry.listInstances("guardian-alpha")).toHaveLength(1);
251
+ });
252
+
253
+ test("default send routes to the most recently active instance", () => {
254
+ const registry = new ChromeExtensionRegistry();
255
+ const { conn: connA, fakeWs: fakeWsA } = makeConnection(
256
+ "guardian-alpha",
257
+ "conn-A",
258
+ "install-A",
259
+ );
260
+ const { conn: connB, fakeWs: fakeWsB } = makeConnection(
261
+ "guardian-alpha",
262
+ "conn-B",
263
+ "install-B",
264
+ );
265
+ // Use a fake clock so A's register timestamp is strictly less
266
+ // than B's, ensuring B becomes the "most recently active"
267
+ // instance regardless of host-clock resolution.
268
+ const originalNow = Date.now;
269
+ let fakeNow = originalNow();
270
+ Date.now = () => fakeNow;
271
+ try {
272
+ registry.register(connA);
273
+ fakeNow += 10;
274
+ registry.register(connB);
275
+ const msg: ServerMessage = {
276
+ type: "host_browser_cancel",
277
+ requestId: "req-1",
278
+ } as ServerMessage;
279
+ expect(registry.send("guardian-alpha", msg)).toBe(true);
280
+ } finally {
281
+ Date.now = originalNow;
282
+ }
283
+ // Default send should have landed on instance B, not A.
284
+ expect(fakeWsA.sent).toHaveLength(0);
285
+ expect(fakeWsB.sent).toHaveLength(1);
286
+ });
287
+
288
+ test("default send follows activity — later sendToInstance flips the default", () => {
289
+ const registry = new ChromeExtensionRegistry();
290
+ const { conn: connA, fakeWs: fakeWsA } = makeConnection(
291
+ "guardian-alpha",
292
+ "conn-A",
293
+ "install-A",
294
+ );
295
+ const { conn: connB, fakeWs: fakeWsB } = makeConnection(
296
+ "guardian-alpha",
297
+ "conn-B",
298
+ "install-B",
299
+ );
300
+ registry.register(connA);
301
+ registry.register(connB);
302
+ // B was registered last so it starts as the default. Force a
303
+ // send to A via sendToInstance — that bumps A's lastActiveAt and
304
+ // should make it the new default target.
305
+ const msg: ServerMessage = {
306
+ type: "host_browser_cancel",
307
+ requestId: "req-1",
308
+ } as ServerMessage;
309
+ // Nudge the clock forward so A's lastActiveAt strictly exceeds
310
+ // B's register-time stamp even on hosts where Date.now() has
311
+ // millisecond resolution.
312
+ const originalNow = Date.now;
313
+ let fakeNow = originalNow() + 10;
314
+ Date.now = () => fakeNow;
315
+ try {
316
+ expect(
317
+ registry.sendToInstance("guardian-alpha", "install-A", msg),
318
+ ).toBe(true);
319
+ fakeNow += 10;
320
+ // Default send should now route to A.
321
+ expect(registry.send("guardian-alpha", msg)).toBe(true);
322
+ } finally {
323
+ Date.now = originalNow;
324
+ }
325
+ // A received one explicit send and one default send.
326
+ expect(fakeWsA.sent).toHaveLength(2);
327
+ // B only received the initial register (no sends).
328
+ expect(fakeWsB.sent).toHaveLength(0);
329
+ });
330
+
331
+ test("sendToInstance returns false for an unknown instance", () => {
332
+ const registry = new ChromeExtensionRegistry();
333
+ const { conn } = makeConnection("guardian-alpha", "conn-A", "install-A");
334
+ registry.register(conn);
335
+ const msg: ServerMessage = {
336
+ type: "host_browser_cancel",
337
+ requestId: "req-1",
338
+ } as ServerMessage;
339
+ expect(
340
+ registry.sendToInstance("guardian-alpha", "install-missing", msg),
341
+ ).toBe(false);
342
+ expect(
343
+ registry.sendToInstance("guardian-missing", "install-A", msg),
344
+ ).toBe(false);
345
+ });
346
+
347
+ test("get returns undefined after the last instance unregisters", () => {
348
+ const registry = new ChromeExtensionRegistry();
349
+ const { conn } = makeConnection("guardian-alpha", "conn-A", "install-A");
350
+ registry.register(conn);
351
+ registry.unregister("conn-A");
352
+ expect(registry.get("guardian-alpha")).toBeUndefined();
353
+ expect(registry.listInstances("guardian-alpha")).toHaveLength(0);
354
+ });
355
+
356
+ test("ties on lastActiveAt are broken by registrationSeq (newest wins)", () => {
357
+ // Freeze Date.now so both instances stamp the exact same
358
+ // lastActiveAt. Without the registrationSeq tiebreaker, the
359
+ // strict `>` comparison in get() would keep whichever entry
360
+ // Map#values() yields first — i.e. the earlier-inserted one,
361
+ // which is the opposite of the "most recently registered"
362
+ // semantics a caller would expect.
363
+ const registry = new ChromeExtensionRegistry();
364
+ const { conn: connA, fakeWs: fakeWsA } = makeConnection(
365
+ "guardian-alpha",
366
+ "conn-A",
367
+ "install-A",
368
+ );
369
+ const { conn: connB, fakeWs: fakeWsB } = makeConnection(
370
+ "guardian-alpha",
371
+ "conn-B",
372
+ "install-B",
373
+ );
374
+ const originalNow = Date.now;
375
+ const frozenNow = originalNow();
376
+ Date.now = () => frozenNow;
377
+ try {
378
+ registry.register(connA);
379
+ registry.register(connB);
380
+ // Both should hold the same lastActiveAt because Date.now is frozen.
381
+ expect(connA.lastActiveAt).toBe(connB.lastActiveAt);
382
+ // connB registered second, so its registrationSeq must be higher.
383
+ expect(connB.registrationSeq).toBeGreaterThan(
384
+ connA.registrationSeq ?? 0,
385
+ );
386
+ const msg: ServerMessage = {
387
+ type: "host_browser_cancel",
388
+ requestId: "req-1",
389
+ } as ServerMessage;
390
+ expect(registry.send("guardian-alpha", msg)).toBe(true);
391
+ } finally {
392
+ Date.now = originalNow;
393
+ }
394
+ // Default send should have landed on the newer instance (B),
395
+ // not the older insertion-order winner (A).
396
+ expect(fakeWsA.sent).toHaveLength(0);
397
+ expect(fakeWsB.sent).toHaveLength(1);
398
+ });
399
+
400
+ test("registrationSeq is monotonically increasing across registrations", () => {
401
+ // Sanity check on the counter itself — new registrations should
402
+ // always stamp a strictly greater sequence number than anything
403
+ // that came before, including re-registrations of the same
404
+ // instance after a supersede.
405
+ const registry = new ChromeExtensionRegistry();
406
+ const { conn: connA } = makeConnection(
407
+ "guardian-alpha",
408
+ "conn-A",
409
+ "install-A",
410
+ );
411
+ const { conn: connB } = makeConnection(
412
+ "guardian-alpha",
413
+ "conn-B",
414
+ "install-B",
415
+ );
416
+ const { conn: connC } = makeConnection(
417
+ "guardian-alpha",
418
+ "conn-C",
419
+ "install-A",
420
+ );
421
+ registry.register(connA);
422
+ registry.register(connB);
423
+ registry.register(connC); // supersedes install-A → conn-A closes
424
+ expect(connA.registrationSeq).toBeDefined();
425
+ expect(connB.registrationSeq).toBeDefined();
426
+ expect(connC.registrationSeq).toBeDefined();
427
+ expect(connB.registrationSeq!).toBeGreaterThan(connA.registrationSeq!);
428
+ expect(connC.registrationSeq!).toBeGreaterThan(connB.registrationSeq!);
429
+ });
430
+
431
+ test("same-millisecond supersede leaves the new connection as default", () => {
432
+ // Re-registering the same install within the same millisecond
433
+ // window must still promote the new connection to the default
434
+ // target — the tie-breaker should apply to re-registrations of
435
+ // the same instance just as it applies to sibling instances.
436
+ const registry = new ChromeExtensionRegistry();
437
+ const { conn: old } = makeConnection(
438
+ "guardian-alpha",
439
+ "old-id",
440
+ "install-A",
441
+ );
442
+ const { conn: fresh, fakeWs: fakeWsFresh } = makeConnection(
443
+ "guardian-alpha",
444
+ "fresh-id",
445
+ "install-A",
446
+ );
447
+ const originalNow = Date.now;
448
+ const frozenNow = originalNow();
449
+ Date.now = () => frozenNow;
450
+ try {
451
+ registry.register(old);
452
+ registry.register(fresh);
453
+ const msg: ServerMessage = {
454
+ type: "host_browser_cancel",
455
+ requestId: "req-1",
456
+ } as ServerMessage;
457
+ expect(registry.send("guardian-alpha", msg)).toBe(true);
458
+ } finally {
459
+ Date.now = originalNow;
460
+ }
461
+ // The fresh connection must receive the default send even though
462
+ // both entries stamped the exact same lastActiveAt.
463
+ expect(fakeWsFresh.sent).toHaveLength(1);
464
+ });
465
+ });
466
+
467
+ // ── Backwards compatibility ─────────────────────────────────────────
468
+ //
469
+ // Connections without a clientInstanceId (older extension builds or
470
+ // dev-bypass paths) synthesize a connection-scoped key so each one
471
+ // lives in its own slot. This gives sibling instances for the same
472
+ // guardian independent lifecycles even without explicit client ids.
473
+ describe("backwards compatibility when clientInstanceId is absent", () => {
474
+ test("two legacy connections under the same guardian coexist", () => {
475
+ const registry = new ChromeExtensionRegistry();
476
+ const { conn: connA } = makeConnection(
477
+ "guardian-alpha",
478
+ "conn-A",
479
+ // clientInstanceId intentionally omitted
480
+ );
481
+ const { conn: connB } = makeConnection(
482
+ "guardian-alpha",
483
+ "conn-B",
484
+ // clientInstanceId intentionally omitted
485
+ );
486
+ registry.register(connA);
487
+ registry.register(connB);
488
+ expect(registry.listInstances("guardian-alpha")).toHaveLength(2);
489
+ });
490
+
491
+ test("legacy unregister does not clobber a newer legacy sibling", () => {
492
+ const registry = new ChromeExtensionRegistry();
493
+ const { conn: oldConn } = makeConnection("guardian-alpha", "old-id");
494
+ const { conn: freshConn } = makeConnection("guardian-alpha", "fresh-id");
495
+ registry.register(oldConn);
496
+ registry.register(freshConn);
497
+ registry.unregister("old-id");
498
+ expect(registry.listInstances("guardian-alpha")).toHaveLength(1);
499
+ // The surviving entry is the newer connection.
500
+ const remaining = registry.listInstances("guardian-alpha");
501
+ expect(remaining[0]).toBe(freshConn);
502
+ });
503
+
504
+ test("legacy and instance-id connections can coexist under the same guardian", () => {
505
+ const registry = new ChromeExtensionRegistry();
506
+ const { conn: legacy } = makeConnection("guardian-alpha", "legacy-id");
507
+ const { conn: modern } = makeConnection(
508
+ "guardian-alpha",
509
+ "modern-id",
510
+ "install-A",
511
+ );
512
+ registry.register(legacy);
513
+ registry.register(modern);
514
+ expect(registry.listInstances("guardian-alpha")).toHaveLength(2);
515
+ expect(registry.getInstance("guardian-alpha", "install-A")).toBe(modern);
516
+ });
517
+ });
518
+ });
@@ -45,7 +45,7 @@ interface SubscriberEntry {
45
45
  * events that match their `assistantId` (and optionally `conversationId`).
46
46
  *
47
47
  * The hub is intentionally simple: synchronous fanout, no buffering, no
48
- * backpressure. Slow-consumer protection lives in the SSE route (PR 7).
48
+ * backpressure. Slow-consumer protection lives in the SSE route.
49
49
  */
50
50
  export class AssistantEventHub {
51
51
  private readonly subscribers = new Set<SubscriberEntry>();
@@ -194,6 +194,6 @@ export class AssistantEventHub {
194
194
  /**
195
195
  * Singleton hub shared across the entire runtime process.
196
196
  *
197
- * Import and use this in daemon send paths (PR 3) and the SSE route (PR 5).
197
+ * Import and use this in daemon send paths and the SSE route.
198
198
  */
199
199
  export const assistantEventHub = new AssistantEventHub({ maxSubscribers: 100 });
@@ -63,6 +63,7 @@ describe("route policy coverage", () => {
63
63
  // excluded because they are handled before JWT auth and are not composed
64
64
  // into buildRouteTable().
65
65
  const PRE_AUTH_ROUTE_MODULES = new Set([
66
+ "browser-extension-pair-routes.ts",
66
67
  "guardian-bootstrap-routes.ts",
67
68
  "guardian-refresh-routes.ts",
68
69
  ]);
@@ -34,7 +34,15 @@ mock.module("../../../config/env.js", () => ({
34
34
  }));
35
35
 
36
36
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../assistant-scope.js";
37
- import { authenticateRequest } from "../middleware.js";
37
+ import {
38
+ mintHostBrowserCapability,
39
+ resetCapabilityTokenSecretForTests,
40
+ setCapabilityTokenSecretForTests,
41
+ } from "../../capability-tokens.js";
42
+ import {
43
+ authenticateHostBrowserResultRequest,
44
+ authenticateRequest,
45
+ } from "../middleware.js";
38
46
  import { initAuthSigningKey, mintToken } from "../token-service.js";
39
47
  import type { ScopeProfile, TokenAudience } from "../types.js";
40
48
 
@@ -262,3 +270,110 @@ describe("authenticateRequest", () => {
262
270
  }
263
271
  });
264
272
  });
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // authenticateHostBrowserResultRequest — capability-token-aware auth for the
276
+ // /v1/host-browser-result POST route. Verifies that both the capability-token
277
+ // and JWT paths are accepted, and that a garbage bearer falls through to the
278
+ // JWT path and emits a 401 like any other invalid token.
279
+ // ---------------------------------------------------------------------------
280
+
281
+ describe("authenticateHostBrowserResultRequest", () => {
282
+ const CAPABILITY_SECRET = Buffer.alloc(32, 7);
283
+
284
+ beforeEach(() => {
285
+ // Pin the capability-token HMAC secret so mint/verify agree across
286
+ // the test run. The module-level secret cache is reset between
287
+ // tests so dev-bypass flipping doesn't leak stale state.
288
+ setCapabilityTokenSecretForTests(CAPABILITY_SECRET);
289
+ });
290
+
291
+ afterAll(() => {
292
+ resetCapabilityTokenSecretForTests();
293
+ });
294
+
295
+ test("accepts a valid capability token and synthesizes an actor AuthContext", () => {
296
+ const { token } = mintHostBrowserCapability("guardian-cap-happy");
297
+ const req = new Request("http://localhost/v1/host-browser-result", {
298
+ method: "POST",
299
+ headers: { Authorization: `Bearer ${token}` },
300
+ });
301
+
302
+ const result = authenticateHostBrowserResultRequest(req);
303
+ expect(result.ok).toBe(true);
304
+ if (result.ok) {
305
+ expect(result.context.principalType).toBe("actor");
306
+ expect(result.context.assistantId).toBe(DAEMON_INTERNAL_ASSISTANT_ID);
307
+ expect(result.context.actorPrincipalId).toBe("guardian-cap-happy");
308
+ expect(result.context.scopeProfile).toBe("actor_client_v1");
309
+ // The synthetic context must carry the scopes the route policy
310
+ // requires — otherwise the router would 403 the POST even though
311
+ // auth succeeded.
312
+ expect(result.context.scopes.has("approval.write")).toBe(true);
313
+ }
314
+ });
315
+
316
+ test("accepts a valid daemon-audience JWT (regression for the legacy path)", () => {
317
+ const token = mintValidToken({ sub: "actor:self:jwt-principal" });
318
+ const req = new Request("http://localhost/v1/host-browser-result", {
319
+ method: "POST",
320
+ headers: { Authorization: `Bearer ${token}` },
321
+ });
322
+
323
+ const result = authenticateHostBrowserResultRequest(req);
324
+ expect(result.ok).toBe(true);
325
+ if (result.ok) {
326
+ expect(result.context.principalType).toBe("actor");
327
+ expect(result.context.actorPrincipalId).toBe("jwt-principal");
328
+ expect(result.context.scopes.has("approval.write")).toBe(true);
329
+ }
330
+ });
331
+
332
+ test("returns 401 when the Authorization header is missing entirely", () => {
333
+ const req = new Request("http://localhost/v1/host-browser-result", {
334
+ method: "POST",
335
+ });
336
+
337
+ const result = authenticateHostBrowserResultRequest(req);
338
+ expect(result.ok).toBe(false);
339
+ if (!result.ok) {
340
+ expect(result.response.status).toBe(401);
341
+ }
342
+ });
343
+
344
+ test("malformed bearer falls through to JWT path and 401s", () => {
345
+ // A bearer that is neither a valid capability token (bad HMAC) nor a
346
+ // parseable JWT must fail the JWT path and return 401. This is the
347
+ // primary regression guard against someone accidentally making the
348
+ // capability-token branch "allow-anything" by swallowing
349
+ // verification failures.
350
+ const req = new Request("http://localhost/v1/host-browser-result", {
351
+ method: "POST",
352
+ headers: { Authorization: "Bearer not-a-token.xxxxxxxxxxxxx" },
353
+ });
354
+
355
+ const result = authenticateHostBrowserResultRequest(req);
356
+ expect(result.ok).toBe(false);
357
+ if (!result.ok) {
358
+ expect(result.response.status).toBe(401);
359
+ }
360
+ });
361
+
362
+ test("dev bypass returns synthetic AuthContext without Authorization header", () => {
363
+ authDisabled = true;
364
+
365
+ const req = new Request("http://localhost/v1/host-browser-result", {
366
+ method: "POST",
367
+ });
368
+
369
+ const result = authenticateHostBrowserResultRequest(req);
370
+ expect(result.ok).toBe(true);
371
+ if (result.ok) {
372
+ // Same synthetic context shape as authenticateRequest's dev
373
+ // bypass — the tests share the same invariant because a single
374
+ // helper builds both.
375
+ expect(result.context.principalType).toBe("actor");
376
+ expect(result.context.actorPrincipalId).toBe("dev-bypass");
377
+ }
378
+ });
379
+ });
@@ -168,6 +168,14 @@ describe("enforcePolicy", () => {
168
168
  expect(policy!.requiredScopes).toContain("approval.write");
169
169
  });
170
170
 
171
+ test("conversation host-access write requires approval.write scope", () => {
172
+ authDisabled = false;
173
+ const policy = getPolicy("conversations/host-access");
174
+ expect(policy).toBeDefined();
175
+ expect(policy!.requiredScopes).toContain("approval.write");
176
+ expect(policy!.requiredScopes).not.toContain("chat.write");
177
+ });
178
+
171
179
  test("events endpoint requires chat.read scope", () => {
172
180
  authDisabled = false;
173
181
  const policy = getPolicy("events");