@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,578 @@
1
+ /**
2
+ * DevTools HTTP discovery helpers for the `cdp-inspect` backend.
3
+ *
4
+ * These helpers are pure HTTP/domain logic — they do not own a
5
+ * websocket transport, a session manager, or any CDP command state.
6
+ * They exist so the higher-level `cdp-inspect` client can:
7
+ *
8
+ * 1. Probe `/json/version` to verify that a loopback port is actually
9
+ * a Chrome/Chromium DevTools endpoint (and not some other service
10
+ * that happens to be listening).
11
+ * 2. Enumerate available page targets via `/json/list`.
12
+ * 3. Pick a sensible default target when the caller doesn't specify
13
+ * one explicitly.
14
+ *
15
+ * Safety boundary: **only loopback hosts are allowed**. A non-loopback
16
+ * host is rejected *before* any network I/O so that this module can
17
+ * never be coerced into making cross-origin requests on behalf of an
18
+ * attacker-controlled config value.
19
+ */
20
+
21
+ /**
22
+ * Stable error codes surfaced by discovery helpers.
23
+ *
24
+ * Callers branch on these codes instead of string-matching messages so
25
+ * upstream UX (status bar, toasts, logs) can render a stable, localized
26
+ * explanation.
27
+ */
28
+ export type DevToolsDiscoveryErrorCode =
29
+ | "unreachable"
30
+ | "non_loopback"
31
+ | "non_chrome"
32
+ | "invalid_response"
33
+ | "no_targets"
34
+ | "timeout";
35
+
36
+ /**
37
+ * Single error type thrown by all discovery helpers. Mirrors the
38
+ * shape of {@link import("../errors.js").CdpError} so catch sites can
39
+ * rely on an explicit `code` field and an optional underlying cause.
40
+ */
41
+ export class DevToolsDiscoveryError extends Error {
42
+ constructor(
43
+ public readonly code: DevToolsDiscoveryErrorCode,
44
+ message: string,
45
+ public readonly cause?: unknown,
46
+ ) {
47
+ super(message);
48
+ this.name = "DevToolsDiscoveryError";
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Normalized `/json/version` payload. Chrome returns canonical field
54
+ * names like `Browser` and `Protocol-Version`, but some forks and
55
+ * tests prefer camelCase. The parser here accepts both and normalizes
56
+ * to the camelCase shape below.
57
+ */
58
+ export interface DevToolsVersionInfo {
59
+ /** Normalized from `"Browser"` or `"browser"`. */
60
+ browser: string;
61
+ /** Normalized from `"Protocol-Version"` or `"protocolVersion"`. */
62
+ protocolVersion: string;
63
+ /** WebSocket URL for the browser-level debugger endpoint. */
64
+ webSocketDebuggerUrl: string;
65
+ }
66
+
67
+ /**
68
+ * A DevTools page target as returned by `/json/list`, filtered down to
69
+ * the fields the cdp-inspect backend actually needs.
70
+ */
71
+ export interface DevToolsTarget {
72
+ id: string;
73
+ type: string;
74
+ title: string;
75
+ url: string;
76
+ webSocketDebuggerUrl: string;
77
+ }
78
+
79
+ /**
80
+ * Loopback allowlist (exact match, case-insensitive). Any host not in
81
+ * this list is rejected *before* we touch the network.
82
+ *
83
+ * We intentionally do not resolve DNS here — if a config ever gains a
84
+ * hostname that happens to resolve to 127.0.0.1, we still refuse it,
85
+ * because DNS rebinding attacks can flip that answer between the
86
+ * pre-check and the actual fetch.
87
+ */
88
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
89
+
90
+ function assertLoopback(host: string): void {
91
+ const normalized = host.toLowerCase();
92
+ if (!LOOPBACK_HOSTS.has(normalized)) {
93
+ throw new DevToolsDiscoveryError(
94
+ "non_loopback",
95
+ `Refusing to probe non-loopback DevTools host "${host}". Only loopback hosts (localhost, 127.0.0.1, ::1) are permitted.`,
96
+ );
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Validate that a `webSocketDebuggerUrl` points to a loopback host.
102
+ *
103
+ * Chrome returns this URL in `/json/version` and `/json/list` responses.
104
+ * A rogue responder on the loopback port could return a ws:// URL
105
+ * pointing to an attacker-controlled host, tricking the client into
106
+ * opening a cross-origin WebSocket. This check ensures the hostname
107
+ * extracted from the URL is in the {@link LOOPBACK_HOSTS} allowlist.
108
+ */
109
+ function assertWsUrlLoopback(wsUrl: string): void {
110
+ let url: URL;
111
+ try {
112
+ url = new URL(wsUrl);
113
+ } catch {
114
+ throw new DevToolsDiscoveryError(
115
+ "invalid_response",
116
+ `webSocketDebuggerUrl is not a valid URL: ${wsUrl}`,
117
+ );
118
+ }
119
+
120
+ const hostname = url.hostname.toLowerCase();
121
+ if (!LOOPBACK_HOSTS.has(hostname)) {
122
+ throw new DevToolsDiscoveryError(
123
+ "non_loopback",
124
+ `webSocketDebuggerUrl host "${url.hostname}" is not loopback. A rogue responder may be redirecting the connection.`,
125
+ );
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Build a fetch-ready loopback URL. `host` is assumed to have already
131
+ * been validated by {@link assertLoopback}. IPv6 bare form (`::1`) is
132
+ * wrapped in square brackets for URL correctness.
133
+ */
134
+ function buildUrl(host: string, port: number, pathname: string): string {
135
+ const normalized = host.toLowerCase();
136
+ const hostSegment = normalized === "::1" ? "[::1]" : normalized;
137
+ return `http://${hostSegment}:${port}${pathname}`;
138
+ }
139
+
140
+ /**
141
+ * Timeout controller handle returned by {@link withTimeout}. `cleanup`
142
+ * must be called in a `finally` block to avoid leaking the timer and
143
+ * the abort listener. `timedOut` is flipped to `true` if the timer
144
+ * fires before `cleanup` runs.
145
+ */
146
+ interface TimeoutHandle {
147
+ signal: AbortSignal;
148
+ cleanup: () => void;
149
+ readonly timedOut: boolean;
150
+ }
151
+
152
+ /**
153
+ * Merge the caller's signal (if any) with a freshly-minted timeout
154
+ * controller. The flag on the returned handle is the single source of
155
+ * truth for "timed out vs. aborted vs. network error" — we can't
156
+ * recover that distinction from the fetch rejection alone.
157
+ */
158
+ function withTimeout(
159
+ timeoutMs: number,
160
+ callerSignal?: AbortSignal,
161
+ ): TimeoutHandle {
162
+ const controller = new AbortController();
163
+ let timedOut = false;
164
+ const timer = setTimeout(() => {
165
+ timedOut = true;
166
+ controller.abort(new Error("timeout"));
167
+ }, timeoutMs);
168
+
169
+ let onCallerAbort: (() => void) | null = null;
170
+ if (callerSignal) {
171
+ if (callerSignal.aborted) {
172
+ controller.abort(callerSignal.reason);
173
+ } else {
174
+ onCallerAbort = () => controller.abort(callerSignal.reason);
175
+ callerSignal.addEventListener("abort", onCallerAbort, { once: true });
176
+ }
177
+ }
178
+
179
+ return {
180
+ signal: controller.signal,
181
+ get timedOut() {
182
+ return timedOut;
183
+ },
184
+ cleanup: () => {
185
+ clearTimeout(timer);
186
+ if (onCallerAbort && callerSignal) {
187
+ callerSignal.removeEventListener("abort", onCallerAbort);
188
+ }
189
+ },
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Classify a `fetch()` rejection into a stable discovery code. We
195
+ * intentionally do not depend on Node's error code strings here — the
196
+ * fetch implementation varies between Bun, Node, and undici — so we
197
+ * look at the name, the message, and the caller-visible abort state
198
+ * instead.
199
+ */
200
+ function classifyFetchError(
201
+ err: unknown,
202
+ timedOut: boolean,
203
+ callerAborted: boolean,
204
+ ): DevToolsDiscoveryError {
205
+ if (callerAborted) {
206
+ return new DevToolsDiscoveryError(
207
+ "unreachable",
208
+ "Discovery fetch was aborted by the caller before completion.",
209
+ err,
210
+ );
211
+ }
212
+ if (timedOut) {
213
+ return new DevToolsDiscoveryError(
214
+ "timeout",
215
+ "Timed out waiting for DevTools HTTP response.",
216
+ err,
217
+ );
218
+ }
219
+
220
+ const message = err instanceof Error ? err.message : String(err);
221
+ const name = err instanceof Error ? err.name : "";
222
+ if (name === "AbortError" || /aborted/i.test(message)) {
223
+ // Unspecified abort — most likely the timeout fired but the flag
224
+ // was not flipped yet. Treat as timeout to give the user the
225
+ // clearer message.
226
+ return new DevToolsDiscoveryError(
227
+ "timeout",
228
+ "Timed out waiting for DevTools HTTP response.",
229
+ err,
230
+ );
231
+ }
232
+
233
+ return new DevToolsDiscoveryError(
234
+ "unreachable",
235
+ `Failed to reach DevTools endpoint: ${message}`,
236
+ err,
237
+ );
238
+ }
239
+
240
+ /**
241
+ * Read the response body as text, propagating abort/timeout errors so
242
+ * the caller can distinguish them from a merely malformed payload.
243
+ *
244
+ * The fetch signal is the same AbortSignal passed to the original
245
+ * `fetch()` call, so if the timeout fires (or the caller aborts) while
246
+ * we're still reading the body, the underlying stream is cancelled and
247
+ * `response.text()` rejects. We rethrow those abort-shaped failures as
248
+ * a distinct sentinel (`TimeoutDuringBodyReadError`) so the caller can
249
+ * map them to `timeout` / `unreachable` instead of `invalid_response`.
250
+ */
251
+ class TimeoutDuringBodyReadError extends Error {
252
+ constructor(
253
+ public readonly timedOut: boolean,
254
+ public readonly callerAborted: boolean,
255
+ public readonly underlying: unknown,
256
+ ) {
257
+ super("Discovery body read aborted.");
258
+ this.name = "TimeoutDuringBodyReadError";
259
+ }
260
+ }
261
+
262
+ function isAbortShaped(err: unknown): boolean {
263
+ if (!(err instanceof Error)) return false;
264
+ if (err.name === "AbortError") return true;
265
+ if (/abort/i.test(err.message)) return true;
266
+ return false;
267
+ }
268
+
269
+ async function readResponseText(
270
+ response: Response,
271
+ handle: TimeoutHandle,
272
+ callerSignal: AbortSignal | undefined,
273
+ ): Promise<string> {
274
+ try {
275
+ return await response.text();
276
+ } catch (err) {
277
+ const callerAborted = callerSignal?.aborted === true;
278
+ if (handle.timedOut || callerAborted || isAbortShaped(err)) {
279
+ throw new TimeoutDuringBodyReadError(handle.timedOut, callerAborted, err);
280
+ }
281
+ throw err;
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Best-effort JSON parser. Returns a parsed object or throws a
287
+ * discovery error with `invalid_response` so the caller doesn't need
288
+ * to wrap this itself. Assumes the body text has already been read
289
+ * (see {@link readResponseText}).
290
+ */
291
+ function parseJsonText(text: string, endpoint: string): unknown {
292
+ try {
293
+ return JSON.parse(text);
294
+ } catch (err) {
295
+ throw new DevToolsDiscoveryError(
296
+ "invalid_response",
297
+ `Expected JSON from ${endpoint} but got: ${text.slice(0, 200)}`,
298
+ err,
299
+ );
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Pull a string field out of an arbitrary JSON object, supporting
305
+ * either of two casings (e.g. `"Browser"` and `"browser"`). Returns
306
+ * `undefined` if neither key exists or the value is not a string.
307
+ */
308
+ function readStringField(
309
+ obj: Record<string, unknown>,
310
+ ...keys: string[]
311
+ ): string | undefined {
312
+ for (const key of keys) {
313
+ const value = obj[key];
314
+ if (typeof value === "string" && value.length > 0) {
315
+ return value;
316
+ }
317
+ }
318
+ return undefined;
319
+ }
320
+
321
+ /**
322
+ * Probe `/json/version` on a loopback DevTools endpoint and return a
323
+ * normalized {@link DevToolsVersionInfo}.
324
+ *
325
+ * Failure modes:
326
+ *
327
+ * - `non_loopback`: host is not one of {localhost, 127.0.0.1, ::1, [::1]}.
328
+ * Raised *before* any network I/O.
329
+ * - `unreachable`: network error, connection refused, DNS failure, etc.
330
+ * - `timeout`: no response within `timeoutMs`.
331
+ * - `invalid_response`: HTTP status != 200, non-JSON body, or missing
332
+ * required fields.
333
+ * - `non_chrome`: the responder does not identify itself as Chrome or
334
+ * Chromium. Guards against e.g. a dev server happening to listen on
335
+ * port 9222.
336
+ */
337
+ export async function probeDevToolsJsonVersion(opts: {
338
+ host: string;
339
+ port: number;
340
+ timeoutMs: number;
341
+ signal?: AbortSignal;
342
+ }): Promise<DevToolsVersionInfo> {
343
+ assertLoopback(opts.host);
344
+
345
+ const url = buildUrl(opts.host, opts.port, "/json/version");
346
+ const handle = withTimeout(opts.timeoutMs, opts.signal);
347
+
348
+ // Keep `handle` active until AFTER the body has been read, so the
349
+ // timeout (and caller abort) still enforce against a server that
350
+ // stalls mid-body. If we cleared the timer right after `fetch()`
351
+ // resolved, a chunked response that stalls on the second chunk would
352
+ // block `response.text()` indefinitely.
353
+ let text: string;
354
+ try {
355
+ let response: Response;
356
+ try {
357
+ response = await fetch(url, { signal: handle.signal });
358
+ } catch (err) {
359
+ throw classifyFetchError(
360
+ err,
361
+ handle.timedOut,
362
+ opts.signal?.aborted === true,
363
+ );
364
+ }
365
+
366
+ if (!response.ok) {
367
+ throw new DevToolsDiscoveryError(
368
+ "invalid_response",
369
+ `DevTools /json/version returned HTTP ${response.status}.`,
370
+ );
371
+ }
372
+
373
+ try {
374
+ text = await readResponseText(response, handle, opts.signal);
375
+ } catch (err) {
376
+ if (err instanceof TimeoutDuringBodyReadError) {
377
+ throw classifyFetchError(
378
+ err.underlying,
379
+ err.timedOut,
380
+ err.callerAborted,
381
+ );
382
+ }
383
+ throw new DevToolsDiscoveryError(
384
+ "invalid_response",
385
+ "Failed to read /json/version response body.",
386
+ err,
387
+ );
388
+ }
389
+ } finally {
390
+ handle.cleanup();
391
+ }
392
+
393
+ const parsed = parseJsonText(text, "/json/version");
394
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
395
+ throw new DevToolsDiscoveryError(
396
+ "invalid_response",
397
+ "DevTools /json/version payload is not a JSON object.",
398
+ );
399
+ }
400
+
401
+ const record = parsed as Record<string, unknown>;
402
+ const browser = readStringField(record, "Browser", "browser");
403
+ const protocolVersion = readStringField(
404
+ record,
405
+ "Protocol-Version",
406
+ "protocolVersion",
407
+ );
408
+ const webSocketDebuggerUrl = readStringField(
409
+ record,
410
+ "webSocketDebuggerUrl",
411
+ "WebSocketDebuggerUrl",
412
+ );
413
+
414
+ if (!browser || !protocolVersion || !webSocketDebuggerUrl) {
415
+ throw new DevToolsDiscoveryError(
416
+ "invalid_response",
417
+ "DevTools /json/version payload is missing required fields (browser, protocolVersion, webSocketDebuggerUrl).",
418
+ );
419
+ }
420
+
421
+ if (!/chrom(e|ium)/i.test(browser)) {
422
+ throw new DevToolsDiscoveryError(
423
+ "non_chrome",
424
+ `DevTools endpoint is not Chrome or Chromium: ${browser}`,
425
+ );
426
+ }
427
+
428
+ assertWsUrlLoopback(webSocketDebuggerUrl);
429
+
430
+ return { browser, protocolVersion, webSocketDebuggerUrl };
431
+ }
432
+
433
+ /**
434
+ * Enumerate `/json/list` and return only usable page targets — i.e.
435
+ * `type === "page"` with a non-empty `webSocketDebuggerUrl`. Throws
436
+ * `no_targets` when the filtered list is empty so the caller doesn't
437
+ * have to decide how to phrase that.
438
+ *
439
+ * Sibling failure modes match {@link probeDevToolsJsonVersion}:
440
+ * `non_loopback`, `unreachable`, `timeout`, `invalid_response`.
441
+ */
442
+ export async function listDevToolsTargets(opts: {
443
+ host: string;
444
+ port: number;
445
+ timeoutMs: number;
446
+ signal?: AbortSignal;
447
+ }): Promise<DevToolsTarget[]> {
448
+ assertLoopback(opts.host);
449
+
450
+ const url = buildUrl(opts.host, opts.port, "/json/list");
451
+ const handle = withTimeout(opts.timeoutMs, opts.signal);
452
+
453
+ // Keep `handle` active until AFTER the body has been read, so the
454
+ // timeout (and caller abort) still enforce against a server that
455
+ // stalls mid-body. See the matching comment in
456
+ // probeDevToolsJsonVersion for the exact failure mode this guards
457
+ // against.
458
+ let text: string;
459
+ try {
460
+ let response: Response;
461
+ try {
462
+ response = await fetch(url, { signal: handle.signal });
463
+ } catch (err) {
464
+ throw classifyFetchError(
465
+ err,
466
+ handle.timedOut,
467
+ opts.signal?.aborted === true,
468
+ );
469
+ }
470
+
471
+ if (!response.ok) {
472
+ throw new DevToolsDiscoveryError(
473
+ "invalid_response",
474
+ `DevTools /json/list returned HTTP ${response.status}.`,
475
+ );
476
+ }
477
+
478
+ try {
479
+ text = await readResponseText(response, handle, opts.signal);
480
+ } catch (err) {
481
+ if (err instanceof TimeoutDuringBodyReadError) {
482
+ throw classifyFetchError(
483
+ err.underlying,
484
+ err.timedOut,
485
+ err.callerAborted,
486
+ );
487
+ }
488
+ throw new DevToolsDiscoveryError(
489
+ "invalid_response",
490
+ "Failed to read /json/list response body.",
491
+ err,
492
+ );
493
+ }
494
+ } finally {
495
+ handle.cleanup();
496
+ }
497
+
498
+ const parsed = parseJsonText(text, "/json/list");
499
+ if (!Array.isArray(parsed)) {
500
+ throw new DevToolsDiscoveryError(
501
+ "invalid_response",
502
+ "DevTools /json/list payload is not a JSON array.",
503
+ );
504
+ }
505
+
506
+ const targets: DevToolsTarget[] = [];
507
+ for (const entry of parsed) {
508
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
509
+ continue;
510
+ }
511
+ const record = entry as Record<string, unknown>;
512
+ const type = readStringField(record, "type");
513
+ if (type !== "page") continue;
514
+
515
+ const webSocketDebuggerUrl = readStringField(
516
+ record,
517
+ "webSocketDebuggerUrl",
518
+ );
519
+ if (!webSocketDebuggerUrl) continue;
520
+
521
+ try {
522
+ assertWsUrlLoopback(webSocketDebuggerUrl);
523
+ } catch {
524
+ continue;
525
+ }
526
+
527
+ const id = readStringField(record, "id") ?? "";
528
+ const title = readStringField(record, "title") ?? "";
529
+ const targetUrl = readStringField(record, "url") ?? "";
530
+
531
+ targets.push({
532
+ id,
533
+ type,
534
+ title,
535
+ url: targetUrl,
536
+ webSocketDebuggerUrl,
537
+ });
538
+ }
539
+
540
+ if (targets.length === 0) {
541
+ throw new DevToolsDiscoveryError(
542
+ "no_targets",
543
+ "No usable page targets returned by DevTools /json/list.",
544
+ );
545
+ }
546
+
547
+ return targets;
548
+ }
549
+
550
+ /**
551
+ * Pick a sensible default target from a filtered list. Prefers targets
552
+ * whose URL is not `chrome://`, `devtools://`, or `about:blank`, then
553
+ * falls back to the first entry. Callers that need more specific
554
+ * control should iterate the list themselves.
555
+ *
556
+ * Throws `no_targets` on an empty input list — this mirrors the shape
557
+ * of {@link listDevToolsTargets}, so callers that chain the two can
558
+ * rely on a single error code path.
559
+ */
560
+ export function pickDefaultTarget(targets: DevToolsTarget[]): DevToolsTarget {
561
+ if (targets.length === 0) {
562
+ throw new DevToolsDiscoveryError(
563
+ "no_targets",
564
+ "pickDefaultTarget called with an empty target list.",
565
+ );
566
+ }
567
+
568
+ const preferred = targets.find((target) => !isUtilityTarget(target));
569
+ return preferred ?? targets[0]!;
570
+ }
571
+
572
+ function isUtilityTarget(target: DevToolsTarget): boolean {
573
+ const url = target.url.toLowerCase();
574
+ if (url.startsWith("chrome://")) return true;
575
+ if (url.startsWith("devtools://")) return true;
576
+ if (url === "about:blank") return true;
577
+ return false;
578
+ }