@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
@@ -9,11 +9,64 @@ mock.module("../util/logger.js", () => ({
9
9
  }),
10
10
  }));
11
11
 
12
- // Track calls to browserManager and url-safety helpers
12
+ // ── Fake CdpClient ───────────────────────────────────────────────────
13
+ //
14
+ // Programmable send handler + call log shared across tests. Each test
15
+ // resets these in `beforeEach` via `resetCdp()`. The mocked
16
+ // `getCdpClient` mirrors the real factory's routing decision (local
17
+ // vs extension is driven by `context.hostBrowserProxy`) so individual
18
+ // tests can exercise either transport without process-wide coupling.
19
+ //
20
+ // Note: bun's `mock.module` is process-global, but `scripts/test.sh`
21
+ // runs each test file in its own bun process so this mock only
22
+ // affects this file's tests.
23
+
24
+ let cdpSendCalls: Array<{ method: string; params?: unknown }> = [];
25
+ let cdpSendHandler: (
26
+ method: string,
27
+ params?: Record<string, unknown>,
28
+ ) => unknown = () => ({});
29
+ let cdpDisposed = false;
30
+
31
+ function makeFakeCdp(kind: "local" | "extension", conversationId: string) {
32
+ return {
33
+ kind,
34
+ conversationId,
35
+ async send<T>(
36
+ method: string,
37
+ params?: Record<string, unknown>,
38
+ ): Promise<T> {
39
+ cdpSendCalls.push({ method, params });
40
+ const value = cdpSendHandler(method, params);
41
+ return (await value) as T;
42
+ },
43
+ dispose() {
44
+ cdpDisposed = true;
45
+ },
46
+ };
47
+ }
48
+
49
+ mock.module("../tools/browser/cdp-client/factory.js", () => ({
50
+ getCdpClient: (context: {
51
+ hostBrowserProxy?: unknown;
52
+ conversationId: string;
53
+ }) =>
54
+ makeFakeCdp(
55
+ context.hostBrowserProxy ? "extension" : "local",
56
+ context.conversationId,
57
+ ),
58
+ }));
59
+
60
+ // ── Minimal browserManager stub ──────────────────────────────────────
61
+ //
62
+ // The local path still installs a Playwright route handler via
63
+ // browserManager.getOrCreateSessionPage() → page.route(...). We keep
64
+ // a tiny stub so the happy path doesn't blow up when the route handler
65
+ // is installed/uninstalled; the route logic itself is only exercised
66
+ // by the SSRF redirect test below.
67
+
13
68
  let mockPage: {
14
- goto: ReturnType<typeof mock>;
15
- title: ReturnType<typeof mock>;
16
- url: ReturnType<typeof mock>;
69
+ url: () => string;
17
70
  route: ReturnType<typeof mock>;
18
71
  unroute: ReturnType<typeof mock>;
19
72
  close: () => Promise<void>;
@@ -21,20 +74,31 @@ let mockPage: {
21
74
  };
22
75
 
23
76
  let getOrCreateSessionPageMock: ReturnType<typeof mock>;
77
+ let clearSnapshotBackendNodeMapMock: ReturnType<typeof mock>;
78
+ let positionWindowSidebarMock: ReturnType<typeof mock>;
24
79
 
25
80
  mock.module("../tools/browser/browser-manager.js", () => {
26
81
  getOrCreateSessionPageMock = mock(async () => mockPage);
82
+ clearSnapshotBackendNodeMapMock = mock(() => {});
83
+ positionWindowSidebarMock = mock(async () => {});
27
84
  return {
28
85
  browserManager: {
29
86
  getOrCreateSessionPage: getOrCreateSessionPageMock,
30
- clearSnapshotMap: mock(() => {}),
87
+ clearSnapshotBackendNodeMap: clearSnapshotBackendNodeMapMock,
31
88
  supportsRouteInterception: true,
32
89
  isInteractive: () => false,
33
- positionWindowSidebar: () => {},
90
+ positionWindowSidebar: positionWindowSidebarMock,
34
91
  },
35
92
  };
36
93
  });
37
94
 
95
+ mock.module("../tools/browser/browser-screencast.js", () => ({
96
+ ensureScreencast: async () => {},
97
+ getSender: () => null,
98
+ stopAllScreencasts: async () => {},
99
+ stopBrowserScreencast: async () => {},
100
+ }));
101
+
38
102
  // Default url-safety: allow everything
39
103
  let parseUrlResult: URL | null = null;
40
104
  let isPrivateResult = false;
@@ -59,12 +123,7 @@ const ctx: ToolContext = {
59
123
 
60
124
  function resetMockPage() {
61
125
  mockPage = {
62
- goto: mock(async () => ({
63
- status: () => 200,
64
- url: () => "https://example.com/",
65
- })),
66
- title: mock(async () => "Example"),
67
- url: mock(() => "https://example.com/"),
126
+ url: () => "https://example.com/",
68
127
  route: mock(async () => {}),
69
128
  unroute: mock(async () => {}),
70
129
  close: async () => {},
@@ -72,20 +131,87 @@ function resetMockPage() {
72
131
  };
73
132
  }
74
133
 
134
+ /**
135
+ * Default CDP handler. Returns values in the CDP response shape
136
+ * (`{ result: { value } }`) for `Runtime.evaluate` calls and resolves
137
+ * with `{}` for other methods.
138
+ *
139
+ * navigateAndWait now reads the pre-nav URL, then polls readyState +
140
+ * href in a single combined evaluate. The default flow:
141
+ *
142
+ * 1. Pre-nav `document.location.href` → "about:blank" (the baseline
143
+ * used by navigateAndWait's commit detection).
144
+ * 2. `Page.navigate`
145
+ * 3. Combined poll `({ readyState, href })` → `{ readyState:
146
+ * "complete", href: "https://example.com/page" }`. Because the
147
+ * href changed from the pre-nav value, commit detection fires
148
+ * on the first poll.
149
+ * 4. `document.title` → "Example".
150
+ */
151
+ function defaultCdpHandler(
152
+ method: string,
153
+ params?: Record<string, unknown>,
154
+ ): unknown {
155
+ if (method === "Page.navigate") return { frameId: "f1" };
156
+ if (method === "Runtime.evaluate") {
157
+ const expression = String(params?.["expression"] ?? "");
158
+ if (expression === "document.location.href") {
159
+ return { result: { value: "about:blank" } };
160
+ }
161
+ if (expression === "document.title") {
162
+ return { result: { value: "Example" } };
163
+ }
164
+ // Combined readyState + href polling expression from
165
+ // navigateAndWait. The commit-detection logic requires a
166
+ // different href from the pre-nav baseline so we return the
167
+ // requested page URL here.
168
+ if (
169
+ expression.includes("readyState") &&
170
+ expression.includes("document.location.href")
171
+ ) {
172
+ return {
173
+ result: {
174
+ value: {
175
+ readyState: "complete",
176
+ href: "https://example.com/page",
177
+ },
178
+ },
179
+ };
180
+ }
181
+ // DOM_DETECT / CAPTCHA_DETECT / DISMISS_MODALS IIFEs fall through
182
+ // to a generic "no challenge" result. The auth-detector IIFE
183
+ // expects `{result: {value: null | {...}}}` shape.
184
+ return { result: { value: null } };
185
+ }
186
+ return {};
187
+ }
188
+
189
+ function resetCdp() {
190
+ cdpSendCalls = [];
191
+ cdpDisposed = false;
192
+ cdpSendHandler = defaultCdpHandler;
193
+ }
194
+
75
195
  describe("executeBrowserNavigate", () => {
76
196
  beforeEach(() => {
77
197
  parseUrlResult = null;
78
198
  isPrivateResult = false;
79
199
  resolveResult = {};
80
200
  resetMockPage();
201
+ resetCdp();
81
202
  });
82
203
 
83
204
  // ── Input validation ───────────────────────────────────────────
205
+ //
206
+ // These run entirely within the upfront validation block and do
207
+ // not touch CDP. The tests intentionally do not assert anything
208
+ // about the CdpClient — the factory should never be called.
84
209
 
85
210
  test("rejects missing or invalid url", async () => {
86
211
  const result = await executeBrowserNavigate({}, ctx);
87
212
  expect(result.isError).toBe(true);
88
213
  expect(result.content).toContain("url is required");
214
+ expect(cdpSendCalls).toEqual([]);
89
215
  });
90
216
 
91
217
  test("rejects non-http(s) protocols", async () => {
@@ -96,6 +222,7 @@ describe("executeBrowserNavigate", () => {
96
222
  );
97
223
  expect(result.isError).toBe(true);
98
224
  expect(result.content).toContain("http or https");
225
+ expect(cdpSendCalls).toEqual([]);
99
226
  });
100
227
 
101
228
  // ── Private network blocking ───────────────────────────────────
@@ -110,6 +237,7 @@ describe("executeBrowserNavigate", () => {
110
237
  expect(result.isError).toBe(true);
111
238
  expect(result.content).toContain("Refusing to navigate");
112
239
  expect(result.content).toContain("localhost");
240
+ expect(cdpSendCalls).toEqual([]);
113
241
  });
114
242
 
115
243
  test("allows private hosts with allow_private_network=true", async () => {
@@ -120,7 +248,7 @@ describe("executeBrowserNavigate", () => {
120
248
  ctx,
121
249
  );
122
250
  expect(result.isError).toBe(false);
123
- expect(result.content).toContain("Status: 200");
251
+ expect(result.content).toContain("Status: unknown");
124
252
  });
125
253
 
126
254
  test("blocks DNS-resolved private addresses by default", async () => {
@@ -133,6 +261,7 @@ describe("executeBrowserNavigate", () => {
133
261
  );
134
262
  expect(result.isError).toBe(true);
135
263
  expect(result.content).toContain("10.0.0.1");
264
+ expect(cdpSendCalls).toEqual([]);
136
265
  });
137
266
 
138
267
  test("skips DNS check with allow_private_network=true", async () => {
@@ -144,12 +273,12 @@ describe("executeBrowserNavigate", () => {
144
273
  ctx,
145
274
  );
146
275
  expect(result.isError).toBe(false);
147
- expect(result.content).toContain("Status: 200");
276
+ expect(result.content).toContain("Status: unknown");
148
277
  });
149
278
 
150
- // ── Successful navigation ──────────────────────────────────────
279
+ // ── Happy path (CDP navigate) ──────────────────────────────────
151
280
 
152
- test("returns structured result on success", async () => {
281
+ test("calls Page.navigate with the requested URL and returns URL+title", async () => {
153
282
  parseUrlResult = new URL("https://example.com/page");
154
283
  const result = await executeBrowserNavigate(
155
284
  { url: "https://example.com/page" },
@@ -158,13 +287,64 @@ describe("executeBrowserNavigate", () => {
158
287
  expect(result.isError).toBe(false);
159
288
  expect(result.content).toContain("Requested URL:");
160
289
  expect(result.content).toContain("Final URL:");
161
- expect(result.content).toContain("Status: 200");
290
+ expect(result.content).toContain("Status: unknown");
162
291
  expect(result.content).toContain("Title: Example");
292
+
293
+ // Page.navigate was called with the expected URL
294
+ const navigateCall = cdpSendCalls.find((c) => c.method === "Page.navigate");
295
+ expect(navigateCall).toBeDefined();
296
+ expect(navigateCall!.params).toEqual({ url: "https://example.com/page" });
297
+
298
+ // navigateAndWait polls readyState+href in a single combined
299
+ // evaluate and also reads `document.location.href` pre-nav; the
300
+ // caller separately reads `document.title` after the nav.
301
+ const evaluateCalls = cdpSendCalls.filter(
302
+ (c) => c.method === "Runtime.evaluate",
303
+ );
304
+ const expressions = evaluateCalls.map(
305
+ (c) => (c.params as Record<string, unknown>)["expression"] as string,
306
+ );
307
+ expect(expressions.some((e) => e.includes("readyState"))).toBe(true);
308
+ expect(expressions).toContain("document.location.href");
309
+ expect(expressions).toContain("document.title");
310
+
311
+ // The CdpClient was disposed in the finally block.
312
+ expect(cdpDisposed).toBe(true);
163
313
  });
164
314
 
165
315
  test("notes redirect when final URL differs", async () => {
166
316
  parseUrlResult = new URL("https://example.com/old");
167
- mockPage.url = mock(() => "https://example.com/new");
317
+ // Pre-nav URL is about:blank so commit detection fires on the
318
+ // first poll. The combined poll returns a different href than
319
+ // the requested URL — that's what triggers the "redirected" note.
320
+ cdpSendHandler = (method, params) => {
321
+ if (method === "Page.navigate") return { frameId: "f1" };
322
+ if (method === "Runtime.evaluate") {
323
+ const expression = String(params?.["expression"] ?? "");
324
+ if (expression === "document.location.href") {
325
+ return { result: { value: "about:blank" } };
326
+ }
327
+ if (expression === "document.title") {
328
+ return { result: { value: "New" } };
329
+ }
330
+ if (
331
+ expression.includes("readyState") &&
332
+ expression.includes("document.location.href")
333
+ ) {
334
+ return {
335
+ result: {
336
+ value: {
337
+ readyState: "complete",
338
+ href: "https://example.com/new",
339
+ },
340
+ },
341
+ };
342
+ }
343
+ return { result: { value: null } };
344
+ }
345
+ return {};
346
+ };
347
+
168
348
  const result = await executeBrowserNavigate(
169
349
  { url: "https://example.com/old" },
170
350
  ctx,
@@ -173,24 +353,95 @@ describe("executeBrowserNavigate", () => {
173
353
  expect(result.content).toContain("redirected");
174
354
  });
175
355
 
176
- test("handles null response status", async () => {
177
- parseUrlResult = new URL("https://example.com");
178
- mockPage.goto = mock(async () => null);
356
+ // ── Timeout / readyState stays "loading" ───────────────────────
357
+
358
+ test("reports a timeout note when document.readyState never completes", async () => {
359
+ parseUrlResult = new URL("https://example.com/slow");
360
+ cdpSendHandler = (method, params) => {
361
+ if (method === "Page.navigate") return { frameId: "f1" };
362
+ if (method === "Runtime.evaluate") {
363
+ const expression = String(params?.["expression"] ?? "");
364
+ if (expression === "document.location.href") {
365
+ // Pre-nav URL read. Returning the same URL as the target
366
+ // would trigger the same-URL reload fallback; using
367
+ // about:blank keeps the cross-URL commit-detection path
368
+ // active so the polling loop actually exercises readyState.
369
+ return { result: { value: "about:blank" } };
370
+ }
371
+ if (expression === "document.title") {
372
+ return { result: { value: "Loading" } };
373
+ }
374
+ if (
375
+ expression.includes("readyState") &&
376
+ expression.includes("document.location.href")
377
+ ) {
378
+ // Stuck in "loading" — forces navigateAndWait to exhaust
379
+ // its timeout budget (or get aborted by the test's signal).
380
+ return {
381
+ result: {
382
+ value: {
383
+ readyState: "loading",
384
+ href: "https://example.com/slow",
385
+ },
386
+ },
387
+ };
388
+ }
389
+ return { result: { value: null } };
390
+ }
391
+ return {};
392
+ };
393
+
394
+ // Use a short deadline for the test — the NAVIGATE_TIMEOUT_MS
395
+ // const is 15s which is too slow for a unit test. We bound this
396
+ // by aborting after ~200ms so the helper surfaces a CdpError
397
+ // with code "aborted" rather than waiting the full 15s.
398
+ //
399
+ // The in-function `navigationTimedOut` branch is NOT the path
400
+ // exercised here (aborts throw instead of returning timedOut).
401
+ // The happy-path timeout is simulated by the other timeout
402
+ // behavior test below.
403
+ const ctrl = new AbortController();
404
+ const timer = setTimeout(() => ctrl.abort(), 200);
179
405
  const result = await executeBrowserNavigate(
180
- { url: "https://example.com" },
181
- ctx,
406
+ { url: "https://example.com/slow" },
407
+ { ...ctx, signal: ctrl.signal },
182
408
  );
183
- expect(result.isError).toBe(false);
184
- expect(result.content).toContain("Status: unknown");
409
+ clearTimeout(timer);
410
+ expect(result.isError).toBe(true);
411
+ expect(result.content).toContain("Navigation failed");
412
+ expect(cdpDisposed).toBe(true);
185
413
  });
186
414
 
187
- // ── Error handling ─────────────────────────────────────────────
415
+ // ── Pre-aborted signal ─────────────────────────────────────────
188
416
 
189
- test("catches navigation errors", async () => {
417
+ test("returns early-abort error when signal is already aborted", async () => {
418
+ parseUrlResult = new URL("https://example.com/page");
419
+ const ctrl = new AbortController();
420
+ ctrl.abort();
421
+ const result = await executeBrowserNavigate(
422
+ { url: "https://example.com/page" },
423
+ { ...ctx, signal: ctrl.signal },
424
+ );
425
+ expect(result.isError).toBe(true);
426
+ expect(result.content).toContain("operation was cancelled");
427
+ // Pre-abort short-circuits before any CDP call is made.
428
+ expect(cdpSendCalls).toEqual([]);
429
+ });
430
+
431
+ // ── Navigation errors ──────────────────────────────────────────
432
+
433
+ test("catches navigation errors from Page.navigate", async () => {
190
434
  parseUrlResult = new URL("https://example.com");
191
- mockPage.goto = mock(async () => {
192
- throw new Error("net::ERR_CONNECTION_REFUSED");
193
- });
435
+ cdpSendHandler = (method) => {
436
+ if (method === "Page.navigate") {
437
+ throw new Error("net::ERR_CONNECTION_REFUSED");
438
+ }
439
+ if (method === "Runtime.evaluate") {
440
+ return { result: { value: "https://example.com/" } };
441
+ }
442
+ return {};
443
+ };
444
+
194
445
  const result = await executeBrowserNavigate(
195
446
  { url: "https://example.com" },
196
447
  ctx,
@@ -198,13 +449,62 @@ describe("executeBrowserNavigate", () => {
198
449
  expect(result.isError).toBe(true);
199
450
  expect(result.content).toContain("Navigation failed");
200
451
  expect(result.content).toContain("ERR_CONNECTION_REFUSED");
452
+ expect(cdpDisposed).toBe(true);
201
453
  });
202
454
 
203
- test("returns security message when route handler blocks a redirect and goto throws", async () => {
455
+ test("surfaces Page.navigate errorText as a navigation failure", async () => {
456
+ // CDP signals DNS / connection errors via the response's
457
+ // `errorText` field rather than throwing. Without this, the
458
+ // navigate helper would poll readyState on the OLD page (which is
459
+ // "complete") and report success with the stale URL — leaking
460
+ // potentially sensitive content the agent never asked for.
461
+ parseUrlResult = new URL("https://nope.invalid");
462
+ cdpSendHandler = (method, params) => {
463
+ if (method === "Page.navigate") {
464
+ return { frameId: "f1", errorText: "net::ERR_NAME_NOT_RESOLVED" };
465
+ }
466
+ if (method === "Runtime.evaluate") {
467
+ const expression = String(params?.["expression"] ?? "");
468
+ if (expression === "document.location.href") {
469
+ return { result: { value: "https://example.com/old" } };
470
+ }
471
+ return { result: { value: null } };
472
+ }
473
+ return {};
474
+ };
475
+
476
+ const result = await executeBrowserNavigate(
477
+ { url: "https://nope.invalid" },
478
+ ctx,
479
+ );
480
+ expect(result.isError).toBe(true);
481
+ expect(result.content).toContain("Navigation failed");
482
+ expect(result.content).toContain("ERR_NAME_NOT_RESOLVED");
483
+ expect(cdpDisposed).toBe(true);
484
+
485
+ // Should NOT have polled readyState — navigate failed before the
486
+ // wait loop ran. navigateAndWait combines readyState + href into a
487
+ // single evaluate, so we look for any expression containing both.
488
+ const readyStateCalls = cdpSendCalls.filter((c) => {
489
+ if (c.method !== "Runtime.evaluate") return false;
490
+ const expr = (c.params as { expression?: string } | undefined)
491
+ ?.expression;
492
+ return (
493
+ typeof expr === "string" &&
494
+ expr.includes("readyState") &&
495
+ expr.includes("document.location.href")
496
+ );
497
+ });
498
+ expect(readyStateCalls).toHaveLength(0);
499
+ });
500
+
501
+ // ── SSRF route interception (local path only) ─────────────────
502
+
503
+ test("returns security message when route handler blocks a redirect", async () => {
204
504
  parseUrlResult = new URL("https://public.example.com");
205
505
  isPrivateResult = false;
206
506
 
207
- // Capture the route handler when page.route is called
507
+ // Capture the installed route handler.
208
508
  let capturedHandler:
209
509
  | ((route: unknown, request: unknown) => Promise<void>)
210
510
  | null = null;
@@ -217,21 +517,33 @@ describe("executeBrowserNavigate", () => {
217
517
  },
218
518
  );
219
519
 
220
- // Make goto invoke the captured handler with a private redirect target, then throw
221
- mockPage.goto = mock(async () => {
222
- if (capturedHandler) {
223
- // Temporarily make isPrivateOrLocalHost return true for the redirect target
224
- isPrivateResult = true;
225
- const mockRoute = {
226
- abort: mock(async () => {}),
227
- continue: mock(async () => {}),
228
- };
229
- const mockRequest = { url: () => "http://169.254.169.254/metadata" };
230
- await capturedHandler(mockRoute, mockRequest);
231
- isPrivateResult = false;
520
+ // When Page.navigate is called, simulate a private redirect by
521
+ // invoking the captured route handler, then throw to mirror how
522
+ // the Playwright route interceptor signals blockage to the caller.
523
+ cdpSendHandler = (method) => {
524
+ if (method === "Page.navigate") {
525
+ if (capturedHandler) {
526
+ const origPrivate = isPrivateResult;
527
+ isPrivateResult = true;
528
+ const mockRoute = {
529
+ abort: mock(async () => {}),
530
+ continue: mock(async () => {}),
531
+ };
532
+ const mockRequest = { url: () => "http://169.254.169.254/metadata" };
533
+ // Invoke the captured handler. Intentionally fire-and-forget
534
+ // because Page.navigate is synchronous from the test's
535
+ // perspective — the handler only mutates `blockedUrl` in the
536
+ // closed-over scope.
537
+ void capturedHandler(mockRoute, mockRequest);
538
+ isPrivateResult = origPrivate;
539
+ }
540
+ throw new Error("net::ERR_BLOCKED_BY_CLIENT");
232
541
  }
233
- throw new Error("net::ERR_BLOCKED_BY_CLIENT");
234
- });
542
+ if (method === "Runtime.evaluate") {
543
+ return { result: { value: "https://public.example.com/" } };
544
+ }
545
+ return {};
546
+ };
235
547
 
236
548
  const result = await executeBrowserNavigate(
237
549
  { url: "https://public.example.com" },
@@ -240,7 +552,37 @@ describe("executeBrowserNavigate", () => {
240
552
  expect(result.isError).toBe(true);
241
553
  expect(result.content).toContain("Navigation blocked");
242
554
  expect(result.content).toContain("allow_private_network=true");
243
- // Should NOT contain the raw Playwright error
555
+ // Should NOT contain the raw underlying error
244
556
  expect(result.content).not.toContain("ERR_BLOCKED_BY_CLIENT");
557
+ expect(cdpDisposed).toBe(true);
558
+ });
559
+
560
+ // ── Extension path (no browserManager / route interception) ───
561
+
562
+ test("extension path skips getOrCreateSessionPage and route interception", async () => {
563
+ parseUrlResult = new URL("https://example.com/page");
564
+ // Supplying a non-null hostBrowserProxy on the context routes the
565
+ // mocked getCdpClient to the extension path (it mirrors the real
566
+ // factory's routing logic).
567
+ const extensionCtx: ToolContext = {
568
+ ...ctx,
569
+ hostBrowserProxy: {} as unknown as ToolContext["hostBrowserProxy"],
570
+ };
571
+ // Reset page call trackers to verify they are not touched.
572
+ const routeCallsBefore = mockPage.route.mock.calls.length;
573
+ const unrouteCallsBefore = mockPage.unroute.mock.calls.length;
574
+
575
+ const result = await executeBrowserNavigate(
576
+ { url: "https://example.com/page" },
577
+ extensionCtx,
578
+ );
579
+
580
+ expect(result.isError).toBe(false);
581
+ // Extension path never installs or removes a Playwright route.
582
+ expect(mockPage.route.mock.calls.length).toBe(routeCallsBefore);
583
+ expect(mockPage.unroute.mock.calls.length).toBe(unrouteCallsBefore);
584
+ // Page.navigate still goes through the CdpClient.
585
+ expect(cdpSendCalls.some((c) => c.method === "Page.navigate")).toBe(true);
586
+ expect(cdpDisposed).toBe(true);
245
587
  });
246
588
  });