@vellumai/assistant 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (463) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docker-entrypoint.sh +12 -2
  4. package/docs/architecture/memory.md +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  7. package/openapi.yaml +184 -69
  8. package/package.json +41 -41
  9. package/scripts/generate-openapi.ts +1 -2
  10. package/src/__tests__/acp-session.test.ts +43 -0
  11. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +1 -0
  13. package/src/__tests__/app-source-watcher.test.ts +37 -11
  14. package/src/__tests__/approval-routes-http.test.ts +178 -1
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  17. package/src/__tests__/browser-manager.test.ts +40 -27
  18. package/src/__tests__/catalog-files.test.ts +862 -0
  19. package/src/__tests__/channel-approvals.test.ts +53 -0
  20. package/src/__tests__/checker.test.ts +104 -170
  21. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  22. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  23. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  24. package/src/__tests__/config-schema.test.ts +125 -48
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  26. package/src/__tests__/context-overflow-approval.test.ts +21 -6
  27. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  28. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  29. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  30. package/src/__tests__/conversation-attachments.test.ts +80 -4
  31. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  32. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  33. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  34. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  35. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  36. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  37. package/src/__tests__/conversation-queue.test.ts +45 -2
  38. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  39. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  40. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  42. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  43. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  44. package/src/__tests__/conversation-store.test.ts +195 -0
  45. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  46. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -3
  47. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  48. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  49. package/src/__tests__/credential-vault.test.ts +152 -13
  50. package/src/__tests__/credentials-cli.test.ts +2 -2
  51. package/src/__tests__/date-context.test.ts +4 -4
  52. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  53. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  54. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  55. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  56. package/src/__tests__/gemini-provider.test.ts +2 -2
  57. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  58. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  59. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  60. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  61. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  62. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  63. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  64. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  65. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  66. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  67. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  68. package/src/__tests__/host-browser-routes.test.ts +198 -0
  69. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  70. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  71. package/src/__tests__/host-file-proxy.test.ts +185 -1
  72. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  73. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  74. package/src/__tests__/host-shell-tool.test.ts +1 -11
  75. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  76. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  77. package/src/__tests__/inline-command-runner.test.ts +7 -5
  78. package/src/__tests__/integration-status.test.ts +6 -7
  79. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  80. package/src/__tests__/log-export-workspace.test.ts +190 -0
  81. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  82. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  83. package/src/__tests__/mcp-health-check.test.ts +10 -3
  84. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  85. package/src/__tests__/migration-export-http.test.ts +61 -2
  86. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  87. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  88. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  89. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  90. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  91. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  92. package/src/__tests__/oauth-cli.test.ts +707 -60
  93. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  94. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  95. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  96. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  97. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  98. package/src/__tests__/oauth-store.test.ts +1386 -182
  99. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  100. package/src/__tests__/onboarding-template-contract.test.ts +74 -55
  101. package/src/__tests__/openai-provider.test.ts +2 -2
  102. package/src/__tests__/outlook-categories.test.ts +1 -1
  103. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  104. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  105. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  106. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  107. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  108. package/src/__tests__/outlook-trash.test.ts +1 -1
  109. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  110. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  111. package/src/__tests__/permission-mode.test.ts +28 -56
  112. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  113. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  114. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  115. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  116. package/src/__tests__/require-fresh-approval.test.ts +40 -3
  117. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  118. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  119. package/src/__tests__/schedule-routes.test.ts +162 -0
  120. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  121. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  122. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  123. package/src/__tests__/set-permission-mode.test.ts +13 -250
  124. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  125. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  126. package/src/__tests__/slack-channel-config.test.ts +12 -15
  127. package/src/__tests__/subagent-detail.test.ts +44 -2
  128. package/src/__tests__/subagent-disposal.test.ts +1 -0
  129. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  130. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  131. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  132. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  133. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  134. package/src/__tests__/subagent-tools.test.ts +1 -0
  135. package/src/__tests__/subagent-types.test.ts +1 -0
  136. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  137. package/src/__tests__/system-prompt.test.ts +72 -1
  138. package/src/__tests__/task-scheduler.test.ts +32 -6
  139. package/src/__tests__/telegram-config.test.ts +10 -13
  140. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  141. package/src/__tests__/terminal-tools.test.ts +11 -5
  142. package/src/__tests__/test-preload.ts +14 -0
  143. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  144. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  145. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  146. package/src/__tests__/tool-executor.test.ts +0 -1
  147. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  148. package/src/__tests__/top-level-renderer.test.ts +73 -1
  149. package/src/__tests__/transport-hints-queue.test.ts +62 -0
  150. package/src/__tests__/trust-store.test.ts +4 -4
  151. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  152. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  153. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  154. package/src/__tests__/workspace-policy.test.ts +2 -7
  155. package/src/acp/client-handler.ts +30 -4
  156. package/src/agent/loop.ts +12 -35
  157. package/src/approvals/guardian-request-resolvers.ts +21 -15
  158. package/src/browser-session/__tests__/manager.test.ts +297 -0
  159. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  160. package/src/browser-session/backends/extension.ts +26 -0
  161. package/src/browser-session/backends/local.ts +24 -0
  162. package/src/browser-session/events.ts +164 -0
  163. package/src/browser-session/index.ts +27 -0
  164. package/src/browser-session/manager.ts +159 -0
  165. package/src/browser-session/types.ts +28 -0
  166. package/src/channels/__tests__/types.test.ts +134 -0
  167. package/src/channels/types.ts +55 -0
  168. package/src/cli/__tests__/run-assistant-command.ts +34 -7
  169. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  170. package/src/cli/commands/browser-relay.ts +339 -409
  171. package/src/cli/commands/credentials.ts +3 -3
  172. package/src/cli/commands/default-action.ts +68 -1
  173. package/src/cli/commands/email.ts +18 -13
  174. package/src/cli/commands/mcp.ts +16 -4
  175. package/src/cli/commands/oauth/__tests__/connect.test.ts +68 -41
  176. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  177. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  178. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  179. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  180. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  181. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  182. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  183. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  184. package/src/cli/commands/oauth/apps.ts +7 -4
  185. package/src/cli/commands/oauth/connect.ts +16 -2
  186. package/src/cli/commands/oauth/disconnect.ts +1 -1
  187. package/src/cli/commands/oauth/providers.ts +200 -36
  188. package/src/cli/commands/oauth/shared.ts +5 -5
  189. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  190. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  191. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  192. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  193. package/src/cli/commands/platform/index.ts +107 -10
  194. package/src/cli/commands/usage.ts +10 -9
  195. package/src/cli/lib/daemon-credential-client.ts +4 -0
  196. package/src/cli/program.ts +10 -3
  197. package/src/config/assistant-feature-flags.ts +59 -55
  198. package/src/config/bundled-skills/app-builder/SKILL.md +33 -173
  199. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  200. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  201. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  202. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  203. package/src/config/bundled-skills/document/SKILL.md +4 -0
  204. package/src/config/bundled-skills/gmail/SKILL.md +12 -7
  205. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  206. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  207. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  208. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  209. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  210. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  211. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  212. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  213. package/src/config/env-registry.ts +14 -0
  214. package/src/config/env.ts +21 -0
  215. package/src/config/feature-flag-registry.json +46 -7
  216. package/src/config/loader.ts +56 -1
  217. package/src/config/sanitize-for-transfer.ts +47 -0
  218. package/src/config/schema.ts +46 -5
  219. package/src/config/schemas/host-browser.ts +66 -0
  220. package/src/config/schemas/memory-lifecycle.ts +1 -1
  221. package/src/config/schemas/memory-retrieval.ts +103 -0
  222. package/src/config/schemas/security.ts +0 -6
  223. package/src/config/schemas/services.ts +16 -0
  224. package/src/config/types.ts +0 -1
  225. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  226. package/src/context/window-manager.ts +19 -1
  227. package/src/credential-execution/approval-bridge.ts +49 -16
  228. package/src/credential-execution/managed-catalog.ts +3 -7
  229. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  230. package/src/daemon/app-source-watcher.ts +35 -0
  231. package/src/daemon/config-watcher.ts +6 -2
  232. package/src/daemon/context-overflow-approval.ts +5 -1
  233. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  234. package/src/daemon/conversation-agent-loop.ts +74 -19
  235. package/src/daemon/conversation-attachments.ts +40 -1
  236. package/src/daemon/conversation-messaging.ts +3 -0
  237. package/src/daemon/conversation-process.ts +66 -3
  238. package/src/daemon/conversation-queue-manager.ts +8 -0
  239. package/src/daemon/conversation-runtime-assembly.ts +159 -20
  240. package/src/daemon/conversation-surfaces.ts +78 -12
  241. package/src/daemon/conversation-tool-setup.ts +74 -11
  242. package/src/daemon/conversation-workspace.ts +12 -0
  243. package/src/daemon/conversation.ts +227 -11
  244. package/src/daemon/date-context.ts +10 -10
  245. package/src/daemon/first-greeting.ts +3 -2
  246. package/src/daemon/handlers/conversations.ts +9 -139
  247. package/src/daemon/handlers/shared.ts +65 -0
  248. package/src/daemon/handlers/skills.ts +232 -37
  249. package/src/daemon/host-bash-proxy.ts +48 -13
  250. package/src/daemon/host-browser-proxy.ts +191 -0
  251. package/src/daemon/host-cu-proxy.ts +36 -11
  252. package/src/daemon/host-file-proxy.ts +57 -9
  253. package/src/daemon/lifecycle.ts +86 -12
  254. package/src/daemon/message-protocol.ts +7 -0
  255. package/src/daemon/message-types/conversations.ts +59 -13
  256. package/src/daemon/message-types/host-browser.ts +100 -0
  257. package/src/daemon/message-types/messages.ts +5 -6
  258. package/src/daemon/message-types/notifications.ts +12 -0
  259. package/src/daemon/message-types/settings.ts +12 -0
  260. package/src/daemon/message-types/skills.ts +10 -0
  261. package/src/daemon/message-types/subagents.ts +2 -0
  262. package/src/daemon/server.ts +112 -35
  263. package/src/daemon/tool-side-effects.ts +6 -0
  264. package/src/daemon/transport-hints.ts +14 -0
  265. package/src/inbound/platform-callback-registration.ts +18 -17
  266. package/src/index.ts +1 -1
  267. package/src/mcp/client.ts +59 -24
  268. package/src/memory/app-store.ts +31 -1
  269. package/src/memory/conversation-crud.ts +38 -10
  270. package/src/memory/conversation-directories.ts +39 -0
  271. package/src/memory/conversation-group-migration.ts +65 -5
  272. package/src/memory/conversation-starters-cadence.ts +76 -0
  273. package/src/memory/conversation-title-service.ts +5 -2
  274. package/src/memory/db-init.ts +12 -0
  275. package/src/memory/embedding-backend.test.ts +75 -0
  276. package/src/memory/embedding-backend.ts +131 -5
  277. package/src/memory/embedding-gemini.test.ts +54 -0
  278. package/src/memory/embedding-gemini.ts +20 -9
  279. package/src/memory/embedding-local.ts +177 -18
  280. package/src/memory/graph/capability-seed.ts +3 -5
  281. package/src/memory/graph/consolidation.ts +10 -23
  282. package/src/memory/graph/extraction-job.ts +15 -0
  283. package/src/memory/graph/retriever.ts +40 -22
  284. package/src/memory/graph/store.test.ts +7 -3
  285. package/src/memory/graph/store.ts +47 -12
  286. package/src/memory/group-crud.ts +25 -9
  287. package/src/memory/llm-usage-store.ts +45 -4
  288. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  289. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  290. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  291. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  292. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  293. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  294. package/src/memory/migrations/index.ts +6 -0
  295. package/src/memory/migrations/registry.ts +8 -0
  296. package/src/memory/schema/conversations.ts +1 -0
  297. package/src/memory/schema/oauth.ts +18 -13
  298. package/src/messaging/provider.ts +1 -1
  299. package/src/notifications/broadcaster.ts +6 -0
  300. package/src/notifications/conversation-pairing.ts +12 -4
  301. package/src/notifications/emit-signal.ts +14 -0
  302. package/src/notifications/signal.ts +11 -0
  303. package/src/oauth/AGENTS.md +76 -0
  304. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  305. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  306. package/src/oauth/byo-connection.test.ts +8 -8
  307. package/src/oauth/byo-connection.ts +7 -7
  308. package/src/oauth/connect-orchestrator.ts +23 -21
  309. package/src/oauth/connect-types.ts +3 -3
  310. package/src/oauth/connection-resolver.test.ts +17 -4
  311. package/src/oauth/connection-resolver.ts +16 -16
  312. package/src/oauth/connection.ts +1 -1
  313. package/src/oauth/manual-token-connection.ts +13 -13
  314. package/src/oauth/oauth-store.ts +214 -100
  315. package/src/oauth/platform-connection.test.ts +5 -5
  316. package/src/oauth/platform-connection.ts +4 -4
  317. package/src/oauth/provider-serializer.ts +31 -5
  318. package/src/oauth/revoke.ts +76 -0
  319. package/src/oauth/seed-providers.ts +127 -87
  320. package/src/oauth/token-persistence.ts +1 -1
  321. package/src/permissions/checker.ts +3 -3
  322. package/src/permissions/defaults.ts +7 -8
  323. package/src/permissions/permission-mode.ts +4 -11
  324. package/src/permissions/prompter.ts +13 -3
  325. package/src/permissions/v2-consent-policy.ts +87 -0
  326. package/src/platform/client.ts +1 -1
  327. package/src/prompts/system-prompt.ts +18 -21
  328. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  329. package/src/prompts/templates/BOOTSTRAP.md +59 -96
  330. package/src/prompts/templates/SOUL.md +11 -11
  331. package/src/providers/anthropic/client.ts +1 -0
  332. package/src/providers/types.ts +1 -1
  333. package/src/runtime/AGENTS.md +23 -0
  334. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  335. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  336. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  337. package/src/runtime/assistant-event-hub.ts +24 -2
  338. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  339. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  341. package/src/runtime/auth/middleware.ts +98 -0
  342. package/src/runtime/auth/route-policy.ts +6 -7
  343. package/src/runtime/auth/token-service.ts +8 -0
  344. package/src/runtime/capability-tokens.ts +414 -0
  345. package/src/runtime/channel-approvals.ts +18 -5
  346. package/src/runtime/chrome-extension-registry.ts +332 -0
  347. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  348. package/src/runtime/guardian-decision-types.ts +7 -0
  349. package/src/runtime/http-server.ts +425 -70
  350. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  351. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  352. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  353. package/src/runtime/migrations/migration-transport.ts +6 -0
  354. package/src/runtime/migrations/migration-wizard.ts +22 -2
  355. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  356. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  357. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  358. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  359. package/src/runtime/pending-interactions.ts +29 -13
  360. package/src/runtime/routes/approval-routes.ts +90 -16
  361. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  362. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  363. package/src/runtime/routes/conversation-analysis-routes.ts +18 -5
  364. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  365. package/src/runtime/routes/conversation-routes.ts +308 -28
  366. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  367. package/src/runtime/routes/group-routes.ts +22 -8
  368. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  369. package/src/runtime/routes/host-browser-routes.ts +279 -0
  370. package/src/runtime/routes/host-file-routes.ts +9 -1
  371. package/src/runtime/routes/identity-routes.ts +259 -16
  372. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  373. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  374. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  375. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  376. package/src/runtime/routes/log-export-routes.ts +60 -25
  377. package/src/runtime/routes/memory-item-routes.ts +1 -7
  378. package/src/runtime/routes/migration-routes.ts +87 -2
  379. package/src/runtime/routes/oauth-apps.ts +15 -17
  380. package/src/runtime/routes/oauth-providers.ts +4 -0
  381. package/src/runtime/routes/schedule-routes.ts +24 -11
  382. package/src/runtime/routes/settings-routes.ts +9 -97
  383. package/src/runtime/routes/skills-routes.ts +52 -2
  384. package/src/runtime/routes/subagents-routes.ts +14 -10
  385. package/src/runtime/routes/usage-routes.ts +8 -7
  386. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  387. package/src/runtime/routes/workspace-routes.ts +8 -1
  388. package/src/runtime/routes/workspace-utils.ts +2 -0
  389. package/src/schedule/scheduler.ts +7 -5
  390. package/src/security/ces-credential-client.ts +20 -0
  391. package/src/security/ces-rpc-credential-backend.ts +17 -0
  392. package/src/security/credential-backend.ts +5 -0
  393. package/src/security/oauth2.ts +42 -25
  394. package/src/security/secure-keys.ts +118 -25
  395. package/src/security/token-manager.ts +23 -10
  396. package/src/skills/catalog-files.ts +492 -0
  397. package/src/skills/inline-command-runner.ts +12 -14
  398. package/src/subagent/manager.ts +131 -26
  399. package/src/subagent/types.ts +19 -0
  400. package/src/tools/apps/executors.ts +11 -2
  401. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  402. package/src/tools/browser/auth-detector.ts +43 -12
  403. package/src/tools/browser/browser-execution.ts +645 -340
  404. package/src/tools/browser/browser-manager.ts +36 -12
  405. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  406. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  407. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  408. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  409. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  410. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  411. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  412. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  413. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  414. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  415. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  416. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  417. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  418. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  419. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  420. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  421. package/src/tools/browser/cdp-client/errors.ts +34 -0
  422. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  423. package/src/tools/browser/cdp-client/factory.ts +204 -0
  424. package/src/tools/browser/cdp-client/index.ts +14 -0
  425. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  426. package/src/tools/browser/cdp-client/types.ts +52 -0
  427. package/src/tools/filesystem/edit.ts +1 -1
  428. package/src/tools/filesystem/list.ts +1 -1
  429. package/src/tools/filesystem/read.ts +1 -1
  430. package/src/tools/filesystem/write.ts +2 -1
  431. package/src/tools/host-filesystem/edit.ts +1 -1
  432. package/src/tools/host-filesystem/read.ts +12 -15
  433. package/src/tools/host-filesystem/write.ts +1 -1
  434. package/src/tools/host-terminal/host-shell.ts +21 -16
  435. package/src/tools/permission-checker.ts +77 -100
  436. package/src/tools/registry.ts +0 -2
  437. package/src/tools/secret-detection-handler.ts +34 -1
  438. package/src/tools/shared/filesystem/image-read.ts +61 -40
  439. package/src/tools/skills/sandbox-runner.ts +3 -6
  440. package/src/tools/subagent/spawn.ts +47 -3
  441. package/src/tools/subagent/status.ts +2 -0
  442. package/src/tools/system/register.ts +2 -16
  443. package/src/tools/terminal/safe-env.ts +7 -0
  444. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  445. package/src/tools/terminal/sandbox.ts +4 -1
  446. package/src/tools/terminal/shell.ts +24 -21
  447. package/src/tools/tool-approval-handler.ts +48 -2
  448. package/src/tools/types.ts +2 -3
  449. package/src/util/platform.ts +14 -19
  450. package/src/watcher/provider-types.ts +1 -1
  451. package/src/workspace/migrations/029-seed-pkb.ts +1 -0
  452. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  453. package/src/workspace/migrations/registry.ts +2 -0
  454. package/src/workspace/top-level-renderer.ts +19 -1
  455. package/src/__tests__/chrome-cdp.test.ts +0 -419
  456. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  457. package/src/__tests__/permission-mode-store.test.ts +0 -277
  458. package/src/browser-extension-relay/protocol.ts +0 -63
  459. package/src/browser-extension-relay/server.ts +0 -203
  460. package/src/config/schemas/sandbox.ts +0 -14
  461. package/src/permissions/permission-mode-store.ts +0 -180
  462. package/src/tools/browser/chrome-cdp.ts +0 -239
  463. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -0,0 +1,870 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // Silence the logger from cdp-inspect-client.
