@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,37 +9,74 @@ mock.module("../util/logger.js", () => ({
9
9
  }),
10
10
  }));
11
11
 
12
- let mockPage: {
13
- evaluate: ReturnType<typeof mock>;
14
- title: ReturnType<typeof mock>;
15
- url: ReturnType<typeof mock>;
16
- goto: ReturnType<typeof mock>;
17
- close: ReturnType<typeof mock>;
18
- isClosed: () => boolean;
19
- };
12
+ /**
13
+ * Shared fake CDP session state. Tests install a custom `cdpSend`
14
+ * implementation in their setup, then assert against `cdpCalls` and
15
+ * `detachCalls` after the tool runs.
16
+ *
17
+ * Rather than mocking `factory.js` or `local-cdp-client.js` directly
18
+ * (both of which would leak module-level mocks into other test files
19
+ * via bun's shared mock registry), we only mock `browser-manager.js`
20
+ * and return a fake Playwright page whose CDP session routes through
21
+ * a programmable handler. That lets the real `LocalCdpClient` +
22
+ * `getCdpClient` factory code run end-to-end, so this file does not
23
+ * interfere with `local-cdp-client.test.ts` or `factory.test.ts`.
24
+ */
25
+ type CdpCall = { method: string; params: Record<string, unknown> };
26
+ let cdpCalls: CdpCall[] = [];
27
+ let cdpSend: (
28
+ method: string,
29
+ params?: Record<string, unknown>,
30
+ ) => Promise<unknown>;
31
+ let detachCalls: number;
20
32
 
21
33
  let closeSessionPageMock: ReturnType<typeof mock>;
22
34
  let closeAllPagesMock: ReturnType<typeof mock>;
23
- let storeSnapshotMapMock: ReturnType<typeof mock>;
24
- let storedMaps: Map<string, Map<string, string>>;
35
+ let clearSnapshotBackendNodeMapMock: ReturnType<typeof mock>;
36
+ let storeSnapshotBackendNodeMapMock: ReturnType<typeof mock>;
37
+ let storedBackendNodeMaps: Map<string, Map<string, number>>;
25
38
 
