@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,743 @@
1
+ /**
2
+ * Unit tests for the DevTools HTTP discovery helpers.
3
+ *
4
+ * These tests boot a tiny `Bun.serve` instance per test (or per
5
+ * describe block) and point the helpers at it. The goal is to cover
6
+ * every error branch without relying on a real Chrome being present
7
+ * on the dev machine or CI runner.
8
+ */
9
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
10
+
11
+ import {
12
+ DevToolsDiscoveryError,
13
+ type DevToolsTarget,
14
+ listDevToolsTargets,
15
+ pickDefaultTarget,
16
+ probeDevToolsJsonVersion,
17
+ } from "../discovery.js";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Test fixture: a tiny Bun.serve that can be reconfigured per test.
21
+ // ---------------------------------------------------------------------------
22
+
23
+ type Handler = (req: Request) => Response | Promise<Response>;
24
+
25
+ interface FakeDevTools {
26
+ server: ReturnType<typeof Bun.serve>;
27
+ port: number;
28
+ setHandler: (handler: Handler) => void;
29
+ stop: () => void;
30
+ }
31
+
32
+ function startFakeDevTools(): FakeDevTools {
33
+ let handler: Handler = () => new Response("not configured", { status: 500 });
34
+ const server = Bun.serve({
35
+ port: 0,
36
+ hostname: "127.0.0.1",
37
+ async fetch(req) {
38
+ return handler(req);
39
+ },
40
+ });
41
+ return {
42
+ server,
43
+ port: server.port as number,
44
+ setHandler: (h) => {
45
+ handler = h;
46
+ },
47
+ stop: () => server.stop(true),
48
+ };
49
+ }
50
+
51
+ function chromeVersionResponse(
52
+ overrides: Record<string, string> = {},
53
+ ): Response {
54
+ return Response.json({
55
+ Browser: "Chrome/124.0.6367.91",
56
+ "Protocol-Version": "1.3",
57
+ "User-Agent": "Mozilla/5.0",
58
+ "V8-Version": "12.4.254.13",
59
+ "WebKit-Version": "537.36",
60
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/abcd-1234",
61
+ ...overrides,
62
+ });
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Loopback enforcement — must reject BEFORE any fetch call.
67
+ // ---------------------------------------------------------------------------
68
+
69
+ describe("probeDevToolsJsonVersion — loopback enforcement", () => {
70
+ test("rejects non-loopback host with non_loopback and does not fetch", async () => {
71
+ const originalFetch = globalThis.fetch;
72
+ let fetchCallCount = 0;
73
+ globalThis.fetch = (async (...args: unknown[]) => {
74
+ fetchCallCount += 1;
75
+ return originalFetch(...(args as Parameters<typeof fetch>));
76
+ }) as typeof fetch;
77
+
78
+ try {
79
+ await expect(
80
+ probeDevToolsJsonVersion({
81
+ host: "192.168.1.1",
82
+ port: 9222,
83
+ timeoutMs: 1000,
84
+ }),
85
+ ).rejects.toMatchObject({
86
+ name: "DevToolsDiscoveryError",
87
+ code: "non_loopback",
88
+ });
89
+ expect(fetchCallCount).toBe(0);
90
+ } finally {
91
+ globalThis.fetch = originalFetch;
92
+ }
93
+ });
94
+
95
+ test("rejects public DNS hostname before fetching", async () => {
96
+ const originalFetch = globalThis.fetch;
97
+ let fetchCallCount = 0;
98
+ globalThis.fetch = (async (...args: unknown[]) => {
99
+ fetchCallCount += 1;
100
+ return originalFetch(...(args as Parameters<typeof fetch>));
101
+ }) as typeof fetch;
102
+
103
+ try {
104
+ await expect(
105
+ probeDevToolsJsonVersion({
106
+ host: "example.com",
107
+ port: 9222,
108
+ timeoutMs: 1000,
109
+ }),
110
+ ).rejects.toMatchObject({
111
+ name: "DevToolsDiscoveryError",
112
+ code: "non_loopback",
113
+ });
114
+ expect(fetchCallCount).toBe(0);
115
+ } finally {
116
+ globalThis.fetch = originalFetch;
117
+ }
118
+ });
119
+
120
+ test("accepts localhost, 127.0.0.1, ::1 (case-insensitive)", async () => {
121
+ const fake = startFakeDevTools();
122
+ fake.setHandler(() => chromeVersionResponse());
123
+ try {
124
+ for (const host of ["localhost", "LOCALHOST", "127.0.0.1"]) {
125
+ const info = await probeDevToolsJsonVersion({
126
+ host,
127
+ port: fake.port,
128
+ timeoutMs: 2000,
129
+ });
130
+ expect(info.browser).toContain("Chrome");
131
+ }
132
+ } finally {
133
+ fake.stop();
134
+ }
135
+ });
136
+
137
+ test("listDevToolsTargets also rejects non-loopback before fetch", async () => {
138
+ const originalFetch = globalThis.fetch;
139
+ let fetchCallCount = 0;
140
+ globalThis.fetch = (async (...args: unknown[]) => {
141
+ fetchCallCount += 1;
142
+ return originalFetch(...(args as Parameters<typeof fetch>));
143
+ }) as typeof fetch;
144
+
145
+ try {
146
+ await expect(
147
+ listDevToolsTargets({
148
+ host: "10.0.0.1",
149
+ port: 9222,
150
+ timeoutMs: 1000,
151
+ }),
152
+ ).rejects.toMatchObject({
153
+ name: "DevToolsDiscoveryError",
154
+ code: "non_loopback",
155
+ });
156
+ expect(fetchCallCount).toBe(0);
157
+ } finally {
158
+ globalThis.fetch = originalFetch;
159
+ }
160
+ });
161
+ });
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // probeDevToolsJsonVersion — happy paths.
165
+ // ---------------------------------------------------------------------------
166
+
167
+ describe("probeDevToolsJsonVersion — parsing", () => {
168
+ let fake: FakeDevTools;
169
+
170
+ beforeEach(() => {
171
+ fake = startFakeDevTools();
172
+ });
173
+
174
+ afterEach(() => {
175
+ fake.stop();
176
+ });
177
+
178
+ test("parses real Chrome field casing", async () => {
179
+ fake.setHandler(() =>
180
+ chromeVersionResponse({
181
+ Browser: "Chrome/126.0.6478.127",
182
+ "Protocol-Version": "1.3",
183
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/xyz",
184
+ }),
185
+ );
186
+
187
+ const info = await probeDevToolsJsonVersion({
188
+ host: "127.0.0.1",
189
+ port: fake.port,
190
+ timeoutMs: 2000,
191
+ });
192
+
193
+ expect(info.browser).toBe("Chrome/126.0.6478.127");
194
+ expect(info.protocolVersion).toBe("1.3");
195
+ expect(info.webSocketDebuggerUrl).toBe(
196
+ "ws://127.0.0.1:9222/devtools/browser/xyz",
197
+ );
198
+ });
199
+
200
+ test("parses normalized camelCase field casing", async () => {
201
+ fake.setHandler(() =>
202
+ Response.json({
203
+ browser: "Chromium/125.0.6422.141",
204
+ protocolVersion: "1.3",
205
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/normalized",
206
+ }),
207
+ );
208
+
209
+ const info = await probeDevToolsJsonVersion({
210
+ host: "127.0.0.1",
211
+ port: fake.port,
212
+ timeoutMs: 2000,
213
+ });
214
+
215
+ expect(info.browser).toBe("Chromium/125.0.6422.141");
216
+ expect(info.protocolVersion).toBe("1.3");
217
+ expect(info.webSocketDebuggerUrl).toBe(
218
+ "ws://127.0.0.1:9222/devtools/browser/normalized",
219
+ );
220
+ });
221
+
222
+ test("rejects non-Chrome responder with non_chrome", async () => {
223
+ fake.setHandler(() => chromeVersionResponse({ Browser: "Firefox/115.0" }));
224
+
225
+ const error = await probeDevToolsJsonVersion({
226
+ host: "127.0.0.1",
227
+ port: fake.port,
228
+ timeoutMs: 2000,
229
+ }).catch((e: unknown) => e);
230
+
231
+ expect(error).toBeInstanceOf(DevToolsDiscoveryError);
232
+ expect((error as DevToolsDiscoveryError).code).toBe("non_chrome");
233
+ });
234
+
235
+ test("rejects missing required fields with invalid_response", async () => {
236
+ fake.setHandler(() =>
237
+ Response.json({
238
+ Browser: "Chrome/123",
239
+ // Missing Protocol-Version and webSocketDebuggerUrl
240
+ }),
241
+ );
242
+
243
+ const error = await probeDevToolsJsonVersion({
244
+ host: "127.0.0.1",
245
+ port: fake.port,
246
+ timeoutMs: 2000,
247
+ }).catch((e: unknown) => e);
248
+
249
+ expect(error).toBeInstanceOf(DevToolsDiscoveryError);
250
+ expect((error as DevToolsDiscoveryError).code).toBe("invalid_response");
251
+ });
252
+
253
+ test("rejects malformed JSON body with invalid_response", async () => {
254
+ fake.setHandler(
255
+ () =>
256
+ new Response("not json at all {{{", {
257
+ status: 200,
258
+ headers: { "content-type": "application/json" },
259
+ }),
260
+ );
261
+
262
+ const error = await probeDevToolsJsonVersion({
263
+ host: "127.0.0.1",
264
+ port: fake.port,
265
+ timeoutMs: 2000,
266
+ }).catch((e: unknown) => e);
267
+
268
+ expect(error).toBeInstanceOf(DevToolsDiscoveryError);
269
+ expect((error as DevToolsDiscoveryError).code).toBe("invalid_response");
270
+ });
271
+
272
+ test("rejects non-object JSON body with invalid_response", async () => {
273
+ fake.setHandler(() => Response.json(["not", "an", "object"]));
274
+
275
+ const error = await probeDevToolsJsonVersion({
276
+ host: "127.0.0.1",
277
+ port: fake.port,
278
+ timeoutMs: 2000,
279
+ }).catch((e: unknown) => e);
280
+
281
+ expect(error).toBeInstanceOf(DevToolsDiscoveryError);
282
+ expect((error as DevToolsDiscoveryError).code).toBe("invalid_response");
283
+ });
284
+
285
+ test("rejects non-200 status with invalid_response", async () => {
286
+ fake.setHandler(() => new Response("nope", { status: 404 }));
287
+
288
+ const error = await probeDevToolsJsonVersion({
289
+ host: "127.0.0.1",
290
+ port: fake.port,
291
+ timeoutMs: 2000,
292
+ }).catch((e: unknown) => e);
293
+
294
+ expect(error).toBeInstanceOf(DevToolsDiscoveryError);
295
+ expect((error as DevToolsDiscoveryError).code).toBe("invalid_response");
296
+ });
297
+ });
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // probeDevToolsJsonVersion — webSocketDebuggerUrl loopback validation.
301
+ // ---------------------------------------------------------------------------
302
+
303
+ describe("probeDevToolsJsonVersion — webSocketDebuggerUrl loopback", () => {
304
+ let fake: FakeDevTools;
305
+
306
+ beforeEach(() => {
307
+ fake = startFakeDevTools();
308
+ });
309
+
310
+ afterEach(() => {
311
+ fake.stop();
312
+ });
313
+
314
+ test("rejects when webSocketDebuggerUrl host is not loopback", async () => {
315
+ fake.setHandler(() =>
316
+ chromeVersionResponse({
317
+ webSocketDebuggerUrl: "ws://evil.com/devtools/browser/abc",
318
+ }),
319
+ );
320
+
321
+ const error = await probeDevToolsJsonVersion({
322
+ host: "127.0.0.1",
323
+ port: fake.port,
324
+ timeoutMs: 2000,
325
+ }).catch((e: unknown) => e);
326
+
327
+ expect(error).toBeInstanceOf(DevToolsDiscoveryError);
328
+ expect((error as DevToolsDiscoveryError).code).toBe("non_loopback");
329
+ expect((error as DevToolsDiscoveryError).message).toContain("evil.com");
330
+ });
331
+
332
+ test("accepts loopback webSocketDebuggerUrl", async () => {
333
+ fake.setHandler(() =>
334
+ chromeVersionResponse({
335
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/abc",
336
+ }),
337
+ );
338
+
339
+ const info = await probeDevToolsJsonVersion({
340
+ host: "127.0.0.1",
341
+ port: fake.port,
342
+ timeoutMs: 2000,
343
+ });
344
+
345
+ expect(info.browser).toContain("Chrome");
346
+ expect(info.webSocketDebuggerUrl).toBe(
347
+ "ws://127.0.0.1:9222/devtools/browser/abc",
348
+ );
349
+ });
350
+ });
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // probeDevToolsJsonVersion — network-level error paths.
354
+ // ---------------------------------------------------------------------------
355
+
356
+ describe("probeDevToolsJsonVersion — network errors", () => {
357
+ test("connection refused (unreachable)", async () => {
358
+ // Boot a server then stop it to get a guaranteed-free port.
359
+ const fake = startFakeDevTools();
360
+ const deadPort = fake.port;
361
+ fake.stop();
362
+
363
+ const error = await probeDevToolsJsonVersion({
364
+ host: "127.0.0.1",
365
+ port: deadPort,
366
+ timeoutMs: 2000,
367
+ }).catch((e: unknown) => e);
368
+
369
+ expect(error).toBeInstanceOf(DevToolsDiscoveryError);
370
+ expect((error as DevToolsDiscoveryError).code).toBe("unreachable");
371
+ });
372
+
373
+ test("stalled server triggers timeout", async () => {
374
+ const fake = startFakeDevTools();
375
+ fake.setHandler(
376
+ () =>
377
+ new Promise<Response>(() => {
378
+ // Intentionally never resolve.
379
+ }),
380
+ );
381
+
382
+ try {
383
+ const error = await probeDevToolsJsonVersion({
384
+ host: "127.0.0.1",
385
+ port: fake.port,
386
+ timeoutMs: 50,
387
+ }).catch((e: unknown) => e);
388
+
389
+ expect(error).toBeInstanceOf(DevToolsDiscoveryError);
390
+ expect((error as DevToolsDiscoveryError).code).toBe("timeout");
391
+ } finally {
392
+ fake.stop();
393
+ }
394
+ });
395
+
396
+ test("stalled response body triggers timeout (not invalid_response)", async () => {
397
+ // Regression test: a responder that sends headers + a partial body
398
+ // and then stalls the stream must still be cancelled by the
399
+ // discovery timeout. If the timer was cleared right after `fetch()`
400
+ // resolved, `response.text()` would hang forever. See review on
401
+ // PR #24601.
402
+ const fake = startFakeDevTools();
403
+ const stalledStreams: ReadableStreamDefaultController<Uint8Array>[] = [];
404
+ fake.setHandler(
405
+ () =>
406
+ new Response(
407
+ new ReadableStream<Uint8Array>({
408
+ start(controller) {
409
+ // Send a partial JSON body so `fetch()` can resolve its
410
+ // headers promise, then never enqueue more. Keep a ref so
411
+ // the test can close the stream during cleanup.
412
+ controller.enqueue(new TextEncoder().encode('{"'));
413
+ stalledStreams.push(controller);
414
+ // Intentionally do NOT call controller.close().
415
+ },
416
+ }),
417
+ {
418
+ status: 200,
419
+ headers: {
420
+ "content-type": "application/json",
421
+ // No content-length so the reader keeps waiting for more.
422
+ "transfer-encoding": "chunked",
423
+ },
424
+ },
425
+ ),
426
+ );
427
+
428
+ try {
429
+ const startedAt = Date.now();
430
+ const error = await probeDevToolsJsonVersion({
431
+ host: "127.0.0.1",
432
+ port: fake.port,
433
+ timeoutMs: 100,
434
+ }).catch((e: unknown) => e);
435
+ const elapsed = Date.now() - startedAt;
436
+
437
+ expect(error).toBeInstanceOf(DevToolsDiscoveryError);
438
+ expect((error as DevToolsDiscoveryError).code).toBe("timeout");
439
+ // Should resolve roughly at `timeoutMs`, not hang indefinitely.
440
+ // Generous upper bound to keep the test stable under load.
441
+ expect(elapsed).toBeLessThan(2000);
442
+ } finally {
443
+ // Unblock the server's streams so Bun.serve can shut down cleanly.
444
+ for (const controller of stalledStreams) {
445
+ try {
446
+ controller.close();
447
+ } catch {
448
+ // Stream may already be in an errored state from the abort.
449
+ }
450
+ }
451
+ fake.stop();
452
+ }
453
+ });
454
+
455
+ test("stalled response body during listDevToolsTargets triggers timeout", async () => {
456
+ // Same regression, checked on the /json/list path.
457
+ const fake = startFakeDevTools();
458
+ const stalledStreams: ReadableStreamDefaultController<Uint8Array>[] = [];
459
+ fake.setHandler(
460
+ () =>
461
+ new Response(
462
+ new ReadableStream<Uint8Array>({
463
+ start(controller) {
464
+ controller.enqueue(new TextEncoder().encode("["));
465
+ stalledStreams.push(controller);
466
+ },
467
+ }),
468
+ {
469
+ status: 200,
470
+ headers: {
471
+ "content-type": "application/json",
472
+ "transfer-encoding": "chunked",
473
+ },
474
+ },
475
+ ),
476
+ );
477
+
478
+ try {
479
+ const startedAt = Date.now();
480
+ const error = await listDevToolsTargets({
481
+ host: "127.0.0.1",
482
+ port: fake.port,
483
+ timeoutMs: 100,
484
+ }).catch((e: unknown) => e);
485
+ const elapsed = Date.now() - startedAt;
486
+
487
+ expect(error).toBeInstanceOf(DevToolsDiscoveryError);
488
+ expect((error as DevToolsDiscoveryError).code).toBe("timeout");
489
+ expect(elapsed).toBeLessThan(2000);
490
+ } finally {
491
+ for (const controller of stalledStreams) {
492
+ try {
493
+ controller.close();
494
+ } catch {
495
+ // Stream may already be in an errored state from the abort.
496
+ }
497
+ }
498
+ fake.stop();
499
+ }
500
+ });
501
+ });
502
+
503
+ // ---------------------------------------------------------------------------
504
+ // listDevToolsTargets — filtering and parsing.
505
+ // ---------------------------------------------------------------------------
506
+
507
+ describe("listDevToolsTargets", () => {
508
+ let fake: FakeDevTools;
509
+
510
+ beforeEach(() => {
511
+ fake = startFakeDevTools();
512
+ });
513
+
514
+ afterEach(() => {
515
+ fake.stop();
516
+ });
517
+
518
+ test("filters non-page targets and returns parsed pages", async () => {
519
+ fake.setHandler(() =>
520
+ Response.json([
521
+ {
522
+ id: "A",
523
+ type: "page",
524
+ title: "Example",
525
+ url: "https://example.com/",
526
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/A",
527
+ },
528
+ {
529
+ id: "B",
530
+ type: "service_worker",
531
+ title: "sw",
532
+ url: "https://example.com/sw.js",
533
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/B",
534
+ },
535
+ {
536
+ id: "C",
537
+ type: "iframe",
538
+ title: "frame",
539
+ url: "https://example.com/frame",
540
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/C",
541
+ },
542
+ {
543
+ id: "D",
544
+ type: "page",
545
+ title: "Second Page",
546
+ url: "https://docs.example.com/",
547
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/D",
548
+ },
549
+ ]),
550
+ );
551
+
552
+ const targets = await listDevToolsTargets({
553
+ host: "127.0.0.1",
554
+ port: fake.port,
555
+ timeoutMs: 2000,
556
+ });
557
+
558
+ expect(targets).toHaveLength(2);
559
+ expect(targets.map((t) => t.id)).toEqual(["A", "D"]);
560
+ expect(targets[0]!.webSocketDebuggerUrl).toBe(
561
+ "ws://127.0.0.1:9222/devtools/page/A",
562
+ );
563
+ });
564
+
565
+ test("drops page targets without webSocketDebuggerUrl", async () => {
566
+ fake.setHandler(() =>
567
+ Response.json([
568
+ {
569
+ id: "A",
570
+ type: "page",
571
+ title: "Missing WS",
572
+ url: "https://example.com/",
573
+ webSocketDebuggerUrl: "",
574
+ },
575
+ {
576
+ id: "B",
577
+ type: "page",
578
+ title: "Good Page",
579
+ url: "https://example.com/ok",
580
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/B",
581
+ },
582
+ ]),
583
+ );
584
+
585
+ const targets = await listDevToolsTargets({
586
+ host: "127.0.0.1",
587
+ port: fake.port,
588
+ timeoutMs: 2000,
589
+ });
590
+
591
+ expect(targets).toHaveLength(1);
592
+ expect(targets[0]!.id).toBe("B");
593
+ });
594
+
595
+ test("throws no_targets when filtered list is empty", async () => {
596
+ fake.setHandler(() =>
597
+ Response.json([
598
+ {
599
+ id: "A",
600
+ type: "service_worker",
601
+ title: "sw",
602
+ url: "https://example.com/sw.js",
603
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/A",
604
+ },
605
+ ]),
606
+ );
607
+
608
+ const error = await listDevToolsTargets({
609
+ host: "127.0.0.1",
610
+ port: fake.port,
611
+ timeoutMs: 2000,
612
+ }).catch((e: unknown) => e);
613
+
614
+ expect(error).toBeInstanceOf(DevToolsDiscoveryError);
615
+ expect((error as DevToolsDiscoveryError).code).toBe("no_targets");
616
+ });
617
+
618
+ test("throws invalid_response when body is not a JSON array", async () => {
619
+ fake.setHandler(() => Response.json({ not: "an array" }));
620
+
621
+ const error = await listDevToolsTargets({
622
+ host: "127.0.0.1",
623
+ port: fake.port,
624
+ timeoutMs: 2000,
625
+ }).catch((e: unknown) => e);
626
+
627
+ expect(error).toBeInstanceOf(DevToolsDiscoveryError);
628
+ expect((error as DevToolsDiscoveryError).code).toBe("invalid_response");
629
+ });
630
+
631
+ test("filters out targets with non-loopback webSocketDebuggerUrl", async () => {
632
+ fake.setHandler(() =>
633
+ Response.json([
634
+ {
635
+ id: "evil",
636
+ type: "page",
637
+ title: "Evil Target",
638
+ url: "https://example.com/evil",
639
+ webSocketDebuggerUrl: "ws://evil.com/devtools/page/evil",
640
+ },
641
+ {
642
+ id: "good",
643
+ type: "page",
644
+ title: "Good Target",
645
+ url: "https://example.com/good",
646
+ webSocketDebuggerUrl: "ws://localhost:9222/devtools/page/good",
647
+ },
648
+ ]),
649
+ );
650
+
651
+ const targets = await listDevToolsTargets({
652
+ host: "127.0.0.1",
653
+ port: fake.port,
654
+ timeoutMs: 2000,
655
+ });
656
+
657
+ expect(targets).toHaveLength(1);
658
+ expect(targets[0]!.id).toBe("good");
659
+ });
660
+
661
+ test("throws no_targets when all targets have non-loopback webSocketDebuggerUrl", async () => {
662
+ fake.setHandler(() =>
663
+ Response.json([
664
+ {
665
+ id: "evil",
666
+ type: "page",
667
+ title: "Evil Target",
668
+ url: "https://example.com/evil",
669
+ webSocketDebuggerUrl: "ws://evil.com/devtools/page/evil",
670
+ },
671
+ ]),
672
+ );
673
+
674
+ const error = await listDevToolsTargets({
675
+ host: "127.0.0.1",
676
+ port: fake.port,
677
+ timeoutMs: 2000,
678
+ }).catch((e: unknown) => e);
679
+
680
+ expect(error).toBeInstanceOf(DevToolsDiscoveryError);
681
+ expect((error as DevToolsDiscoveryError).code).toBe("no_targets");
682
+ });
683
+ });
684
+
685
+ // ---------------------------------------------------------------------------
686
+ // pickDefaultTarget — prefers real pages, falls back to first.
687
+ // ---------------------------------------------------------------------------
688
+
689
+ describe("pickDefaultTarget", () => {
690
+ function makeTarget(partial: Partial<DevToolsTarget>): DevToolsTarget {
691
+ return {
692
+ id: partial.id ?? "id",
693
+ type: partial.type ?? "page",
694
+ title: partial.title ?? "title",
695
+ url: partial.url ?? "https://example.com/",
696
+ webSocketDebuggerUrl:
697
+ partial.webSocketDebuggerUrl ?? "ws://127.0.0.1:9222/devtools/page/id",
698
+ };
699
+ }
700
+
701
+ test("throws no_targets on empty input", () => {
702
+ expect(() => pickDefaultTarget([])).toThrow(DevToolsDiscoveryError);
703
+ try {
704
+ pickDefaultTarget([]);
705
+ } catch (e) {
706
+ expect((e as DevToolsDiscoveryError).code).toBe("no_targets");
707
+ }
708
+ });
709
+
710
+ test("prefers a real https page over chrome:// targets", () => {
711
+ const targets: DevToolsTarget[] = [
712
+ makeTarget({ id: "newtab", url: "chrome://newtab/" }),
713
+ makeTarget({ id: "devtools", url: "devtools://devtools/bundled/idx" }),
714
+ makeTarget({ id: "site", url: "https://example.com/docs" }),
715
+ ];
716
+ const picked = pickDefaultTarget(targets);
717
+ expect(picked.id).toBe("site");
718
+ });
719
+
720
+ test("prefers a real page over about:blank", () => {
721
+ const targets: DevToolsTarget[] = [
722
+ makeTarget({ id: "blank", url: "about:blank" }),
723
+ makeTarget({ id: "real", url: "https://example.com/" }),
724
+ ];
725
+ const picked = pickDefaultTarget(targets);
726
+ expect(picked.id).toBe("real");
727
+ });
728
+
729
+ test("falls back to first when every target is a utility page", () => {
730
+ const targets: DevToolsTarget[] = [
731
+ makeTarget({ id: "newtab", url: "chrome://newtab/" }),
732
+ makeTarget({ id: "devtools", url: "devtools://devtools/bundled/idx" }),
733
+ makeTarget({ id: "blank", url: "about:blank" }),
734
+ ];
735
+ const picked = pickDefaultTarget(targets);
736
+ expect(picked.id).toBe("newtab");
737
+ });
738
+
739
+ test("returns the only candidate when the list has length 1", () => {
740
+ const targets = [makeTarget({ id: "only", url: "https://example.com/" })];
741
+ expect(pickDefaultTarget(targets).id).toBe("only");
742
+ });
743
+ });