4
+ mock.module("../../../../util/logger.js", () => ({
5
+ getLogger: () => ({
6
+ info: () => {},
7
+ debug: () => {},
8
+ warn: () => {},
9
+ error: () => {},
10
+ }),
11
+ }));
12
+
13
+ // Import under test AFTER mock.module calls so that the module's
14
+ // top-level logger import resolves to our fake.
15
+ const { CdpInspectClient, createCdpInspectClient } =
16
+ await import("../cdp-inspect-client.js");
17
+ const { CdpError } = await import("../errors.js");
18
+ const { CdpWsTransportError } = await import("../cdp-inspect/ws-transport.js");
19
+
20
+ type CdpInspectClientInstance = InstanceType<typeof CdpInspectClient>;
21
+
22
+ /**
23
+ * Minimal fake CdpWsTransport used by the test harness below. The
24
+ * handler is per-send so individual tests can model success, CDP
25
+ * errors, transport errors, and abort behavior on specific methods.
26
+ */
27
+ interface FakeTransportOptions {
28
+ onSend?: (
29
+ method: string,
30
+ params: Record<string, unknown> | undefined,
31
+ opts: { sessionId?: string; signal?: AbortSignal },
32
+ ) => unknown | Promise<unknown>;
33
+ trackSends?: Array<{
34
+ method: string;
35
+ params?: Record<string, unknown>;
36
+ sessionId?: string;
37
+ }>;
38
+ trackDisposeCount?: { count: number };
39
+ }
40
+
41
+ function createFakeTransport(options: FakeTransportOptions) {
42
+ const transport = {
43
+ send: async <T = unknown>(
44
+ method: string,
45
+ params?: Record<string, unknown>,
46
+ opts?: { sessionId?: string; signal?: AbortSignal },
47
+ ): Promise<T> => {
48
+ options.trackSends?.push({
49
+ method,
50
+ params,
51
+ sessionId: opts?.sessionId,
52
+ });
53
+ if (options.onSend) {
54
+ const result = await options.onSend(method, params, opts ?? {});
55
+ return result as T;
56
+ }
57
+ return undefined as T;
58
+ },
59
+ addEventListener: () => () => {},
60
+ dispose: () => {
61
+ if (options.trackDisposeCount) {
62
+ options.trackDisposeCount.count += 1;
63
+ }
64
+ },
65
+ };
66
+ return transport;
67
+ }
68
+
69
+ /**
70
+ * Build a client wired to mocked discovery + transport helpers. The
71
+ * caller supplies handlers for the moving pieces; everything else
72
+ * defaults to a happy-path attach.
73
+ */
74
+ interface HarnessOptions {
75
+ probeImpl?: (opts: unknown) => Promise<{
76
+ browser: string;
77
+ protocolVersion: string;
78
+ webSocketDebuggerUrl: string;
79
+ }>;
80
+ listImpl?: (opts: unknown) => Promise<
81
+ Array<{
82
+ id: string;
83
+ type: string;
84
+ title: string;
85
+ url: string;
86
+ webSocketDebuggerUrl: string;
87
+ }>
88
+ >;
89
+ connectImpl?: (
90
+ url: string,
91
+ opts?: { connectTimeoutMs?: number },
92
+ ) => Promise<ReturnType<typeof createFakeTransport>>;
93
+ transportOnSend?: FakeTransportOptions["onSend"];
94
+ conversationId?: string;
95
+ }
96
+
97
+ interface Harness {
98
+ client: CdpInspectClientInstance;
99
+ sends: Array<{
100
+ method: string;
101
+ params?: Record<string, unknown>;
102
+ sessionId?: string;
103
+ }>;
104
+ disposeCount: { count: number };
105
+ probeCalls: number;
106
+ listCalls: number;
107
+ connectCalls: number;
108
+ attachCallCount: () => number;
109
+ }
110
+
111
+ function createHarness(opts: HarnessOptions = {}): Harness {
112
+ const sends: Array<{
113
+ method: string;
114
+ params?: Record<string, unknown>;
115
+ sessionId?: string;
116
+ }> = [];
117
+ const disposeCount = { count: 0 };
118
+ let probeCalls = 0;
119
+ let listCalls = 0;
120
+ let connectCalls = 0;
121
+
122
+ // Track Target.attachToTarget specifically so tests can assert
123
+ // how many attach attempts the client has made. The counter is
124
+ // bumped ONLY in the default happy-path branch so tests that
125
+ // install a custom `transportOnSend` (and therefore model their
126
+ // own attach semantics) can't accidentally double-count.
127
+ const attachSends: Array<unknown> = [];
128
+
129
+ const defaultOnSend: FakeTransportOptions["onSend"] = (method) => {
130
+ if (method === "Target.attachToTarget") {
131
+ attachSends.push(method);
132
+ return { sessionId: "fake-session-id" };
133
+ }
134
+ return { ok: true };
135
+ };
136
+
137
+ const transportOnSend: FakeTransportOptions["onSend"] = async (
138
+ method,
139
+ params,
140
+ o,
141
+ ) => {
142
+ if (opts.transportOnSend) {
143
+ return opts.transportOnSend(method, params, o);
144
+ }
145
+ return defaultOnSend!(method, params, o);
146
+ };
147
+
148
+ const client = createCdpInspectClient(opts.conversationId ?? "conv-1", {
149
+ host: "127.0.0.1",
150
+ port: 9222,
151
+ discoveryTimeoutMs: 100,
152
+ wsConnectTimeoutMs: 100,
153
+ helpers: {
154
+ probeDevToolsJsonVersion: async (probeOpts: unknown) => {
155
+ probeCalls += 1;
156
+ if (opts.probeImpl) return opts.probeImpl(probeOpts);
157
+ return {
158
+ browser: "Chrome/125.0.0.0",
159
+ protocolVersion: "1.3",
160
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
161
+ };
162
+ },
163
+ listDevToolsTargets: async (listOpts: unknown) => {
164
+ listCalls += 1;
165
+ if (opts.listImpl) return opts.listImpl(listOpts);
166
+ return [
167
+ {
168
+ id: "target-1",
169
+ type: "page",
170
+ title: "Example",
171
+ url: "https://example.com/",
172
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/target-1",
173
+ },
174
+ ];
175
+ },
176
+ // pickDefaultTarget uses the real implementation — it's pure.
177
+ connectCdpWsTransport: async (
178
+ url: string,
179
+ connectOpts?: { connectTimeoutMs?: number },
180
+ ) => {
181
+ connectCalls += 1;
182
+ if (opts.connectImpl) return opts.connectImpl(url, connectOpts);
183
+ return createFakeTransport({
184
+ onSend: transportOnSend,
185
+ trackSends: sends,
186
+ trackDisposeCount: disposeCount,
187
+ });
188
+ },
189
+ },
190
+ });
191
+
192
+ return {
193
+ client,
194
+ sends,
195
+ disposeCount,
196
+ get probeCalls() {
197
+ return probeCalls;
198
+ },
199
+ get listCalls() {
200
+ return listCalls;
201
+ },
202
+ get connectCalls() {
203
+ return connectCalls;
204
+ },
205
+ attachCallCount: () => attachSends.length,
206
+ };
207
+ }
208
+
209
+ describe("CdpInspectClient", () => {
210
+ beforeEach(() => {
211
+ // no-op — each test gets its own harness
212
+ });
213
+
214
+ test("kind is 'cdp-inspect' and exposes conversationId", () => {
215
+ const { client } = createHarness({ conversationId: "conv-kind" });
216
+ expect(client).toBeInstanceOf(CdpInspectClient);
217
+ expect(client.kind).toBe("cdp-inspect");
218
+ expect(client.conversationId).toBe("conv-kind");
219
+ });
220
+
221
+ test("send() probes version, lists targets, attaches, and forwards the call", async () => {
222
+ const harness = createHarness({
223
+ transportOnSend: (method) => {
224
+ if (method === "Target.attachToTarget") {
225
+ return { sessionId: "session-abc" };
226
+ }
227
+ if (method === "Browser.getVersion") {
228
+ return { product: "HeadlessChrome/125.0.0.0" };
229
+ }
230
+ return undefined;
231
+ },
232
+ });
233
+ const result = await harness.client.send<{ product: string }>(
234
+ "Browser.getVersion",
235
+ );
236
+ expect(result).toEqual({ product: "HeadlessChrome/125.0.0.0" });
237
+ expect(harness.probeCalls).toBe(1);
238
+ expect(harness.listCalls).toBe(1);
239
+ expect(harness.connectCalls).toBe(1);
240
+ // One attach + one forwarded Browser.getVersion.
241
+ expect(harness.sends).toEqual([
242
+ {
243
+ method: "Target.attachToTarget",
244
+ params: { targetId: "target-1", flatten: true },
245
+ sessionId: undefined,
246
+ },
247
+ {
248
+ method: "Browser.getVersion",
249
+ params: undefined,
250
+ sessionId: "session-abc",
251
+ },
252
+ ]);
253
+ });
254
+
255
+ test("multiple send() calls share a single attach", async () => {
256
+ const harness = createHarness();
257
+ await harness.client.send("Runtime.enable");
258
+ await harness.client.send("Page.enable");
259
+ await harness.client.send("DOM.enable");
260
+ expect(harness.probeCalls).toBe(1);
261
+ expect(harness.listCalls).toBe(1);
262
+ expect(harness.connectCalls).toBe(1);
263
+ expect(harness.attachCallCount()).toBe(1);
264
+ expect(harness.sends.length).toBe(4); // 1 attach + 3 forwarded
265
+ });
266
+
267
+ test("concurrent send() calls share a single in-flight attach", async () => {
268
+ const harness = createHarness();
269
+ await Promise.all([
270
+ harness.client.send("Runtime.enable"),
271
+ harness.client.send("Page.enable"),
272
+ harness.client.send("DOM.enable"),
273
+ ]);
274
+ expect(harness.probeCalls).toBe(1);
275
+ expect(harness.listCalls).toBe(1);
276
+ expect(harness.connectCalls).toBe(1);
277
+ expect(harness.attachCallCount()).toBe(1);
278
+ });
279
+
280
+ test("send() retries ensureSession after an initial attach failure", async () => {
281
+ // First probe call rejects (simulating e.g. Chrome not yet listening).
282
+ // Second probe call succeeds. Because the cached sessionPromise must
283
+ // be cleared on rejection, the second send() performs a full retry.
284
+ let probeCount = 0;
285
+ const harness = createHarness({
286
+ probeImpl: async () => {
287
+ probeCount += 1;
288
+ if (probeCount === 1) {
289
+ throw new Error("connect ECONNREFUSED");
290
+ }
291
+ return {
292
+ browser: "Chrome/125.0.0.0",
293
+ protocolVersion: "1.3",
294
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
295
+ };
296
+ },
297
+ });
298
+
299
+ let firstErr: unknown;
300
+ try {
301
+ await harness.client.send("Browser.getVersion");
302
+ } catch (err) {
303
+ firstErr = err;
304
+ }
305
+ expect(firstErr).toBeInstanceOf(CdpError);
306
+ expect((firstErr as InstanceType<typeof CdpError>).code).toBe(
307
+ "transport_error",
308
+ );
309
+ expect(probeCount).toBe(1);
310
+ expect(harness.connectCalls).toBe(0);
311
+
312
+ // Second call — cached promise was cleared, so probe + list +
313
+ // connect + attach all run again, then the forwarded call
314
+ // resolves normally. listCalls is only 1 because the first
315
+ // attempt threw inside probeDevToolsJsonVersion before it ever
316
+ // reached listDevToolsTargets.
317
+ const result = await harness.client.send<{ ok: boolean }>(
318
+ "Browser.getVersion",
319
+ );
320
+ expect(result).toEqual({ ok: true });
321
+ expect(probeCount).toBe(2);
322
+ expect(harness.listCalls).toBe(1);
323
+ expect(harness.connectCalls).toBe(1);
324
+ expect(harness.attachCallCount()).toBe(1);
325
+ });
326
+
327
+ test("send() maps CDP protocol errors from attach to CdpError 'cdp_error'", async () => {
328
+ const harness = createHarness({
329
+ transportOnSend: async (method) => {
330
+ if (method === "Target.attachToTarget") {
331
+ throw new CdpWsTransportError(
332
+ "cdp_error",
333
+ "No target with given id found",
334
+ {
335
+ cdpMethod: "Target.attachToTarget",
336
+ cdpCode: -32602,
337
+ cdpMessage: "No target with given id found",
338
+ },
339
+ );
340
+ }
341
+ return undefined;
342
+ },
343
+ });
344
+
345
+ let caught: unknown;
346
+ try {
347
+ await harness.client.send("Browser.getVersion");
348
+ } catch (err) {
349
+ caught = err;
350
+ }
351
+ expect(caught).toBeInstanceOf(CdpError);
352
+ const cdpErr = caught as InstanceType<typeof CdpError>;
353
+ expect(cdpErr.code).toBe("cdp_error");
354
+ expect(cdpErr.message).toBe("No target with given id found");
355
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
356
+ expect(cdpErr.underlying).toBeInstanceOf(CdpWsTransportError);
357
+ });
358
+
359
+ test("send() maps transport failures during attach to CdpError 'transport_error'", async () => {
360
+ const harness = createHarness({
361
+ connectImpl: async () => {
362
+ throw new CdpWsTransportError(
363
+ "transport_error",
364
+ "websocket closed before open",
365
+ );
366
+ },
367
+ });
368
+ let caught: unknown;
369
+ try {
370
+ await harness.client.send("Browser.getVersion");
371
+ } catch (err) {
372
+ caught = err;
373
+ }
374
+ expect(caught).toBeInstanceOf(CdpError);
375
+ const cdpErr = caught as InstanceType<typeof CdpError>;
376
+ expect(cdpErr.code).toBe("transport_error");
377
+ expect(cdpErr.message).toBe("websocket closed before open");
378
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
379
+ });
380
+
381
+ test("send() with an already-aborted signal throws 'aborted' without touching the transport", async () => {
382
+ const harness = createHarness();
383
+ const controller = new AbortController();
384
+ controller.abort();
385
+ let caught: unknown;
386
+ try {
387
+ await harness.client.send(
388
+ "Browser.getVersion",
389
+ undefined,
390
+ controller.signal,
391
+ );
392
+ } catch (err) {
393
+ caught = err;
394
+ }
395
+ expect(caught).toBeInstanceOf(CdpError);
396
+ const cdpErr = caught as InstanceType<typeof CdpError>;
397
+ expect(cdpErr.code).toBe("aborted");
398
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
399
+ // Nothing ran — no discovery, no connect, no transport sends.
400
+ expect(harness.probeCalls).toBe(0);
401
+ expect(harness.listCalls).toBe(0);
402
+ expect(harness.connectCalls).toBe(0);
403
+ expect(harness.sends.length).toBe(0);
404
+ });
405
+
406
+ test("send() classifies as 'aborted' when the signal fires during attach", async () => {
407
+ const controller = new AbortController();
408
+ const harness = createHarness({
409
+ probeImpl: async () => {
410
+ // Simulate caller aborting while discovery is in flight.
411
+ // Discovery itself throws a generic error (as real fetch
412
+ // would), and the abort flag is flipped — we expect the
413
+ // resulting CdpError to carry code "aborted".
414
+ controller.abort();
415
+ throw new Error("aborted during fetch");
416
+ },
417
+ });
418
+ let caught: unknown;
419
+ try {
420
+ await harness.client.send(
421
+ "Browser.getVersion",
422
+ undefined,
423
+ controller.signal,
424
+ );
425
+ } catch (err) {
426
+ caught = err;
427
+ }
428
+ expect(caught).toBeInstanceOf(CdpError);
429
+ const cdpErr = caught as InstanceType<typeof CdpError>;
430
+ expect(cdpErr.code).toBe("aborted");
431
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
432
+ });
433
+
434
+ test("send() classifies as 'aborted' when the signal fires during the forwarded call", async () => {
435
+ const controller = new AbortController();
436
+ const harness = createHarness({
437
+ transportOnSend: async (method) => {
438
+ if (method === "Target.attachToTarget") {
439
+ return { sessionId: "session-abc" };
440
+ }
441
+ // Simulate the transport throwing an abort error after
442
+ // the caller aborts mid-call.
443
+ controller.abort();
444
+ throw new CdpWsTransportError("aborted", "aborted during send", {
445
+ cdpMethod: method,
446
+ });
447
+ },
448
+ });
449
+ let caught: unknown;
450
+ try {
451
+ await harness.client.send(
452
+ "Page.navigate",
453
+ { url: "about:blank" },
454
+ controller.signal,
455
+ );
456
+ } catch (err) {
457
+ caught = err;
458
+ }
459
+ expect(caught).toBeInstanceOf(CdpError);
460
+ const cdpErr = caught as InstanceType<typeof CdpError>;
461
+ expect(cdpErr.code).toBe("aborted");
462
+ expect(cdpErr.cdpMethod).toBe("Page.navigate");
463
+ });
464
+
465
+ test("send() maps forwarded CDP protocol errors to 'cdp_error'", async () => {
466
+ const harness = createHarness({
467
+ transportOnSend: async (method) => {
468
+ if (method === "Target.attachToTarget") {
469
+ return { sessionId: "session-abc" };
470
+ }
471
+ throw new CdpWsTransportError("cdp_error", "invalid expression", {
472
+ cdpMethod: method,
473
+ cdpCode: -32000,
474
+ cdpMessage: "invalid expression",
475
+ });
476
+ },
477
+ });
478
+ let caught: unknown;
479
+ try {
480
+ await harness.client.send("Runtime.evaluate", { expression: "??" });
481
+ } catch (err) {
482
+ caught = err;
483
+ }
484
+ expect(caught).toBeInstanceOf(CdpError);
485
+ const cdpErr = caught as InstanceType<typeof CdpError>;
486
+ expect(cdpErr.code).toBe("cdp_error");
487
+ expect(cdpErr.message).toBe("invalid expression");
488
+ expect(cdpErr.cdpMethod).toBe("Runtime.evaluate");
489
+ expect(cdpErr.cdpParams).toEqual({ expression: "??" });
490
+ });
491
+
492
+ test("dispose() is idempotent and tears down the underlying transport", async () => {
493
+ const harness = createHarness();
494
+ await harness.client.send("Browser.getVersion");
495
+ harness.client.dispose();
496
+ // dispose schedules transport.dispose on the resolved attach
497
+ // promise's then() — flush microtasks.
498
+ await new Promise((resolve) => setTimeout(resolve, 0));
499
+ expect(harness.disposeCount.count).toBe(1);
500
+
501
+ // Second dispose is a no-op.
502
+ harness.client.dispose();
503
+ await new Promise((resolve) => setTimeout(resolve, 0));
504
+ expect(harness.disposeCount.count).toBe(1);
505
+ });
506
+
507
+ test("dispose() without any sends does not call connectCdpWsTransport", async () => {
508
+ const harness = createHarness();
509
+ harness.client.dispose();
510
+ await new Promise((resolve) => setTimeout(resolve, 0));
511
+ expect(harness.connectCalls).toBe(0);
512
+ expect(harness.disposeCount.count).toBe(0);
513
+ });
514
+
515
+ test("send() after dispose throws CdpError with code 'disposed'", async () => {
516
+ const harness = createHarness();
517
+ harness.client.dispose();
518
+ let caught: unknown;
519
+ try {
520
+ await harness.client.send("Browser.getVersion");
521
+ } catch (err) {
522
+ caught = err;
523
+ }
524
+ expect(caught).toBeInstanceOf(CdpError);
525
+ const cdpErr = caught as InstanceType<typeof CdpError>;
526
+ expect(cdpErr.code).toBe("disposed");
527
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
528
+ // No discovery or transport activity took place.
529
+ expect(harness.probeCalls).toBe(0);
530
+ expect(harness.listCalls).toBe(0);
531
+ expect(harness.connectCalls).toBe(0);
532
+ });
533
+
534
+ test("attach that returns no sessionId throws 'cdp_error'", async () => {
535
+ const harness = createHarness({
536
+ transportOnSend: async (method) => {
537
+ if (method === "Target.attachToTarget") {
538
+ // Missing sessionId field — a broken fork response.
539
+ return {};
540
+ }
541
+ return undefined;
542
+ },
543
+ });
544
+ let caught: unknown;
545
+ try {
546
+ await harness.client.send("Browser.getVersion");
547
+ } catch (err) {
548
+ caught = err;
549
+ }
550
+ expect(caught).toBeInstanceOf(CdpError);
551
+ const cdpErr = caught as InstanceType<typeof CdpError>;
552
+ expect(cdpErr.code).toBe("cdp_error");
553
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
554
+ });
555
+
556
+ test("attach failure tears down the partially-opened transport", async () => {
557
+ const localDisposeCount = { count: 0 };
558
+ const transport = createFakeTransport({
559
+ onSend: async (method) => {
560
+ if (method === "Target.attachToTarget") {
561
+ throw new CdpWsTransportError("cdp_error", "attach failed", {
562
+ cdpMethod: method,
563
+ });
564
+ }
565
+ return undefined;
566
+ },
567
+ trackDisposeCount: localDisposeCount,
568
+ });
569
+ const client = createCdpInspectClient("conv-attach-fail", {
570
+ host: "127.0.0.1",
571
+ port: 9222,
572
+ helpers: {
573
+ probeDevToolsJsonVersion: async () => ({
574
+ browser: "Chrome/125.0.0.0",
575
+ protocolVersion: "1.3",
576
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
577
+ }),
578
+ listDevToolsTargets: async () => [
579
+ {
580
+ id: "target-1",
581
+ type: "page",
582
+ title: "Example",
583
+ url: "https://example.com/",
584
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/target-1",
585
+ },
586
+ ],
587
+ connectCdpWsTransport: async () => transport,
588
+ },
589
+ });
590
+
591
+ let caught: unknown;
592
+ try {
593
+ await client.send("Browser.getVersion");
594
+ } catch (err) {
595
+ caught = err;
596
+ }
597
+ expect(caught).toBeInstanceOf(CdpError);
598
+ // The transport opened by attach() should have been disposed so
599
+ // the socket doesn't leak.
600
+ expect(localDisposeCount.count).toBe(1);
601
+ });
602
+
603
+ test("send() aborts promptly when signal fires during ensureSession", async () => {
604
+ // Discovery is deliberately stalled so the caller has to rely on
605
+ // raceAbort() to cut through. If raceAbort worked correctly, the
606
+ // send() promise rejects with an 'aborted' CdpError the instant
607
+ // the controller fires — even though probeDevToolsJsonVersion is
608
+ // still hanging on an unresolved await.
609
+ const controller = new AbortController();
610
+ let probeSignalSeen: AbortSignal | undefined;
611
+ let probeResolve: (() => void) | undefined;
612
+ const probeStarted = new Promise<void>((resolve) => {
613
+ probeResolve = resolve;
614
+ });
615
+
616
+ const client = createCdpInspectClient("conv-abort-during-ensure", {
617
+ host: "127.0.0.1",
618
+ port: 9222,
619
+ helpers: {
620
+ probeDevToolsJsonVersion: async (probeOpts) => {
621
+ // Capture the signal so we can assert the shared attach
622
+ // controller actually fired downstream.
623
+ probeSignalSeen = (probeOpts as { signal?: AbortSignal }).signal;
624
+ probeResolve?.();
625
+ // Hang forever unless the shared controller fires.
626
+ await new Promise<never>((_, reject) => {
627
+ const onAbort = () => {
628
+ reject(new Error("probe aborted via shared controller"));
629
+ };
630
+ if (probeSignalSeen?.aborted) {
631
+ onAbort();
632
+ } else {
633
+ probeSignalSeen?.addEventListener("abort", onAbort, {
634
+ once: true,
635
+ });
636
+ }
637
+ });
638
+ throw new Error("unreachable");
639
+ },
640
+ listDevToolsTargets: async () => [
641
+ {
642
+ id: "target-1",
643
+ type: "page",
644
+ title: "Example",
645
+ url: "https://example.com/",
646
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/target-1",
647
+ },
648
+ ],
649
+ connectCdpWsTransport: async () =>
650
+ createFakeTransport({
651
+ onSend: async (method) => {
652
+ if (method === "Target.attachToTarget") {
653
+ return { sessionId: "fake-session-id" };
654
+ }
655
+ return undefined;
656
+ },
657
+ }),
658
+ },
659
+ });
660
+
661
+ const sendPromise = client.send(
662
+ "Browser.getVersion",
663
+ undefined,
664
+ controller.signal,
665
+ );
666
+ // Wait until probe is actually running, then abort.
667
+ await probeStarted;
668
+ controller.abort();
669
+
670
+ let caught: unknown;
671
+ try {
672
+ await sendPromise;
673
+ } catch (err) {
674
+ caught = err;
675
+ }
676
+ expect(caught).toBeInstanceOf(CdpError);
677
+ const cdpErr = caught as InstanceType<typeof CdpError>;
678
+ expect(cdpErr.code).toBe("aborted");
679
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
680
+ // Probe saw a non-null signal, meaning ensureSession plumbed one
681
+ // all the way down to the discovery helper.
682
+ expect(probeSignalSeen).toBeDefined();
683
+ // After the last (and only) waiter aborted, the shared controller
684
+ // should have been aborted — downstream probe's await should have
685
+ // been rejected.
686
+ expect(probeSignalSeen?.aborted).toBe(true);
687
+ });
688
+
689
+ test("a new send() after all waiters abort starts a fresh attach", async () => {
690
+ // Regression test for the race condition where onAbort aborts
691
+ // the shared controller but `this.pending` is only cleared later
692
+ // via the async `.catch()` handler in startAttach(). A new caller
693
+ // entering ensureSession() between those two events would reuse
694
+ // the already-aborted pending attach and immediately fail with an
695
+ // `aborted` error even though it never aborted its own signal.
696
+ //
697
+ // Fix: onAbort now clears `this.pending` synchronously BEFORE
698
+ // firing the shared controller.abort(), so any new caller after
699
+ // the abort starts a fresh attach.
700
+ let probeCount = 0;
701
+ let listCount = 0;
702
+ let connectCount = 0;
703
+
704
+ // The first probe hangs until the shared controller aborts it.
705
+ // The second probe (from the fresh attach) resolves normally.
706
+ const firstProbeStarted = Promise.withResolvers<void>();
707
+
708
+ const client = createCdpInspectClient("conv-race", {
709
+ host: "127.0.0.1",
710
+ port: 9222,
711
+ helpers: {
712
+ probeDevToolsJsonVersion: async (probeOpts) => {
713
+ probeCount += 1;
714
+ const signal = (probeOpts as { signal?: AbortSignal }).signal;
715
+ if (probeCount === 1) {
716
+ firstProbeStarted.resolve();
717
+ // Stall until the shared controller aborts us.
718
+ await new Promise<never>((_, reject) => {
719
+ const onAbort = () => {
720
+ reject(new Error("probe aborted via shared controller"));
721
+ };
722
+ if (signal?.aborted) {
723
+ onAbort();
724
+ } else {
725
+ signal?.addEventListener("abort", onAbort, { once: true });
726
+ }
727
+ });
728
+ throw new Error("unreachable");
729
+ }
730
+ return {
731
+ browser: "Chrome/125.0.0.0",
732
+ protocolVersion: "1.3",
733
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
734
+ };
735
+ },
736
+ listDevToolsTargets: async () => {
737
+ listCount += 1;
738
+ return [
739
+ {
740
+ id: "target-1",
741
+ type: "page",
742
+ title: "Example",
743
+ url: "https://example.com/",
744
+ webSocketDebuggerUrl:
745
+ "ws://127.0.0.1:9222/devtools/page/target-1",
746
+ },
747
+ ];
748
+ },
749
+ connectCdpWsTransport: async () => {
750
+ connectCount += 1;
751
+ return createFakeTransport({
752
+ onSend: async (method) => {
753
+ if (method === "Target.attachToTarget") {
754
+ return { sessionId: "session-race" };
755
+ }
756
+ return { ok: true };
757
+ },
758
+ });
759
+ },
760
+ },
761
+ });
762
+
763
+ // 1. First caller kicks off the attach with signal A.
764
+ const signalA = new AbortController();
765
+ const firstSend = client.send("Runtime.enable", undefined, signalA.signal);
766
+
767
+ // 2. Wait until the first probe has actually started so we know
768
+ // the attach is in-flight and signal A is the only waiter.
769
+ await firstProbeStarted.promise;
770
+
771
+ // 3. Abort signal A. Because it's the only waiter, onAbort will
772
+ // fire the shared controller and (with the fix) clear
773
+ // `this.pending` synchronously.
774
+ signalA.abort();
775
+
776
+ // First caller should reject with `aborted`.
777
+ let firstErr: unknown;
778
+ try {
779
+ await firstSend;
780
+ } catch (err) {
781
+ firstErr = err;
782
+ }
783
+ expect(firstErr).toBeInstanceOf(CdpError);
784
+ expect((firstErr as InstanceType<typeof CdpError>).code).toBe("aborted");
785
+
786
+ // At this point, with the fix, `this.pending` has been cleared
787
+ // synchronously — but without the fix, it would still be set to
788
+ // the aborted pending until the `.catch()` handler in
789
+ // startAttach() runs asynchronously. We intentionally do NOT
790
+ // flush microtasks here before kicking off the second send() so
791
+ // that we exercise the race window.
792
+ //
793
+ // 4. New send() with its own signal B. With the fix, this should
794
+ // start a fresh attach and complete successfully. Without the
795
+ // fix, it would reuse the aborted pending and fail with an
796
+ // `aborted` error even though signal B was never aborted.
797
+ const signalB = new AbortController();
798
+ const secondSend = client.send("Page.enable", undefined, signalB.signal);
799
+
800
+ // Second caller should succeed.
801
+ const secondResult = await secondSend;
802
+ expect(secondResult).toEqual({ ok: true });
803
+
804
+ // 5. Assert that the second send() kicked off a fresh attach —
805
+ // probe, list, and connect should all have been called twice.
806
+ expect(probeCount).toBe(2);
807
+ expect(listCount).toBe(1);
808
+ expect(connectCount).toBe(1);
809
+ // listCount and connectCount are 1 because the first attach
810
+ // aborted during the probe stage — it never reached list or
811
+ // connect. The second (fresh) attach ran probe + list + connect
812
+ // all once.
813
+ });
814
+
815
+ test("concurrent send() callers can abort independently", async () => {
816
+ // Two callers race the same in-flight attach. The first caller
817
+ // aborts; the second caller must still complete normally once the
818
+ // shared attach resolves.
819
+ const aborter = new AbortController();
820
+ let probeResolve: (() => void) | undefined;
821
+ let releaseProbe: (() => void) | undefined;
822
+ const probeRunning = new Promise<void>((resolve) => {
823
+ probeResolve = resolve;
824
+ });
825
+ const probeCanFinish = new Promise<void>((resolve) => {
826
+ releaseProbe = resolve;
827
+ });
828
+
829
+ const harness = createHarness({
830
+ probeImpl: async () => {
831
+ probeResolve?.();
832
+ await probeCanFinish;
833
+ return {
834
+ browser: "Chrome/125.0.0.0",
835
+ protocolVersion: "1.3",
836
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
837
+ };
838
+ },
839
+ });
840
+
841
+ const aborted = harness.client.send(
842
+ "Runtime.enable",
843
+ undefined,
844
+ aborter.signal,
845
+ );
846
+ const stable = harness.client.send("Page.enable");
847
+
848
+ await probeRunning;
849
+ aborter.abort();
850
+
851
+ // First caller aborts promptly…
852
+ let firstErr: unknown;
853
+ try {
854
+ await aborted;
855
+ } catch (err) {
856
+ firstErr = err;
857
+ }
858
+ expect(firstErr).toBeInstanceOf(CdpError);
859
+ expect((firstErr as InstanceType<typeof CdpError>).code).toBe("aborted");
860
+
861
+ // …but the second caller is still alive and can make progress
862
+ // once the shared attach finishes.
863
+ releaseProbe?.();
864
+ await stable;
865
+ expect(harness.probeCalls).toBe(1);
866
+ expect(harness.listCalls).toBe(1);
867
+ expect(harness.connectCalls).toBe(1);
868
+ expect(harness.attachCallCount()).toBe(1);
869
+ });
870
+ });