26
39
  mock.module("../tools/browser/browser-manager.js", () => {
27
- storedMaps = new Map();
40
+ storedBackendNodeMaps = new Map();
28
41
  closeSessionPageMock = mock(async () => {});
29
42
  closeAllPagesMock = mock(async () => {});
30
- storeSnapshotMapMock = mock(
31
- (conversationId: string, map: Map<string, string>) => {
32
- storedMaps.set(conversationId, map);
43
+ clearSnapshotBackendNodeMapMock = mock((conversationId: string) => {
44
+ storedBackendNodeMaps.delete(conversationId);
45
+ });
46
+ storeSnapshotBackendNodeMapMock = mock(
47
+ (conversationId: string, map: Map<string, number>) => {
48
+ storedBackendNodeMaps.set(conversationId, map);
33
49
  },
34
50
  );
51
+ // Fake Playwright page whose CDPSession routes to our per-test
52
+ // handler. LocalCdpClient lazily creates the session on first send,
53
+ // which is how the real tool path drives us.
54
+ const fakeSession = {
55
+ send: async (method: string, params?: Record<string, unknown>) => {
56
+ cdpCalls.push({ method, params: params ?? {} });
57
+ return cdpSend(method, params);
58
+ },
59
+ detach: async () => {
60
+ detachCalls += 1;
61
+ },
62
+ };
63
+ const fakePage = {
64
+ context: () => ({
65
+ newCDPSession: async () => fakeSession,
66
+ }),
67
+ };
35
68
  return {
36
69
  browserManager: {
37
- getOrCreateSessionPage: async () => mockPage,
70
+ getOrCreateSessionPage: async (_conversationId: string) => fakePage,
38
71
  closeSessionPage: closeSessionPageMock,
39
72
  closeAllPages: closeAllPagesMock,
40
- storeSnapshotMap: storeSnapshotMapMock,
41
- resolveSnapshotSelector: (conversationId: string, elementId: string) => {
42
- const map = storedMaps.get(conversationId);
73
+ storeSnapshotBackendNodeMap: storeSnapshotBackendNodeMapMock,
74
+ clearSnapshotBackendNodeMap: clearSnapshotBackendNodeMapMock,
75
+ resolveSnapshotBackendNodeId: (
76
+ conversationId: string,
77
+ elementId: string,
78
+ ) => {
79
+ const map = storedBackendNodeMaps.get(conversationId);
43
80
  if (!map) return null;
44
81
  return map.get(elementId) ?? null;
45
82
  },
@@ -67,95 +104,221 @@ const ctx: ToolContext = {
67
104
  trustClass: "guardian",
68
105
  };
69
106
 
70
- function resetMockPage() {
71
- mockPage = {
72
- evaluate: mock(async () => []),
73
- title: mock(async () => "Test Page"),
74
- url: mock(() => "https://example.com/"),
75
- goto: mock(async () => ({
76
- status: () => 200,
77
- url: () => "https://example.com/",
78
- })),
79
- close: mock(async () => {}),
80
- isClosed: () => false,
107
+ // ── Fixtures ─────────────────────────────────────────────────────────
108
+
109
+ /**
110
+ * Minimal three-element Accessibility.getFullAXTree fixture. Mirrors
111
+ * the shape CDP returns — a flat array of nodes with parent/child
112
+ * references via `childIds`. Roles are chosen from the snapshot
113
+ * transformer's interactive-role allowlist so the output contains
114
+ * exactly three elements. The root `WebArea` node owns all three as
115
+ * children so document-order traversal yields e1/e2/e3.
116
+ */
117
+ function buildAxTreeFixture(): { nodes: Record<string, unknown>[] } {
118
+ return {
119
+ nodes: [
120
+ {
121
+ nodeId: "1",
122
+ role: { type: "role", value: "WebArea" },
123
+ name: { type: "computedString", value: "" },
124
+ childIds: ["2", "3", "4"],
125
+ backendDOMNodeId: 1,
126
+ },
127
+ {
128
+ nodeId: "2",
129
+ role: { type: "role", value: "link" },
130
+ name: { type: "computedString", value: "About Us" },
131
+ properties: [
132
+ { name: "url", value: { type: "string", value: "/about" } },
133
+ ],
134
+ childIds: [],
135
+ backendDOMNodeId: 42,
136
+ },
137
+ {
138
+ nodeId: "3",
139
+ role: { type: "role", value: "button" },
140
+ name: { type: "computedString", value: "Submit" },
141
+ properties: [],
142
+ childIds: [],
143
+ backendDOMNodeId: 99,
144
+ },
145
+ {
146
+ nodeId: "4",
147
+ role: { type: "role", value: "textbox" },
148
+ name: { type: "computedString", value: "Email" },
149
+ properties: [
150
+ {
151
+ name: "placeholder",
152
+ value: { type: "string", value: "you@example.com" },
153
+ },
154
+ ],
155
+ childIds: [],
156
+ backendDOMNodeId: 101,
157
+ },
158
+ ],
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Build a default CDP `send` handler that returns canned success
164
+ * responses for the methods `executeBrowserSnapshot` touches. Tests
165
+ * pass a map of overrides to customise individual responses or throw
166
+ * from a specific method.
167
+ */
168
+ function installCdpSend(
169
+ overrides: Partial<{
170
+ url: string;
171
+ title: string;
172
+ axTree: unknown;
173
+ throwFrom: string;
174
+ }> = {},
175
+ ) {
176
+ const url = overrides.url ?? "https://example.com/";
177
+ const title = overrides.title ?? "Example Page";
178
+ const axTree = overrides.axTree ?? { nodes: [] };
179
+ const throwFrom = overrides.throwFrom;
180
+
181
+ let runtimeEvaluateCall = 0;
182
+ cdpSend = async (method, _params) => {
183
+ if (throwFrom === method) {
184
+ throw new Error("tab detached");
185
+ }
186
+ switch (method) {
187
+ case "Runtime.evaluate": {
188
+ // First call → URL, second → title. Anything beyond returns "".
189
+ const value =
190
+ runtimeEvaluateCall === 0
191
+ ? url
192
+ : runtimeEvaluateCall === 1
193
+ ? title
194
+ : "";
195
+ runtimeEvaluateCall += 1;
196
+ return { result: { value } };
197
+ }
198
+ case "Accessibility.enable":
199
+ return {};
200
+ case "Accessibility.getFullAXTree":
201
+ return axTree;
202
+ default:
203
+ return {};
204
+ }
81
205
  };
206
+ return { url, title };
207
+ }
208
+
209
+ function resetCdpState() {
210
+ cdpCalls = [];
211
+ detachCalls = 0;
212
+ cdpSend = async () => ({});
82
213
  }
83
214
 
84
215
  // ── browser_snapshot ─────────────────────────────────────────────────
85
216
 
86
- describe("executeBrowserSnapshot", () => {
217
+ describe("executeBrowserSnapshot (CDP Accessibility.getFullAXTree)", () => {
87
218
  beforeEach(() => {
88
- resetMockPage();
89
- storedMaps.clear();
90
- storeSnapshotMapMock.mockClear();
219
+ resetCdpState();
220
+ storedBackendNodeMaps.clear();
221
+ storeSnapshotBackendNodeMapMock.mockClear();
91
222
  });
92
223
 
93
- test("returns page URL and title with no elements", async () => {
224
+ test("returns URL, title, and a list of interactive elements", async () => {
225
+ installCdpSend({
226
+ url: "https://example.com/",
227
+ title: "Example Page",
228
+ axTree: buildAxTreeFixture(),
229
+ });
230
+
94
231
  const result = await executeBrowserSnapshot({}, ctx);
232
+
95
233
  expect(result.isError).toBe(false);
96
234
  expect(result.content).toContain("URL: https://example.com/");
97
- expect(result.content).toContain("Title: Test Page");
98
- expect(result.content).toContain("(no interactive elements found)");
235
+ expect(result.content).toContain("Title: Example Page");
236
+ expect(result.content).toContain("[e1] <link");
237
+ expect(result.content).toContain("About Us");
238
+ expect(result.content).toContain("[e2] <button> Submit");
239
+ expect(result.content).toContain("[e3] <textbox");
240
+ expect(result.content).toContain("Email");
241
+ expect(result.content).toContain("3 interactive elements found.");
99
242
  });
100
243
 
101
- test("lists interactive elements with element IDs", async () => {
102
- mockPage.evaluate = mock(async () => [
103
- { eid: "e1", tag: "a", attrs: { href: "/about" }, text: "About Us" },
104
- { eid: "e2", tag: "button", attrs: {}, text: "Submit" },
105
- {
106
- eid: "e3",
107
- tag: "input",
108
- attrs: { type: "text", name: "email", placeholder: "Email" },
109
- text: "",
110
- },
111
- ]);
244
+ test("calls Accessibility.enable and getFullAXTree via CDP", async () => {
245
+ installCdpSend();
112
246
 
113
- const result = await executeBrowserSnapshot({}, ctx);
114
- expect(result.isError).toBe(false);
115
- expect(result.content).toContain('[e1] <a href="/about"> About Us');
116
- expect(result.content).toContain("[e2] <button> Submit");
117
- expect(result.content).toContain(
118
- '[e3] <input type="text" name="email" placeholder="Email">',
119
- );
120
- expect(result.content).toContain("3 interactive elements found.");
247
+ await executeBrowserSnapshot({}, ctx);
248
+
249
+ const methods = cdpCalls.map((c) => c.method);
250
+ expect(methods).toContain("Accessibility.enable");
251
+ expect(methods).toContain("Accessibility.getFullAXTree");
252
+ });
253
+
254
+ test("stores backendNodeId map keyed by eid", async () => {
255
+ installCdpSend({
256
+ axTree: buildAxTreeFixture(),
257
+ });
258
+
259
+ await executeBrowserSnapshot({}, ctx);
260
+
261
+ expect(storeSnapshotBackendNodeMapMock).toHaveBeenCalledTimes(1);
262
+ const backendMap = storedBackendNodeMaps.get("test-conversation");
263
+ expect(backendMap).toBeDefined();
264
+ expect(backendMap!.get("e1")).toBe(42);
265
+ expect(backendMap!.get("e2")).toBe(99);
266
+ expect(backendMap!.get("e3")).toBe(101);
121
267
  });
122
268
 
123
- test("stores selector map in browser manager", async () => {
124
- mockPage.evaluate = mock(async () => [
125
- { eid: "e1", tag: "a", attrs: { href: "/" }, text: "Home" },
126
- { eid: "e2", tag: "button", attrs: {}, text: "OK" },
127
- ]);
269
+ test("does not invoke the legacy DOM tagging bridge", async () => {
270
+ installCdpSend({
271
+ axTree: buildAxTreeFixture(),
272
+ });
128
273
 
129
274
  await executeBrowserSnapshot({}, ctx);
130
- expect(storeSnapshotMapMock).toHaveBeenCalledTimes(1);
131
275
 
132
- const stored = storedMaps.get("test-conversation");
133
- expect(stored).toBeDefined();
134
- expect(stored!.get("e1")).toBe('[data-vellum-eid="e1"]');
135
- expect(stored!.get("e2")).toBe('[data-vellum-eid="e2"]');
276
+ // The legacy eid → data-vellum-eid bridge has been removed;
277
+ // interaction tools use the backendNodeId map directly.
278
+ const methods = cdpCalls.map((c) => c.method);
279
+ expect(methods).not.toContain("DOM.pushNodesByBackendIdsToFrontend");
280
+ expect(methods).not.toContain("DOM.setAttributeValue");
136
281
  });
137
282
 
138
- test("handles single element with singular count", async () => {
139
- mockPage.evaluate = mock(async () => [
140
- { eid: "e1", tag: "button", attrs: {}, text: "Click" },
141
- ]);
283
+ test("renders '(no interactive elements found)' on empty AX tree", async () => {
284
+ installCdpSend({ axTree: { nodes: [] } });
142
285
 
143
286
  const result = await executeBrowserSnapshot({}, ctx);
144
- expect(result.content).toContain("1 interactive element found.");
287
+ expect(result.isError).toBe(false);
288
+ expect(result.content).toContain("(no interactive elements found)");
289
+ // Empty map should still be stored so stale eids from a previous
290
+ // snapshot cannot resolve after this call.
291
+ expect(storeSnapshotBackendNodeMapMock).toHaveBeenCalledTimes(1);
292
+ const backendMap = storedBackendNodeMaps.get("test-conversation");
293
+ expect(backendMap?.size ?? 0).toBe(0);
145
294
  });
146
295
 
147
- test("handles evaluate error", async () => {
148
- mockPage.evaluate = mock(async () => {
149
- throw new Error("page crashed");
150
- });
296
+ test("returns error content when Accessibility.getFullAXTree throws", async () => {
297
+ installCdpSend({ throwFrom: "Accessibility.getFullAXTree" });
298
+
151
299
  const result = await executeBrowserSnapshot({}, ctx);
300
+
152
301
  expect(result.isError).toBe(true);
153
302
  expect(result.content).toContain("Snapshot failed");
154
- expect(result.content).toContain("page crashed");
303
+ expect(result.content).toContain("tab detached");
304
+ // dispose still runs in the finally block, which schedules an
305
+ // async session.detach() — flush microtasks before asserting.
306
+ await new Promise((resolve) => setTimeout(resolve, 0));
307
+ expect(detachCalls).toBe(1);
308
+ });
309
+
310
+ test("disposes the CdpClient even on success", async () => {
311
+ installCdpSend();
312
+
313
+ await executeBrowserSnapshot({}, ctx);
314
+ // dispose schedules detach asynchronously; flush microtasks.
315
+ await new Promise((resolve) => setTimeout(resolve, 0));
316
+
317
+ expect(detachCalls).toBe(1);
155
318
  });
156
319
 
157
320
  test("shows (none) for empty title", async () => {
158
- mockPage.title = mock(async () => "");
321
+ installCdpSend({ title: "", axTree: { nodes: [] } });
159
322
  const result = await executeBrowserSnapshot({}, ctx);
160
323
  expect(result.content).toContain("Title: (none)");
161
324
  });
@@ -165,7 +328,7 @@ describe("executeBrowserSnapshot", () => {
165
328
 
166
329
  describe("executeBrowserClose", () => {
167
330
  beforeEach(() => {
168
- resetMockPage();
331
+ resetCdpState();
169
332
  closeSessionPageMock.mockClear();
170
333
  closeAllPagesMock.mockClear();
171
334
  });
@@ -1,4 +1,4 @@
1
- import { afterEach, describe, expect, mock, test } from "bun:test";
1
+ import { afterEach, describe, expect, jest, mock, test } from "bun:test";
2
2
 
3
3
  const mockConfig = {
4
4
  timeouts: {
@@ -393,6 +393,155 @@ describe("HostBashProxy", () => {
393
393
  });
394
394
  });
395
395
 
396
+ describe("abort listener lifecycle", () => {
397
+ // Helper that wraps an AbortSignal to observe add/removeEventListener
398
+ // invocations without tripping over tsc's strict overload matching on
399
+ // AbortSignal itself.
400
+ type Spied = {
401
+ signal: AbortSignal;
402
+ addCalls: string[];
403
+ removeCalls: string[];
404
+ };
405
+ function spySignal(source: AbortSignal): Spied {
406
+ const addCalls: string[] = [];
407
+ const removeCalls: string[] = [];
408
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
409
+ const s = source as any;
410
+ const origAdd = source.addEventListener.bind(source);
411
+ const origRemove = source.removeEventListener.bind(source);
412
+ s.addEventListener = (
413
+ type: string,
414
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
415
+ ...rest: any[]
416
+ ) => {
417
+ addCalls.push(type);
418
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
419
+ return (origAdd as any)(type, ...rest);
420
+ };
421
+ s.removeEventListener = (
422
+ type: string,
423
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
424
+ ...rest: any[]
425
+ ) => {
426
+ removeCalls.push(type);
427
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
428
+ return (origRemove as any)(type, ...rest);
429
+ };
430
+ return { signal: source, addCalls, removeCalls };
431
+ }
432
+
433
+ test("removes abort listener from signal after resolve completes", async () => {
434
+ setup();
435
+ const controller = new AbortController();
436
+ const spy = spySignal(controller.signal);
437
+
438
+ const resultPromise = proxy.request(
439
+ { command: "echo hello" },
440
+ "session-1",
441
+ spy.signal,
442
+ );
443
+
444
+ expect(spy.addCalls).toEqual(["abort"]);
445
+ expect(spy.removeCalls).toEqual([]);
446
+
447
+ const requestId = (sentMessages[0] as Record<string, unknown>)
448
+ .requestId as string;
449
+ proxy.resolve(requestId, {
450
+ stdout: "hello\n",
451
+ stderr: "",
452
+ exitCode: 0,
453
+ timedOut: false,
454
+ });
455
+ await resultPromise;
456
+
457
+ // Listener is detached after normal completion.
458
+ expect(spy.removeCalls).toEqual(["abort"]);
459
+
460
+ // Subsequent aborts are harmless no-ops (no side effects on the proxy).
461
+ controller.abort();
462
+ // No additional emitted envelopes from the late abort.
463
+ expect(sentMessages).toHaveLength(1);
464
+ });
465
+
466
+ test("removes abort listener from signal on timer timeout", async () => {
467
+ setup();
468
+
469
+ jest.useFakeTimers();
470
+ try {
471
+ const controller = new AbortController();
472
+ const spy = spySignal(controller.signal);
473
+
474
+ const resultPromise = proxy.request(
475
+ { command: "echo slow", timeout_seconds: 30 },
476
+ "session-1",
477
+ spy.signal,
478
+ );
479
+
480
+ expect(spy.addCalls).toEqual(["abort"]);
481
+ expect(spy.removeCalls).toEqual([]);
482
+
483
+ // Proxy timeout is timeout_seconds + 3 = 33s. Advance past it.
484
+ jest.advanceTimersByTime(34 * 1000);
485
+
486
+ const result = await resultPromise;
487
+ expect(result.isError).toBe(true);
488
+ expect(result.content).toContain("Host bash proxy timed out");
489
+
490
+ // Listener is detached after the timer fires.
491
+ expect(spy.removeCalls).toEqual(["abort"]);
492
+
493
+ // Subsequent aborts should be harmless — no cancel emitted.
494
+ controller.abort();
495
+ expect(sentMessages).toHaveLength(1);
496
+ } finally {
497
+ jest.useRealTimers();
498
+ }
499
+ });
500
+ });
501
+
502
+ describe("sender throws synchronously", () => {
503
+ test("rejects the promise, clears pending state and timer, invokes onInternalResolve", async () => {
504
+ const resolvedIds: string[] = [];
505
+ sentMessages = [];
506
+ sendToClient = () => {
507
+ throw new Error("transport down");
508
+ };
509
+ proxy = new HostBashProxy(sendToClient, (id) => resolvedIds.push(id));
510
+
511
+ // request() synchronously calls sendToClient inside the Promise
512
+ // executor. A throw there surfaces as a rejected promise.
513
+ const resultPromise = proxy.request(
514
+ { command: "echo hello" },
515
+ "session-1",
516
+ );
517
+
518
+ await expect(resultPromise).rejects.toThrow("transport down");
519
+
520
+ // The internal resolve should fire exactly once as part of cleanup.
521
+ expect(resolvedIds).toHaveLength(1);
522
+
523
+ // Issue a new request on a fresh (non-throwing) sender and verify
524
+ // the proxy is still functional — no stale timers or bookkeeping
525
+ // from the failed request.
526
+ sentMessages = [];
527
+ proxy.updateSender((msg) => sentMessages.push(msg), true);
528
+ const okPromise = proxy.request({ command: "echo ok" }, "session-1");
529
+ expect(sentMessages).toHaveLength(1);
530
+ const okRequestId = (sentMessages[0] as Record<string, unknown>)
531
+ .requestId as string;
532
+ expect(proxy.hasPendingRequest(okRequestId)).toBe(true);
533
+ proxy.resolve(okRequestId, {
534
+ stdout: "ok\n",
535
+ stderr: "",
536
+ exitCode: 0,
537
+ timedOut: false,
538
+ });
539
+ const okResult = await okPromise;
540
+ expect(okResult.content).toContain("ok");
541
+ expect(okResult.isError).toBe(false);
542
+ });
543
+ });
544
+
396
545
  describe("onInternalResolve callback", () => {
397
546
  test("fires on abort", async () => {
398
547
  const resolvedIds: string[] = [];