@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,330 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { HostBrowserProxy } from "../../../../daemon/host-browser-proxy.js";
4
+ import { CdpError } from "../errors.js";
5
+ import {
6
+ createExtensionCdpClient,
7
+ ExtensionCdpClient,
8
+ } from "../extension-cdp-client.js";
9
+
10
+ type ProxyResult = { content: string; isError: boolean };
11
+
12
+ /**
13
+ * Build a fake HostBrowserProxy whose `request` method delegates to the
14
+ * provided handler. The returned object is structurally compatible with
15
+ * the parts of HostBrowserProxy that ExtensionCdpClient touches.
16
+ */
17
+ function fakeProxy(
18
+ handler: (
19
+ input: unknown,
20
+ conversationId: string,
21
+ signal?: AbortSignal,
22
+ ) => Promise<ProxyResult> | ProxyResult,
23
+ ): {
24
+ proxy: HostBrowserProxy;
25
+ request: ReturnType<typeof mock>;
26
+ } {
27
+ const request = mock(
28
+ async (
29
+ input: unknown,
30
+ conversationId: string,
31
+ signal?: AbortSignal,
32
+ ): Promise<ProxyResult> => handler(input, conversationId, signal),
33
+ );
34
+ const proxy = { request } as unknown as HostBrowserProxy;
35
+ return { proxy, request };
36
+ }
37
+
38
+ describe("ExtensionCdpClient", () => {
39
+ test("kind is 'extension' and exposes conversationId", () => {
40
+ const { proxy } = fakeProxy(async () => ({
41
+ content: "{}",
42
+ isError: false,
43
+ }));
44
+ const client = createExtensionCdpClient(proxy, "conv-abc");
45
+ expect(client).toBeInstanceOf(ExtensionCdpClient);
46
+ expect(client.kind).toBe("extension");
47
+ expect(client.conversationId).toBe("conv-abc");
48
+ });
49
+
50
+ test("happy path: send returns parsed JSON and forwards method/params/conversationId", async () => {
51
+ const { proxy, request } = fakeProxy(async () => ({
52
+ content: JSON.stringify({
53
+ product: "Chrome/123.0",
54
+ userAgent: "Mozilla/5.0",
55
+ }),
56
+ isError: false,
57
+ }));
58
+
59
+ const client = createExtensionCdpClient(proxy, "conv-1");
60
+ const result = await client.send<{ product: string; userAgent: string }>(
61
+ "Browser.getVersion",
62
+ );
63
+
64
+ expect(result).toEqual({
65
+ product: "Chrome/123.0",
66
+ userAgent: "Mozilla/5.0",
67
+ });
68
+ expect(request).toHaveBeenCalledTimes(1);
69
+ const call = request.mock.calls[0];
70
+ expect(call?.[0]).toEqual({
71
+ cdpMethod: "Browser.getVersion",
72
+ cdpParams: undefined,
73
+ cdpSessionId: undefined,
74
+ });
75
+ expect(call?.[1]).toBe("conv-1");
76
+ expect(call?.[2]).toBeUndefined();
77
+ });
78
+
79
+ test("params are forwarded verbatim to proxy.request", async () => {
80
+ const { proxy, request } = fakeProxy(async () => ({
81
+ content: JSON.stringify({ frameId: "frame-1", loaderId: "loader-1" }),
82
+ isError: false,
83
+ }));
84
+
85
+ const client = createExtensionCdpClient(proxy, "conv-2");
86
+ await client.send("Page.navigate", { url: "https://example.com/" });
87
+
88
+ expect(request).toHaveBeenCalledTimes(1);
89
+ expect(request.mock.calls[0]?.[0]).toEqual({
90
+ cdpMethod: "Page.navigate",
91
+ cdpParams: { url: "https://example.com/" },
92
+ cdpSessionId: undefined,
93
+ });
94
+ });
95
+
96
+ test("cdpSessionId from constructor is forwarded on every send", async () => {
97
+ const { proxy, request } = fakeProxy(async () => ({
98
+ content: "{}",
99
+ isError: false,
100
+ }));
101
+
102
+ const client = createExtensionCdpClient(proxy, "conv-3", "session-xyz");
103
+ await client.send("Runtime.evaluate", { expression: "1" });
104
+ await client.send("DOM.getDocument");
105
+
106
+ expect(request).toHaveBeenCalledTimes(2);
107
+ for (const call of request.mock.calls) {
108
+ expect((call?.[0] as { cdpSessionId?: string }).cdpSessionId).toBe(
109
+ "session-xyz",
110
+ );
111
+ }
112
+ });
113
+
114
+ test("result.isError === true throws CdpError('cdp_error') with .underlying === parsed", async () => {
115
+ const errorBody = {
116
+ code: -32000,
117
+ message: "Cannot find context with specified id",
118
+ };
119
+ const { proxy } = fakeProxy(async () => ({
120
+ content: JSON.stringify(errorBody),
121
+ isError: true,
122
+ }));
123
+
124
+ const client = createExtensionCdpClient(proxy, "conv-4");
125
+
126
+ let caught: unknown;
127
+ try {
128
+ await client.send("Runtime.evaluate", { expression: "boom" });
129
+ } catch (err) {
130
+ caught = err;
131
+ }
132
+
133
+ expect(caught).toBeInstanceOf(CdpError);
134
+ const err = caught as CdpError;
135
+ expect(err.code).toBe("cdp_error");
136
+ expect(err.message).toBe("Cannot find context with specified id");
137
+ expect(err.cdpMethod).toBe("Runtime.evaluate");
138
+ expect(err.cdpParams).toEqual({ expression: "boom" });
139
+ expect(err.underlying).toEqual(errorBody);
140
+ });
141
+
142
+ test("result.isError with non-string message falls back to default message", async () => {
143
+ const { proxy } = fakeProxy(async () => ({
144
+ content: JSON.stringify({ code: -32000 }),
145
+ isError: true,
146
+ }));
147
+
148
+ const client = createExtensionCdpClient(proxy, "conv-5");
149
+
150
+ let caught: unknown;
151
+ try {
152
+ await client.send("Target.attachToTarget");
153
+ } catch (err) {
154
+ caught = err;
155
+ }
156
+
157
+ expect(caught).toBeInstanceOf(CdpError);
158
+ const err = caught as CdpError;
159
+ expect(err.code).toBe("cdp_error");
160
+ expect(err.message).toBe("CDP error for Target.attachToTarget");
161
+ expect(err.underlying).toEqual({ code: -32000 });
162
+ });
163
+
164
+ test("non-JSON content throws CdpError('transport_error')", async () => {
165
+ const { proxy } = fakeProxy(async () => ({
166
+ content: "not json at all",
167
+ isError: false,
168
+ }));
169
+
170
+ const client = createExtensionCdpClient(proxy, "conv-6");
171
+
172
+ let caught: unknown;
173
+ try {
174
+ await client.send("Browser.getVersion");
175
+ } catch (err) {
176
+ caught = err;
177
+ }
178
+
179
+ expect(caught).toBeInstanceOf(CdpError);
180
+ const err = caught as CdpError;
181
+ expect(err.code).toBe("transport_error");
182
+ expect(err.cdpMethod).toBe("Browser.getVersion");
183
+ expect(err.underlying).toBeDefined();
184
+ // Underlying should be the JSON.parse error.
185
+ expect(err.underlying).toBeInstanceOf(Error);
186
+ });
187
+
188
+ test("result.content === 'Aborted' throws CdpError('aborted')", async () => {
189
+ const { proxy, request } = fakeProxy(async () => ({
190
+ content: "Aborted",
191
+ isError: true,
192
+ }));
193
+
194
+ const client = createExtensionCdpClient(proxy, "conv-7");
195
+
196
+ let caught: unknown;
197
+ try {
198
+ await client.send("Page.navigate", { url: "https://slow.example.com/" });
199
+ } catch (err) {
200
+ caught = err;
201
+ }
202
+
203
+ expect(caught).toBeInstanceOf(CdpError);
204
+ const err = caught as CdpError;
205
+ expect(err.code).toBe("aborted");
206
+ expect(err.cdpMethod).toBe("Page.navigate");
207
+ expect(err.cdpParams).toEqual({ url: "https://slow.example.com/" });
208
+ expect(request).toHaveBeenCalledTimes(1);
209
+ });
210
+
211
+ test("proxy.request throwing wraps as CdpError('transport_error') with .underlying", async () => {
212
+ const underlying = new Error("socket closed");
213
+ const { proxy } = fakeProxy(async () => {
214
+ throw underlying;
215
+ });
216
+
217
+ const client = createExtensionCdpClient(proxy, "conv-8");
218
+
219
+ let caught: unknown;
220
+ try {
221
+ await client.send("Runtime.evaluate", { expression: "1" });
222
+ } catch (err) {
223
+ caught = err;
224
+ }
225
+
226
+ expect(caught).toBeInstanceOf(CdpError);
227
+ const err = caught as CdpError;
228
+ expect(err.code).toBe("transport_error");
229
+ expect(err.message).toBe("socket closed");
230
+ expect(err.cdpMethod).toBe("Runtime.evaluate");
231
+ expect(err.cdpParams).toEqual({ expression: "1" });
232
+ expect(err.underlying).toBe(underlying);
233
+ });
234
+
235
+ test("proxy.request throwing non-Error wraps as CdpError('transport_error') with stringified message", async () => {
236
+ const { proxy } = fakeProxy(async () => {
237
+ throw "string error";
238
+ });
239
+
240
+ const client = createExtensionCdpClient(proxy, "conv-9");
241
+
242
+ let caught: unknown;
243
+ try {
244
+ await client.send("Browser.getVersion");
245
+ } catch (err) {
246
+ caught = err;
247
+ }
248
+
249
+ expect(caught).toBeInstanceOf(CdpError);
250
+ const err = caught as CdpError;
251
+ expect(err.code).toBe("transport_error");
252
+ expect(err.message).toBe("string error");
253
+ expect(err.underlying).toBe("string error");
254
+ });
255
+
256
+ test("send after dispose throws CdpError('disposed') without calling proxy", async () => {
257
+ const { proxy, request } = fakeProxy(async () => ({
258
+ content: "{}",
259
+ isError: false,
260
+ }));
261
+
262
+ const client = createExtensionCdpClient(proxy, "conv-10");
263
+ client.dispose();
264
+ // dispose is idempotent
265
+ client.dispose();
266
+
267
+ let caught: unknown;
268
+ try {
269
+ await client.send("Browser.getVersion");
270
+ } catch (err) {
271
+ caught = err;
272
+ }
273
+
274
+ expect(caught).toBeInstanceOf(CdpError);
275
+ const err = caught as CdpError;
276
+ expect(err.code).toBe("disposed");
277
+ expect(err.cdpMethod).toBe("Browser.getVersion");
278
+ expect(request).not.toHaveBeenCalled();
279
+ });
280
+
281
+ test("send with already-aborted signal throws CdpError('aborted') without calling proxy", async () => {
282
+ const { proxy, request } = fakeProxy(async () => ({
283
+ content: "{}",
284
+ isError: false,
285
+ }));
286
+
287
+ const client = createExtensionCdpClient(proxy, "conv-11");
288
+ const controller = new AbortController();
289
+ controller.abort();
290
+
291
+ let caught: unknown;
292
+ try {
293
+ await client.send("Browser.getVersion", undefined, controller.signal);
294
+ } catch (err) {
295
+ caught = err;
296
+ }
297
+
298
+ expect(caught).toBeInstanceOf(CdpError);
299
+ const err = caught as CdpError;
300
+ expect(err.code).toBe("aborted");
301
+ expect(err.message).toBe("Aborted before send");
302
+ expect(err.cdpMethod).toBe("Browser.getVersion");
303
+ expect(request).not.toHaveBeenCalled();
304
+ });
305
+
306
+ test("signal that aborts after proxy resolve throws CdpError('aborted')", async () => {
307
+ const controller = new AbortController();
308
+ const { proxy } = fakeProxy(async () => {
309
+ // Abort the signal before we return, simulating race between
310
+ // proxy resolution and the caller's abort.
311
+ controller.abort();
312
+ return { content: JSON.stringify({ ok: true }), isError: false };
313
+ });
314
+
315
+ const client = createExtensionCdpClient(proxy, "conv-12");
316
+
317
+ let caught: unknown;
318
+ try {
319
+ await client.send("Browser.getVersion", undefined, controller.signal);
320
+ } catch (err) {
321
+ caught = err;
322
+ }
323
+
324
+ expect(caught).toBeInstanceOf(CdpError);
325
+ const err = caught as CdpError;
326
+ expect(err.code).toBe("aborted");
327
+ expect(err.message).toBe("CDP call aborted");
328
+ expect(err.cdpMethod).toBe("Browser.getVersion");
329
+ });
330
+ });
@@ -0,0 +1,377 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { HostBrowserProxy } from "../../../../daemon/host-browser-proxy.js";
4
+ import type { ToolContext } from "../../../types.js";
5
+ import { CdpError } from "../errors.js";
6
+
7
+ type FakeClient = {
8
+ kind: "extension" | "local" | "cdp-inspect";
9
+ conversationId: string;
10
+ send: ReturnType<typeof mock>;
11
+ dispose: ReturnType<typeof mock>;
12
+ };
13
+
14
+ function makeFakeExtensionClient(conversationId: string): FakeClient {
15
+ return {
16
+ kind: "extension",
17
+ conversationId,
18
+ send: mock(async () => ({ ok: true, via: "extension" })),
19
+ dispose: mock(() => {}),
20
+ };
21
+ }
22
+
23
+ function makeFakeLocalClient(conversationId: string): FakeClient {
24
+ return {
25
+ kind: "local",
26
+ conversationId,
27
+ send: mock(async () => ({ ok: true, via: "local" })),
28
+ dispose: mock(() => {}),
29
+ };
30
+ }
31
+
32
+ function makeFakeCdpInspectClient(conversationId: string): FakeClient {
33
+ return {
34
+ kind: "cdp-inspect",
35
+ conversationId,
36
+ send: mock(async () => ({ ok: true, via: "cdp-inspect" })),
37
+ dispose: mock(() => {}),
38
+ };
39
+ }
40
+
41
+ let lastExtensionClient: FakeClient | undefined;
42
+ let lastLocalClient: FakeClient | undefined;
43
+ let lastCdpInspectClient: FakeClient | undefined;
44
+
45
+ const createExtensionCdpClientMock = mock(
46
+ (_proxy: HostBrowserProxy, conversationId: string) => {
47
+ const client = makeFakeExtensionClient(conversationId);
48
+ lastExtensionClient = client;
49
+ return client;
50
+ },
51
+ );
52
+
53
+ const createLocalCdpClientMock = mock((conversationId: string) => {
54
+ const client = makeFakeLocalClient(conversationId);
55
+ lastLocalClient = client;
56
+ return client;
57
+ });
58
+
59
+ const createCdpInspectClientMock = mock(
60
+ (conversationId: string, _options: unknown) => {
61
+ const client = makeFakeCdpInspectClient(conversationId);
62
+ lastCdpInspectClient = client;
63
+ return client;
64
+ },
65
+ );
66
+
67
+ /**
68
+ * Mutable config state. Tests flip `cdpInspectEnabled` to control
69
+ * the factory's config-based selection without needing a real config
70
+ * file.
71
+ */
72
+ let cdpInspectEnabled = false;
73
+
74
+ mock.module("../extension-cdp-client.js", () => ({
75
+ createExtensionCdpClient: createExtensionCdpClientMock,
76
+ }));
77
+ mock.module("../local-cdp-client.js", () => ({
78
+ createLocalCdpClient: createLocalCdpClientMock,
79
+ }));
80
+ mock.module("../cdp-inspect-client.js", () => ({
81
+ createCdpInspectClient: createCdpInspectClientMock,
82
+ }));
83
+ mock.module("../../../../config/loader.js", () => ({
84
+ getConfig: () => ({
85
+ hostBrowser: {
86
+ cdpInspect: {
87
+ enabled: cdpInspectEnabled,
88
+ host: "localhost",
89
+ port: 9222,
90
+ probeTimeoutMs: 500,
91
+ },
92
+ },
93
+ }),
94
+ }));
95
+
96
+ // Import under test AFTER mock.module calls so that the factory's
97
+ // top-level imports resolve to our fakes.
98
+ const { getCdpClient } = await import("../factory.js");
99
+
100
+ /**
101
+ * Minimal ToolContext suitable for factory tests. Only the fields the
102
+ * factory reads (`conversationId` and `hostBrowserProxy`) need to be
103
+ * populated; other required fields are cast away.
104
+ */
105
+ function makeContext(
106
+ overrides: Partial<ToolContext> & { conversationId: string },
107
+ ): ToolContext {
108
+ return overrides as unknown as ToolContext;
109
+ }
110
+
111
+ describe("getCdpClient", () => {
112
+ beforeEach(() => {
113
+ createExtensionCdpClientMock.mockClear();
114
+ createLocalCdpClientMock.mockClear();
115
+ createCdpInspectClientMock.mockClear();
116
+ lastExtensionClient = undefined;
117
+ lastLocalClient = undefined;
118
+ lastCdpInspectClient = undefined;
119
+ cdpInspectEnabled = false;
120
+ });
121
+
122
+ test("routes to ExtensionCdpClient when hostBrowserProxy is set", () => {
123
+ const fakeProxy = {
124
+ request: mock(async () => ({})),
125
+ } as unknown as HostBrowserProxy;
126
+ const ctx = makeContext({
127
+ conversationId: "test-convo",
128
+ hostBrowserProxy: fakeProxy,
129
+ });
130
+
131
+ const client = getCdpClient(ctx);
132
+
133
+ expect(client.kind).toBe("extension");
134
+ expect(client.conversationId).toBe("test-convo");
135
+ expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
136
+ expect(createExtensionCdpClientMock).toHaveBeenCalledWith(
137
+ fakeProxy,
138
+ "test-convo",
139
+ );
140
+ expect(createLocalCdpClientMock).not.toHaveBeenCalled();
141
+ expect(createCdpInspectClientMock).not.toHaveBeenCalled();
142
+ });
143
+
144
+ test("extension wins even when cdpInspect is enabled", () => {
145
+ cdpInspectEnabled = true;
146
+ const fakeProxy = {
147
+ request: mock(async () => ({})),
148
+ } as unknown as HostBrowserProxy;
149
+ const ctx = makeContext({
150
+ conversationId: "ext-wins",
151
+ hostBrowserProxy: fakeProxy,
152
+ });
153
+
154
+ const client = getCdpClient(ctx);
155
+
156
+ expect(client.kind).toBe("extension");
157
+ expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
158
+ expect(createCdpInspectClientMock).not.toHaveBeenCalled();
159
+ expect(createLocalCdpClientMock).not.toHaveBeenCalled();
160
+ });
161
+
162
+ test("routes to CdpInspectClient when cdpInspect is enabled and extension is absent", () => {
163
+ cdpInspectEnabled = true;
164
+ const ctx = makeContext({
165
+ conversationId: "inspect-convo",
166
+ hostBrowserProxy: undefined,
167
+ });
168
+
169
+ const client = getCdpClient(ctx);
170
+
171
+ expect(client.kind).toBe("cdp-inspect");
172
+ expect(client.conversationId).toBe("inspect-convo");
173
+ expect(createCdpInspectClientMock).toHaveBeenCalledTimes(1);
174
+ expect(createCdpInspectClientMock).toHaveBeenCalledWith("inspect-convo", {
175
+ host: "localhost",
176
+ port: 9222,
177
+ discoveryTimeoutMs: 500,
178
+ });
179
+ expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
180
+ expect(createLocalCdpClientMock).not.toHaveBeenCalled();
181
+ });
182
+
183
+ test("routes to LocalCdpClient when cdpInspect is disabled and extension is absent", () => {
184
+ cdpInspectEnabled = false;
185
+ const ctx = makeContext({
186
+ conversationId: "local-convo",
187
+ hostBrowserProxy: undefined,
188
+ });
189
+
190
+ const client = getCdpClient(ctx);
191
+
192
+ expect(client.kind).toBe("local");
193
+ expect(client.conversationId).toBe("local-convo");
194
+ expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
195
+ expect(createLocalCdpClientMock).toHaveBeenCalledWith("local-convo");
196
+ expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
197
+ expect(createCdpInspectClientMock).not.toHaveBeenCalled();
198
+ });
199
+
200
+ test("routes to LocalCdpClient when hostBrowserProxy key is omitted", () => {
201
+ const ctx = makeContext({ conversationId: "another-convo" });
202
+
203
+ const client = getCdpClient(ctx);
204
+
205
+ expect(client.kind).toBe("local");
206
+ expect(client.conversationId).toBe("another-convo");
207
+ expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
208
+ expect(createLocalCdpClientMock).toHaveBeenCalledWith("another-convo");
209
+ expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
210
+ expect(createCdpInspectClientMock).not.toHaveBeenCalled();
211
+ });
212
+
213
+ test("forwards send() through the manager to the extension-backed client", async () => {
214
+ const fakeProxy = {
215
+ request: mock(async () => ({})),
216
+ } as unknown as HostBrowserProxy;
217
+ const ctx = makeContext({
218
+ conversationId: "send-ext",
219
+ hostBrowserProxy: fakeProxy,
220
+ });
221
+
222
+ const client = getCdpClient(ctx);
223
+ const result = await client.send<{ ok: boolean; via: string }>(
224
+ "Page.navigate",
225
+ { url: "https://example.com" },
226
+ );
227
+
228
+ expect(result).toEqual({ ok: true, via: "extension" });
229
+ expect(lastExtensionClient?.send).toHaveBeenCalledTimes(1);
230
+ expect(lastExtensionClient?.send).toHaveBeenCalledWith(
231
+ "Page.navigate",
232
+ { url: "https://example.com" },
233
+ undefined,
234
+ );
235
+ expect(lastLocalClient).toBeUndefined();
236
+ expect(lastCdpInspectClient).toBeUndefined();
237
+ });
238
+
239
+ test("forwards send() through the manager to the local-backed client", async () => {
240
+ const ctx = makeContext({ conversationId: "send-local" });
241
+
242
+ const client = getCdpClient(ctx);
243
+ const result = await client.send<{ ok: boolean; via: string }>(
244
+ "Runtime.evaluate",
245
+ { expression: "1+1" },
246
+ );
247
+
248
+ expect(result).toEqual({ ok: true, via: "local" });
249
+ expect(lastLocalClient?.send).toHaveBeenCalledTimes(1);
250
+ expect(lastLocalClient?.send).toHaveBeenCalledWith(
251
+ "Runtime.evaluate",
252
+ { expression: "1+1" },
253
+ undefined,
254
+ );
255
+ expect(lastExtensionClient).toBeUndefined();
256
+ expect(lastCdpInspectClient).toBeUndefined();
257
+ });
258
+
259
+ test("forwards send() through the manager to the cdp-inspect-backed client", async () => {
260
+ cdpInspectEnabled = true;
261
+ const ctx = makeContext({ conversationId: "send-inspect" });
262
+
263
+ const client = getCdpClient(ctx);
264
+ const result = await client.send<{ ok: boolean; via: string }>(
265
+ "Page.navigate",
266
+ { url: "https://example.com" },
267
+ );
268
+
269
+ expect(result).toEqual({ ok: true, via: "cdp-inspect" });
270
+ expect(lastCdpInspectClient?.send).toHaveBeenCalledTimes(1);
271
+ expect(lastCdpInspectClient?.send).toHaveBeenCalledWith(
272
+ "Page.navigate",
273
+ { url: "https://example.com" },
274
+ undefined,
275
+ );
276
+ expect(lastExtensionClient).toBeUndefined();
277
+ expect(lastLocalClient).toBeUndefined();
278
+ });
279
+
280
+ test("propagates CdpError thrown by the underlying client", async () => {
281
+ const ctx = makeContext({ conversationId: "err-local" });
282
+ const client = getCdpClient(ctx);
283
+ const thrown = new CdpError("cdp_error", "kaboom", {
284
+ cdpMethod: "Page.navigate",
285
+ });
286
+ lastLocalClient!.send = mock(async () => {
287
+ throw thrown;
288
+ });
289
+
290
+ await expect(
291
+ client.send("Page.navigate", { url: "https://example.com" }),
292
+ ).rejects.toBe(thrown);
293
+ });
294
+
295
+ test("propagates caller AbortSignal to the underlying client", async () => {
296
+ const ctx = makeContext({ conversationId: "abort-local" });
297
+ const client = getCdpClient(ctx);
298
+ const controller = new AbortController();
299
+ let sawSignal: AbortSignal | undefined;
300
+ lastLocalClient!.send = mock(
301
+ async (
302
+ _method: string,
303
+ _params?: Record<string, unknown>,
304
+ signal?: AbortSignal,
305
+ ) => {
306
+ sawSignal = signal;
307
+ if (signal?.aborted) {
308
+ throw new CdpError("aborted", "aborted before send");
309
+ }
310
+ return {};
311
+ },
312
+ );
313
+
314
+ controller.abort();
315
+ await expect(
316
+ client.send("Page.navigate", { url: "https://x" }, controller.signal),
317
+ ).rejects.toMatchObject({ code: "aborted" });
318
+ expect(sawSignal).toBe(controller.signal);
319
+ });
320
+
321
+ test("dispose() tears down the underlying client and rejects further sends", async () => {
322
+ const ctx = makeContext({ conversationId: "dispose-local" });
323
+ const client = getCdpClient(ctx);
324
+
325
+ client.dispose();
326
+ expect(lastLocalClient?.dispose).toHaveBeenCalledTimes(1);
327
+
328
+ // A second dispose is a no-op.
329
+ client.dispose();
330
+ expect(lastLocalClient?.dispose).toHaveBeenCalledTimes(1);
331
+
332
+ await expect(client.send("Runtime.evaluate")).rejects.toMatchObject({
333
+ code: "disposed",
334
+ });
335
+ });
336
+
337
+ test("dispose() on an extension-backed client tears down the extension client", () => {
338
+ const fakeProxy = {
339
+ request: mock(async () => ({})),
340
+ } as unknown as HostBrowserProxy;
341
+ const ctx = makeContext({
342
+ conversationId: "dispose-ext",
343
+ hostBrowserProxy: fakeProxy,
344
+ });
345
+
346
+ const client = getCdpClient(ctx);
347
+ client.dispose();
348
+
349
+ expect(lastExtensionClient?.dispose).toHaveBeenCalledTimes(1);
350
+ });
351
+
352
+ test("dispose() on a cdp-inspect-backed client tears down the inspect client", () => {
353
+ cdpInspectEnabled = true;
354
+ const ctx = makeContext({ conversationId: "dispose-inspect" });
355
+
356
+ const client = getCdpClient(ctx);
357
+ client.dispose();
358
+
359
+ expect(lastCdpInspectClient?.dispose).toHaveBeenCalledTimes(1);
360
+ });
361
+
362
+ test("send() after dispose() on a cdp-inspect-backed client rejects with disposed", async () => {
363
+ cdpInspectEnabled = true;
364
+ const ctx = makeContext({ conversationId: "post-dispose-inspect" });
365
+
366
+ const client = getCdpClient(ctx);
367
+ client.dispose();
368
+
369
+ // Double dispose is a no-op.
370
+ client.dispose();
371
+ expect(lastCdpInspectClient?.dispose).toHaveBeenCalledTimes(1);
372
+
373
+ await expect(client.send("Page.navigate")).rejects.toMatchObject({
374
+ code: "disposed",
375
+ });
376
+ });
377
+ });