@vellumai/assistant 0.6.2 → 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 (396) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docs/architecture/memory.md +1 -1
  4. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  5. package/openapi.yaml +184 -69
  6. package/package.json +41 -41
  7. package/scripts/generate-openapi.ts +1 -2
  8. package/src/__tests__/acp-session.test.ts +43 -0
  9. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  10. package/src/__tests__/app-executors.test.ts +1 -0
  11. package/src/__tests__/app-source-watcher.test.ts +37 -11
  12. package/src/__tests__/approval-routes-http.test.ts +178 -1
  13. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  14. package/src/__tests__/browser-manager.test.ts +40 -27
  15. package/src/__tests__/catalog-files.test.ts +862 -0
  16. package/src/__tests__/channel-approvals.test.ts +53 -0
  17. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +125 -48
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  21. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  22. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  24. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  25. package/src/__tests__/conversation-attachments.test.ts +80 -4
  26. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  27. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  28. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  29. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  30. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  31. package/src/__tests__/conversation-queue.test.ts +45 -2
  32. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  33. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  34. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  35. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  36. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  37. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  38. package/src/__tests__/conversation-store.test.ts +195 -0
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  40. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  41. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  42. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  43. package/src/__tests__/credential-vault.test.ts +152 -13
  44. package/src/__tests__/credentials-cli.test.ts +2 -2
  45. package/src/__tests__/date-context.test.ts +4 -4
  46. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  47. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  48. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  49. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  50. package/src/__tests__/gemini-provider.test.ts +2 -2
  51. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  52. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  53. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  54. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  55. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  56. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  57. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  58. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  59. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  60. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  61. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  62. package/src/__tests__/host-browser-routes.test.ts +198 -0
  63. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  64. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  65. package/src/__tests__/host-file-proxy.test.ts +185 -1
  66. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  67. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  68. package/src/__tests__/host-shell-tool.test.ts +1 -11
  69. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  70. package/src/__tests__/integration-status.test.ts +6 -7
  71. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  72. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  73. package/src/__tests__/mcp-health-check.test.ts +10 -3
  74. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  75. package/src/__tests__/migration-export-http.test.ts +61 -2
  76. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  77. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  78. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  80. package/src/__tests__/oauth-cli.test.ts +707 -60
  81. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  82. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  83. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  84. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  85. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  86. package/src/__tests__/oauth-store.test.ts +1386 -182
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  88. package/src/__tests__/onboarding-template-contract.test.ts +75 -57
  89. package/src/__tests__/openai-provider.test.ts +2 -2
  90. package/src/__tests__/outlook-categories.test.ts +1 -1
  91. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  92. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  93. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  94. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  95. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  96. package/src/__tests__/outlook-trash.test.ts +1 -1
  97. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  98. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  99. package/src/__tests__/permission-mode.test.ts +28 -56
  100. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  101. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  102. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  103. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  104. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  105. package/src/__tests__/schedule-routes.test.ts +162 -0
  106. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  107. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  108. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  109. package/src/__tests__/set-permission-mode.test.ts +13 -250
  110. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  111. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  112. package/src/__tests__/slack-channel-config.test.ts +12 -15
  113. package/src/__tests__/subagent-detail.test.ts +44 -2
  114. package/src/__tests__/subagent-disposal.test.ts +1 -0
  115. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  116. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  117. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  118. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  119. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  120. package/src/__tests__/subagent-tools.test.ts +1 -0
  121. package/src/__tests__/subagent-types.test.ts +1 -0
  122. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  123. package/src/__tests__/system-prompt.test.ts +72 -1
  124. package/src/__tests__/task-scheduler.test.ts +32 -6
  125. package/src/__tests__/telegram-config.test.ts +10 -13
  126. package/src/__tests__/terminal-tools.test.ts +9 -0
  127. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  128. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  129. package/src/__tests__/top-level-renderer.test.ts +73 -1
  130. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  131. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  132. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  133. package/src/acp/client-handler.ts +30 -4
  134. package/src/agent/loop.ts +12 -6
  135. package/src/approvals/guardian-request-resolvers.ts +21 -15
  136. package/src/browser-session/__tests__/manager.test.ts +297 -0
  137. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  138. package/src/browser-session/backends/extension.ts +26 -0
  139. package/src/browser-session/backends/local.ts +24 -0
  140. package/src/browser-session/events.ts +164 -0
  141. package/src/browser-session/index.ts +27 -0
  142. package/src/browser-session/manager.ts +159 -0
  143. package/src/browser-session/types.ts +28 -0
  144. package/src/channels/__tests__/types.test.ts +134 -0
  145. package/src/channels/types.ts +53 -3
  146. package/src/cli/commands/browser-relay.ts +339 -409
  147. package/src/cli/commands/credentials.ts +3 -3
  148. package/src/cli/commands/email.ts +18 -13
  149. package/src/cli/commands/mcp.ts +16 -4
  150. package/src/cli/commands/oauth/__tests__/connect.test.ts +44 -44
  151. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  152. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  153. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  154. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  155. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  156. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  157. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  158. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  159. package/src/cli/commands/oauth/apps.ts +7 -4
  160. package/src/cli/commands/oauth/connect.ts +6 -3
  161. package/src/cli/commands/oauth/disconnect.ts +1 -1
  162. package/src/cli/commands/oauth/providers.ts +200 -36
  163. package/src/cli/commands/oauth/shared.ts +5 -5
  164. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  165. package/src/cli/commands/platform/index.ts +107 -10
  166. package/src/cli/commands/usage.ts +10 -9
  167. package/src/cli/lib/daemon-credential-client.ts +4 -0
  168. package/src/cli/program.ts +1 -1
  169. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  170. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  171. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  172. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  173. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  174. package/src/config/bundled-skills/document/SKILL.md +4 -0
  175. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  176. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  177. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  178. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  179. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  180. package/src/config/env-registry.ts +14 -0
  181. package/src/config/env.ts +21 -0
  182. package/src/config/feature-flag-registry.json +44 -5
  183. package/src/config/loader.ts +56 -1
  184. package/src/config/sanitize-for-transfer.ts +47 -0
  185. package/src/config/schema.ts +46 -5
  186. package/src/config/schemas/host-browser.ts +66 -0
  187. package/src/config/schemas/memory-lifecycle.ts +1 -1
  188. package/src/config/schemas/memory-retrieval.ts +103 -0
  189. package/src/config/schemas/security.ts +0 -6
  190. package/src/config/schemas/services.ts +8 -0
  191. package/src/config/types.ts +0 -1
  192. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  193. package/src/context/window-manager.ts +19 -1
  194. package/src/credential-execution/approval-bridge.ts +49 -15
  195. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  196. package/src/daemon/app-source-watcher.ts +35 -0
  197. package/src/daemon/context-overflow-approval.ts +5 -0
  198. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  199. package/src/daemon/conversation-agent-loop.ts +58 -24
  200. package/src/daemon/conversation-attachments.ts +40 -0
  201. package/src/daemon/conversation-process.ts +48 -1
  202. package/src/daemon/conversation-runtime-assembly.ts +118 -36
  203. package/src/daemon/conversation-surfaces.ts +37 -36
  204. package/src/daemon/conversation-tool-setup.ts +74 -8
  205. package/src/daemon/conversation-workspace.ts +12 -0
  206. package/src/daemon/conversation.ts +226 -8
  207. package/src/daemon/date-context.ts +10 -10
  208. package/src/daemon/first-greeting.ts +3 -2
  209. package/src/daemon/handlers/conversations.ts +9 -140
  210. package/src/daemon/handlers/shared.ts +58 -0
  211. package/src/daemon/handlers/skills.ts +232 -37
  212. package/src/daemon/host-bash-proxy.ts +48 -13
  213. package/src/daemon/host-browser-proxy.ts +191 -0
  214. package/src/daemon/host-cu-proxy.ts +36 -11
  215. package/src/daemon/host-file-proxy.ts +57 -9
  216. package/src/daemon/lifecycle.ts +65 -11
  217. package/src/daemon/message-protocol.ts +7 -0
  218. package/src/daemon/message-types/conversations.ts +55 -13
  219. package/src/daemon/message-types/host-browser.ts +100 -0
  220. package/src/daemon/message-types/messages.ts +5 -5
  221. package/src/daemon/message-types/skills.ts +10 -0
  222. package/src/daemon/message-types/subagents.ts +2 -0
  223. package/src/daemon/server.ts +92 -12
  224. package/src/daemon/tool-side-effects.ts +6 -0
  225. package/src/daemon/transport-hints.ts +5 -24
  226. package/src/inbound/platform-callback-registration.ts +18 -17
  227. package/src/mcp/client.ts +59 -24
  228. package/src/memory/app-store.ts +31 -1
  229. package/src/memory/conversation-crud.ts +23 -0
  230. package/src/memory/conversation-starters-cadence.ts +76 -0
  231. package/src/memory/conversation-title-service.ts +5 -2
  232. package/src/memory/db-init.ts +12 -0
  233. package/src/memory/embedding-backend.test.ts +75 -0
  234. package/src/memory/embedding-backend.ts +131 -5
  235. package/src/memory/embedding-gemini.test.ts +54 -0
  236. package/src/memory/embedding-gemini.ts +20 -9
  237. package/src/memory/embedding-local.ts +176 -17
  238. package/src/memory/graph/consolidation.ts +10 -23
  239. package/src/memory/graph/extraction-job.ts +15 -0
  240. package/src/memory/graph/retriever.ts +40 -22
  241. package/src/memory/graph/store.test.ts +7 -3
  242. package/src/memory/graph/store.ts +47 -12
  243. package/src/memory/llm-usage-store.ts +45 -4
  244. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  245. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  246. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  247. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  248. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  249. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  250. package/src/memory/migrations/index.ts +6 -0
  251. package/src/memory/migrations/registry.ts +8 -0
  252. package/src/memory/schema/conversations.ts +1 -0
  253. package/src/memory/schema/oauth.ts +18 -13
  254. package/src/oauth/AGENTS.md +76 -0
  255. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  256. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  257. package/src/oauth/byo-connection.test.ts +8 -8
  258. package/src/oauth/byo-connection.ts +7 -7
  259. package/src/oauth/connect-orchestrator.ts +23 -21
  260. package/src/oauth/connect-types.ts +3 -3
  261. package/src/oauth/connection-resolver.test.ts +17 -4
  262. package/src/oauth/connection-resolver.ts +16 -16
  263. package/src/oauth/connection.ts +1 -1
  264. package/src/oauth/manual-token-connection.ts +13 -13
  265. package/src/oauth/oauth-store.ts +214 -100
  266. package/src/oauth/platform-connection.test.ts +3 -3
  267. package/src/oauth/platform-connection.ts +4 -4
  268. package/src/oauth/provider-serializer.ts +31 -5
  269. package/src/oauth/revoke.ts +76 -0
  270. package/src/oauth/seed-providers.ts +126 -87
  271. package/src/oauth/token-persistence.ts +1 -1
  272. package/src/permissions/permission-mode.ts +4 -11
  273. package/src/permissions/prompter.ts +13 -1
  274. package/src/permissions/v2-consent-policy.ts +87 -0
  275. package/src/prompts/system-prompt.ts +18 -21
  276. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  277. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  278. package/src/providers/anthropic/client.ts +1 -0
  279. package/src/providers/types.ts +1 -1
  280. package/src/runtime/AGENTS.md +23 -0
  281. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  282. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  283. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  284. package/src/runtime/assistant-event-hub.ts +2 -2
  285. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  286. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  287. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  288. package/src/runtime/auth/middleware.ts +98 -0
  289. package/src/runtime/auth/route-policy.ts +6 -7
  290. package/src/runtime/capability-tokens.ts +414 -0
  291. package/src/runtime/channel-approvals.ts +18 -5
  292. package/src/runtime/chrome-extension-registry.ts +332 -0
  293. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  294. package/src/runtime/guardian-decision-types.ts +7 -0
  295. package/src/runtime/http-server.ts +425 -70
  296. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  297. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  298. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  299. package/src/runtime/migrations/migration-transport.ts +6 -0
  300. package/src/runtime/migrations/migration-wizard.ts +22 -2
  301. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  302. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  303. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  304. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  305. package/src/runtime/pending-interactions.ts +29 -13
  306. package/src/runtime/routes/approval-routes.ts +90 -16
  307. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  308. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  309. package/src/runtime/routes/conversation-analysis-routes.ts +2 -1
  310. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  311. package/src/runtime/routes/conversation-routes.ts +301 -27
  312. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  313. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  314. package/src/runtime/routes/host-browser-routes.ts +279 -0
  315. package/src/runtime/routes/host-file-routes.ts +9 -1
  316. package/src/runtime/routes/identity-routes.ts +259 -16
  317. package/src/runtime/routes/log-export-routes.ts +42 -22
  318. package/src/runtime/routes/memory-item-routes.ts +1 -7
  319. package/src/runtime/routes/migration-routes.ts +87 -2
  320. package/src/runtime/routes/oauth-apps.ts +15 -17
  321. package/src/runtime/routes/oauth-providers.ts +4 -0
  322. package/src/runtime/routes/schedule-routes.ts +24 -11
  323. package/src/runtime/routes/settings-routes.ts +9 -97
  324. package/src/runtime/routes/skills-routes.ts +52 -2
  325. package/src/runtime/routes/subagents-routes.ts +14 -10
  326. package/src/runtime/routes/usage-routes.ts +8 -7
  327. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  328. package/src/runtime/routes/workspace-routes.ts +8 -1
  329. package/src/runtime/routes/workspace-utils.ts +2 -0
  330. package/src/schedule/scheduler.ts +7 -5
  331. package/src/security/ces-credential-client.ts +20 -0
  332. package/src/security/ces-rpc-credential-backend.ts +17 -0
  333. package/src/security/credential-backend.ts +5 -0
  334. package/src/security/oauth2.ts +42 -25
  335. package/src/security/secure-keys.ts +118 -25
  336. package/src/security/token-manager.ts +23 -10
  337. package/src/skills/catalog-files.ts +492 -0
  338. package/src/subagent/manager.ts +131 -26
  339. package/src/subagent/types.ts +19 -0
  340. package/src/tools/apps/executors.ts +11 -2
  341. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  342. package/src/tools/browser/auth-detector.ts +43 -12
  343. package/src/tools/browser/browser-execution.ts +645 -340
  344. package/src/tools/browser/browser-manager.ts +36 -12
  345. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  346. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  347. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  348. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  349. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  350. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  351. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  352. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  353. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  354. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  355. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  356. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  357. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  358. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  359. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  360. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  361. package/src/tools/browser/cdp-client/errors.ts +34 -0
  362. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  363. package/src/tools/browser/cdp-client/factory.ts +204 -0
  364. package/src/tools/browser/cdp-client/index.ts +14 -0
  365. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  366. package/src/tools/browser/cdp-client/types.ts +52 -0
  367. package/src/tools/filesystem/edit.ts +1 -1
  368. package/src/tools/filesystem/list.ts +1 -1
  369. package/src/tools/filesystem/read.ts +1 -1
  370. package/src/tools/filesystem/write.ts +2 -1
  371. package/src/tools/host-filesystem/edit.ts +1 -1
  372. package/src/tools/host-filesystem/read.ts +12 -15
  373. package/src/tools/host-filesystem/write.ts +1 -1
  374. package/src/tools/host-terminal/host-shell.ts +21 -16
  375. package/src/tools/permission-checker.ts +77 -82
  376. package/src/tools/registry.ts +0 -2
  377. package/src/tools/secret-detection-handler.ts +34 -0
  378. package/src/tools/shared/filesystem/image-read.ts +61 -40
  379. package/src/tools/subagent/spawn.ts +47 -3
  380. package/src/tools/subagent/status.ts +2 -0
  381. package/src/tools/system/register.ts +2 -16
  382. package/src/tools/terminal/safe-env.ts +7 -0
  383. package/src/tools/terminal/shell.ts +21 -16
  384. package/src/tools/tool-approval-handler.ts +48 -2
  385. package/src/tools/types.ts +2 -0
  386. package/src/util/platform.ts +14 -19
  387. package/src/workspace/top-level-renderer.ts +19 -1
  388. package/src/__tests__/chrome-cdp.test.ts +0 -419
  389. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  390. package/src/__tests__/permission-mode-store.test.ts +0 -277
  391. package/src/browser-extension-relay/protocol.ts +0 -63
  392. package/src/browser-extension-relay/server.ts +0 -203
  393. package/src/config/schemas/sandbox.ts +0 -14
  394. package/src/permissions/permission-mode-store.ts +0 -180
  395. package/src/tools/browser/chrome-cdp.ts +0 -239
  396. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -32,6 +32,7 @@ export const SAFE_ENV_VARS = [
32
32
  "CES_BOOTSTRAP_SOCKET_DIR",
33
33
  "GATEWAY_INTERNAL_URL",
34
34
  "VELLUM_PLATFORM_URL",
35
+ "VELLUM_DOCS_BASE_URL",
35
36
  "CES_CREDENTIAL_URL",
36
37
  "CES_MANAGED_MODE",
37
38
  "IS_CONTAINERIZED",
@@ -42,6 +43,8 @@ export const SAFE_ENV_VARS = [
42
43
  "VELLUM_PROFILER_MAX_BYTES",
43
44
  "VELLUM_PROFILER_MAX_RUNS",
44
45
  "VELLUM_PROFILER_MIN_FREE_MB",
46
+ "VELLUM_MEMORY_LIMIT",
47
+ "VELLUM_CPU_LIMIT",
45
48
  ] as const;
46
49
 
47
50
  /**
@@ -71,5 +74,9 @@ export function buildSanitizedEnv(): Record<string, string> {
71
74
  // Expose the workspace directory so skills and child processes can read/write
72
75
  // workspace-scoped files (e.g. avatar traits, user data).
73
76
  env.VELLUM_WORKSPACE_DIR = getWorkspaceDir();
77
+ // Ensure UTF-8 locale so multi-byte characters (em dashes, curly quotes,
78
+ // arrows, etc.) survive piping through tools like pbcopy without corruption.
79
+ if (!env.LANG) env.LANG = "C.UTF-8";
80
+ if (!env.LC_ALL) env.LC_ALL = "C.UTF-8";
74
81
  return env;
75
82
  }
@@ -327,30 +327,35 @@ class ShellTool implements Tool {
327
327
  detached: true,
328
328
  });
329
329
 
330
- const timer = setTimeout(() => {
331
- timedOut = true;
330
+ // Kill the entire process tree. Tries the process group first
331
+ // (negative PID), then falls back to killing the direct child if the
332
+ // PID is unavailable or the group kill fails.
333
+ const killTree = () => {
334
+ if (child.pid != null) {
335
+ try {
336
+ process.kill(-child.pid, "SIGKILL");
337
+ return;
338
+ } catch {
339
+ // Process group may have already exited — fall through.
340
+ }
341
+ }
332
342
  try {
333
- process.kill(-child.pid!, "SIGKILL");
343
+ child.kill("SIGKILL");
334
344
  } catch {
335
- // Process group may have already exited.
345
+ // Child may have already exited.
336
346
  }
347
+ };
348
+
349
+ const timer = setTimeout(() => {
350
+ timedOut = true;
351
+ killTree();
337
352
  }, timeoutMs);
338
353
 
339
354
  // Cooperative cancellation via AbortSignal
340
- const onAbort = () => {
341
- try {
342
- process.kill(-child.pid!, "SIGKILL");
343
- } catch {
344
- // Process group may have already exited.
345
- }
346
- };
355
+ const onAbort = () => killTree();
347
356
  if (context.signal) {
348
357
  if (context.signal.aborted) {
349
- try {
350
- process.kill(-child.pid!, "SIGKILL");
351
- } catch {
352
- // Process group may have already exited.
353
- }
358
+ killTree();
354
359
  } else {
355
360
  context.signal.addEventListener("abort", onAbort, { once: true });
356
361
  }
@@ -4,6 +4,10 @@ import {
4
4
  getCanonicalGuardianRequest,
5
5
  updateCanonicalGuardianRequest,
6
6
  } from "../memory/canonical-guardian-store.js";
7
+ import {
8
+ isConversationHostAccessEnabled,
9
+ isPermissionControlsV2Enabled,
10
+ } from "../permissions/v2-consent-policy.js";
7
11
  import { isUntrustedTrustClass } from "../runtime/actor-trust-resolver.js";
8
12
  import { createOrReuseToolGrantRequest } from "../runtime/tool-grant-request-helper.js";
9
13
  import { redactSecrets } from "../security/secret-scanner.js";
@@ -178,6 +182,10 @@ function guardianApprovalDeniedMessage(
178
182
  return `Permission denied for "${toolName}": this action requires guardian approval and the current actor is not the guardian.`;
179
183
  }
180
184
 
185
+ function trustedContactHostAccessDeniedMessage(toolName: string): string {
186
+ return `Permission denied for "${toolName}": computer access is not enabled for this conversation. Confirm the guardian's intent conversationally and ask them to enable computer access for this conversation before retrying.`;
187
+ }
188
+
181
189
  export type PreExecutionGateResult =
182
190
  | { allowed: true; tool: Tool; grantConsumed?: boolean }
183
191
  | { allowed: false; result: ToolExecutionResult };
@@ -216,6 +224,8 @@ export class ToolApprovalHandler {
216
224
  startTime: number,
217
225
  emitLifecycleEvent: (event: ToolLifecycleEvent) => void,
218
226
  ): Promise<PreExecutionGateResult> {
227
+ const v2Enabled = isPermissionControlsV2Enabled();
228
+
219
229
  // Bail out immediately if the session was aborted before this tool started.
220
230
  if (context.signal?.aborted) {
221
231
  const durationMs = Date.now() - startTime;
@@ -286,9 +296,44 @@ export class ToolApprovalHandler {
286
296
  | Parameters<typeof consumeGrantForInvocation>[0]
287
297
  | null = null;
288
298
 
299
+ const guardianApprovalRequired = requiresGuardianApprovalForActor(
300
+ name,
301
+ input,
302
+ executionTarget,
303
+ );
304
+
305
+ if (
306
+ v2Enabled &&
307
+ context.trustClass === "trusted_contact" &&
308
+ guardianApprovalRequired &&
309
+ executionTarget === "host" &&
310
+ !isConversationHostAccessEnabled(context.conversationId)
311
+ ) {
312
+ const durationMs = Date.now() - startTime;
313
+ const reason = trustedContactHostAccessDeniedMessage(name);
314
+ emitLifecycleEvent({
315
+ type: "permission_denied",
316
+ toolName: name,
317
+ executionTarget,
318
+ input,
319
+ workingDir: context.workingDir,
320
+ conversationId: context.conversationId,
321
+ requestId: context.requestId,
322
+ riskLevel,
323
+ decision: "deny",
324
+ reason,
325
+ durationMs,
326
+ });
327
+ return {
328
+ allowed: false,
329
+ result: { content: reason, isError: true },
330
+ };
331
+ }
332
+
289
333
  if (
290
334
  isUntrustedTrustClass(context.trustClass) &&
291
- requiresGuardianApprovalForActor(name, input, executionTarget)
335
+ guardianApprovalRequired &&
336
+ !(v2Enabled && context.trustClass === "trusted_contact")
292
337
  ) {
293
338
  const inputDigest = computeToolApprovalDigest(name, input);
294
339
  needsGrantConsumption = true;
@@ -513,7 +558,8 @@ export class ToolApprovalHandler {
513
558
  toolName: name,
514
559
  inputDigest,
515
560
  questionText: buildToolGrantQuestionText(name, input, context),
516
- requesterIdentifier: context.requesterDisplayName || context.requesterIdentifier,
561
+ requesterIdentifier:
562
+ context.requesterDisplayName || context.requesterIdentifier,
517
563
  });
518
564
 
519
565
  // Only wait inline if the escalation succeeded (created or deduped).
@@ -176,6 +176,8 @@ export interface ToolContext {
176
176
  toolUseId?: string;
177
177
  /** Optional proxy for delegating host_bash execution to a connected client (managed/cloud-hosted mode). */
178
178
  hostBashProxy?: import("../daemon/host-bash-proxy.js").HostBashProxy;
179
+ /** Optional proxy for delegating CDP commands to a connected client (managed/cloud-hosted mode). */
180
+ hostBrowserProxy?: import("../daemon/host-browser-proxy.js").HostBrowserProxy;
179
181
  /** Optional proxy for delegating host_file_read/write/edit execution to a connected client (managed/cloud-hosted mode). */
180
182
  hostFileProxy?: import("../daemon/host-file-proxy.js").HostFileProxy;
181
183
  /** True when the assistant is running as a platform-managed remote instance. Used to auto-approve sandboxed bash tools. */
@@ -136,30 +136,13 @@ export function isTCPEnabled(): boolean {
136
136
 
137
137
  /**
138
138
  * Returns the hostname/address for the TCP listener.
139
- * If iOS pairing is enabled (flag file): '0.0.0.0' (LAN-accessible).
140
- * Default: '127.0.0.1' (localhost only).
139
+ * Always binds to localhost only. iOS pairing uses the gateway
140
+ * relay.
141
141
  */
142
142
  export function getTCPHost(): string {
143
- if (isIOSPairingEnabled()) return "0.0.0.0";
144
143
  return "127.0.0.1";
145
144
  }
146
145
 
147
- /**
148
- * Returns whether iOS pairing mode is enabled.
149
- * When enabled, the TCP listener binds to 0.0.0.0 (all interfaces)
150
- * instead of 127.0.0.1 (localhost only), making the daemon reachable
151
- * from iOS devices on the same local network.
152
- *
153
- * Checks for the presence of the flag file ~/.vellum/ios-pairing-enabled.
154
- * Default: false.
155
- *
156
- * This is separate from isTCPEnabled() — TCP can be enabled for localhost-only
157
- * access without exposing the daemon to the LAN.
158
- */
159
- export function isIOSPairingEnabled(): boolean {
160
- return existsSync(join(vellumRoot(), "ios-pairing-enabled"));
161
- }
162
-
163
146
  /**
164
147
  * Returns the XDG-compliant path for the platform API token
165
148
  * (~/.config/vellum/platform-token). This is the canonical location
@@ -202,6 +185,18 @@ export function getPidPath(): string {
202
185
  return join(vellumRoot(), "vellum.pid");
203
186
  }
204
187
 
188
+ /**
189
+ * Returns the path to the runtime HTTP port file (~/.vellum/runtime-port).
190
+ * The daemon writes its active HTTP port here on startup so thin helpers
191
+ * that need to reach the runtime (e.g. the chrome-extension native messaging
192
+ * helper) can locate a non-default `RUNTIME_HTTP_PORT` without a manifest
193
+ * change. Root-level path by design — the file is read by helpers that may
194
+ * not know the workspace override path.
195
+ */
196
+ export function getRuntimePortFilePath(): string {
197
+ return join(vellumRoot(), "runtime-port");
198
+ }
199
+
205
200
  export function getDbPath(): string {
206
201
  return join(getDataDir(), "db", "assistant.db");
207
202
  }
@@ -1,7 +1,21 @@
1
+ import { homedir, userInfo } from "node:os";
2
+
1
3
  import type { TopLevelSnapshot } from "./top-level-scanner.js";
2
4
 
3
5
  export interface WorkspaceTopLevelRenderOptions {
4
6
  conversationAttachmentsPath?: string | null;
7
+ /**
8
+ * Host home directory on the client machine. When provided, takes
9
+ * precedence over the daemon's own `os.homedir()`. This matters for
10
+ * platform-managed (containerized) daemons where `os.homedir()` returns
11
+ * the container's home, not the user's actual Mac.
12
+ */
13
+ hostHomeDir?: string;
14
+ /**
15
+ * Host username on the client machine. When provided, takes precedence
16
+ * over the daemon's own `os.userInfo().username`. See `hostHomeDir`.
17
+ */
18
+ hostUsername?: string;
5
19
  }
6
20
 
7
21
  /**
@@ -19,11 +33,15 @@ export function renderWorkspaceTopLevelContext(
19
33
  lines.push(`Directories: ${snapshot.directories.join(", ")}`);
20
34
  lines.push(`Files: ${snapshot.files.join(", ")}`);
21
35
  if (options.conversationAttachmentsPath) {
22
- lines.push(`Current conversation attachments: ${options.conversationAttachmentsPath}`);
36
+ lines.push(
37
+ `Current conversation attachments: ${options.conversationAttachmentsPath}`,
38
+ );
23
39
  }
24
40
  if (snapshot.truncated) {
25
41
  lines.push("(list truncated — more entries exist)");
26
42
  }
43
+ lines.push(`Host home directory: ${options.hostHomeDir ?? homedir()}`);
44
+ lines.push(`Host username: ${options.hostUsername ?? userInfo().username}`);
27
45
  lines.push("</workspace>");
28
46
  return lines.join("\n");
29
47
  }
@@ -1,419 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- // ---------------------------------------------------------------------------
4
- // Mocks
5
- // ---------------------------------------------------------------------------
6
-
7
- // We need to mock child_process.spawn and execSync, and the global fetch /
8
- // WebSocket so tests don't need a real Chrome process.
9
-
10
- const spawnMock = mock(() => {
11
- const proc = { unref: mock(() => {}) };
12
- return proc;
13
- });
14
-
15
- const execSyncMock = mock(() => "");
16
-
17
- mock.module("node:child_process", () => ({
18
- spawn: spawnMock,
19
- execSync: execSyncMock,
20
- }));
21
-
22
- const {
23
- ensureChromeWithCdp,
24
- isCdpReady,
25
- minimizeChromeWindow,
26
- restoreChromeWindow,
27
- } = await import("../tools/browser/chrome-cdp.js");
28
-
29
- // Track fetch calls so we can assert readiness-check behavior
30
- let fetchImpl: (url: string | URL | Request) => Promise<Response>;
31
-
32
- const originalFetch = globalThis.fetch;
33
-
34
- /** Helper: a fetchImpl that simulates a CDP endpoint with page targets. */
35
- function cdpReadyFetch(url: string | URL | Request): Promise<Response> {
36
- const urlStr = String(url);
37
- if (urlStr.includes("/json/list")) {
38
- return Promise.resolve(
39
- new Response(JSON.stringify([{ type: "page" }]), { status: 200 }),
40
- );
41
- }
42
- return Promise.resolve(new Response("{}", { status: 200 }));
43
- }
44
-
45
- beforeEach(() => {
46
- // Default: CDP not ready
47
- fetchImpl = async () => {
48
- throw new Error("Connection refused");
49
- };
50
- globalThis.fetch = (async (
51
- input: string | URL | Request,
52
- _init?: RequestInit,
53
- ) => {
54
- return fetchImpl(input);
55
- }) as typeof globalThis.fetch;
56
- spawnMock.mockClear();
57
- execSyncMock.mockClear();
58
- });
59
-
60
- afterEach(() => {
61
- globalThis.fetch = originalFetch;
62
- });
63
-
64
- // ---------------------------------------------------------------------------
65
- // isCdpReady
66
- // ---------------------------------------------------------------------------
67
-
68
- describe("isCdpReady", () => {
69
- test("returns true when the endpoint responds with 200 and has page targets", async () => {
70
- fetchImpl = cdpReadyFetch;
71
- expect(await isCdpReady()).toBe(true);
72
- });
73
-
74
- test("returns false when fetch throws (connection refused)", async () => {
75
- fetchImpl = async () => {
76
- throw new Error("Connection refused");
77
- };
78
- expect(await isCdpReady()).toBe(false);
79
- });
80
-
81
- test("returns false when the endpoint responds with non-ok status", async () => {
82
- fetchImpl = async () => new Response("", { status: 500 });
83
- expect(await isCdpReady()).toBe(false);
84
- });
85
-
86
- test("returns false when CDP is up but has no page targets", async () => {
87
- fetchImpl = async (url) => {
88
- const urlStr = String(url);
89
- if (urlStr.includes("/json/list")) {
90
- return new Response("[]", { status: 200 });
91
- }
92
- return new Response("{}", { status: 200 });
93
- };
94
- expect(await isCdpReady()).toBe(false);
95
- });
96
-
97
- test("uses the provided base URL", async () => {
98
- const calledUrls: string[] = [];
99
- fetchImpl = async (url) => {
100
- const urlStr = String(url);
101
- calledUrls.push(urlStr);
102
- if (urlStr.includes("/json/list")) {
103
- return new Response(JSON.stringify([{ type: "page" }]), {
104
- status: 200,
105
- });
106
- }
107
- return new Response("{}", { status: 200 });
108
- };
109
- await isCdpReady("http://localhost:9333");
110
- expect(calledUrls.some((u) => u.startsWith("http://localhost:9333/"))).toBe(
111
- true,
112
- );
113
- });
114
- });
115
-
116
- // ---------------------------------------------------------------------------
117
- // ensureChromeWithCdp
118
- // ---------------------------------------------------------------------------
119
-
120
- describe("ensureChromeWithCdp", () => {
121
- test("returns immediately if CDP is already ready (launchedByUs=false)", async () => {
122
- fetchImpl = cdpReadyFetch;
123
- const session = await ensureChromeWithCdp();
124
- expect(session.launchedByUs).toBe(false);
125
- expect(session.baseUrl).toBe("http://localhost:9222");
126
- expect(spawnMock).not.toHaveBeenCalled();
127
- });
128
-
129
- test("spawns Chrome and retries when CDP is not initially ready", async () => {
130
- let callCount = 0;
131
- fetchImpl = async (url) => {
132
- callCount++;
133
- // First isCdpReady check fails (2 fetch calls: /json/version + /json/list).
134
- // Stale-check /json/version also fails.
135
- // After spawn, successive isCdpReady calls succeed on the 5th overall call.
136
- if (callCount >= 5) {
137
- const urlStr = String(url);
138
- if (urlStr.includes("/json/list")) {
139
- return new Response(JSON.stringify([{ type: "page" }]), {
140
- status: 200,
141
- });
142
- }
143
- return new Response("{}", { status: 200 });
144
- }
145
- throw new Error("Connection refused");
146
- };
147
-
148
- // Replace setTimeout with a zero-delay version to skip retry waits
149
- const origSetTimeout = globalThis.setTimeout;
150
- globalThis.setTimeout = ((
151
- fn: TimerHandler,
152
- _ms?: number,
153
- ...args: unknown[]
154
- ) => {
155
- return origSetTimeout(fn, 0, ...args);
156
- }) as typeof setTimeout;
157
- try {
158
- const session = await ensureChromeWithCdp({
159
- startUrl: "https://example.com/",
160
- });
161
- expect(session.launchedByUs).toBe(true);
162
- expect(session.baseUrl).toBe("http://localhost:9222");
163
- expect(spawnMock).toHaveBeenCalledTimes(1);
164
-
165
- // Verify spawn was called with the right Chrome path and args
166
- const spawnArgs = spawnMock.mock.calls[0] as unknown as [
167
- string,
168
- string[],
169
- ];
170
- expect(spawnArgs[0]).toContain("Google Chrome");
171
- const flags = spawnArgs[1];
172
- expect(flags).toContain("--remote-debugging-port=9222");
173
- expect(flags).toContain("--force-renderer-accessibility");
174
- expect(flags.some((f: string) => f.includes("Chrome-CDP"))).toBe(true);
175
- expect(flags).toContain("https://example.com/");
176
- } finally {
177
- globalThis.setTimeout = origSetTimeout;
178
- }
179
- });
180
-
181
- test("uses custom port when specified", async () => {
182
- fetchImpl = cdpReadyFetch;
183
- const session = await ensureChromeWithCdp({ port: 9333 });
184
- expect(session.baseUrl).toBe("http://localhost:9333");
185
- });
186
-
187
- test("uses custom userDataDir when specified", async () => {
188
- fetchImpl = cdpReadyFetch;
189
- const session = await ensureChromeWithCdp({
190
- userDataDir: "/tmp/test-chrome",
191
- });
192
- expect(session.userDataDir).toBe("/tmp/test-chrome");
193
- });
194
-
195
- test("throws after exhausting retries", async () => {
196
- // Never becomes ready
197
- fetchImpl = async () => {
198
- throw new Error("Connection refused");
199
- };
200
-
201
- // Replace setTimeout with a zero-delay version to skip retry waits
202
- const origSetTimeout = globalThis.setTimeout;
203
- globalThis.setTimeout = ((
204
- fn: TimerHandler,
205
- _ms?: number,
206
- ...args: unknown[]
207
- ) => {
208
- return origSetTimeout(fn, 0, ...args);
209
- }) as typeof setTimeout;
210
- try {
211
- const promise = ensureChromeWithCdp();
212
- await expect(promise).rejects.toThrow("CDP endpoint not responding");
213
- } finally {
214
- globalThis.setTimeout = origSetTimeout;
215
- }
216
- });
217
- });
218
-
219
- // ---------------------------------------------------------------------------
220
- // Window management (minimize / restore)
221
- // ---------------------------------------------------------------------------
222
-
223
- describe("minimizeChromeWindow", () => {
224
- test("does nothing when no page targets exist", async () => {
225
- fetchImpl = async () => new Response("[]", { status: 200 });
226
- // Should not throw
227
- await minimizeChromeWindow();
228
- });
229
-
230
- test("sends minimize command via WebSocket", async () => {
231
- // Track the WebSocket interactions
232
- const sentMessages: string[] = [];
233
- let wsOnOpen: (() => void) | undefined;
234
- let wsOnMessage: ((event: { data: string }) => void) | undefined;
235
-
236
- fetchImpl = async (url) => {
237
- const urlStr = String(url);
238
- if (urlStr.includes("/json/list")) {
239
- return new Response(
240
- JSON.stringify([
241
- {
242
- type: "page",
243
- webSocketDebuggerUrl: "ws://localhost:9222/devtools/page/ABC",
244
- },
245
- ]),
246
- );
247
- }
248
- return new Response("{}", { status: 200 });
249
- };
250
-
251
- const OriginalWebSocket = globalThis.WebSocket;
252
- globalThis.WebSocket = class MockWebSocket {
253
- constructor(_url: string) {}
254
- addEventListener(event: string, handler: (...args: unknown[]) => void) {
255
- if (event === "open") wsOnOpen = handler as () => void;
256
- if (event === "message")
257
- wsOnMessage = handler as (event: { data: string }) => void;
258
- }
259
- send(data: string) {
260
- sentMessages.push(data);
261
- const msg = JSON.parse(data);
262
- if (msg.method === "Browser.getWindowForTarget") {
263
- // Simulate response with windowId
264
- setTimeout(() => {
265
- wsOnMessage?.({
266
- data: JSON.stringify({ id: 1, result: { windowId: 42 } }),
267
- });
268
- }, 0);
269
- } else if (msg.method === "Browser.setWindowBounds") {
270
- setTimeout(() => {
271
- wsOnMessage?.({ data: JSON.stringify({ id: 2, result: {} }) });
272
- }, 0);
273
- }
274
- }
275
- close() {}
276
- } as unknown as typeof WebSocket;
277
-
278
- const promise = minimizeChromeWindow();
279
-
280
- // Trigger the open event
281
- await new Promise((r) => setTimeout(r, 10));
282
- wsOnOpen?.();
283
-
284
- await promise;
285
-
286
- // Verify the setWindowBounds call had windowState: "minimized"
287
- const boundsMsg = sentMessages.find((m) =>
288
- m.includes("Browser.setWindowBounds"),
289
- );
290
- expect(boundsMsg).toBeDefined();
291
- const parsed = JSON.parse(boundsMsg!);
292
- expect(parsed.params.bounds.windowState).toBe("minimized");
293
-
294
- globalThis.WebSocket = OriginalWebSocket;
295
- });
296
- });
297
-
298
- describe("setWindowState error handling", () => {
299
- test("rejects when Browser.setWindowBounds returns an error", async () => {
300
- let wsOnOpen: (() => void) | undefined;
301
- let wsOnMessage: ((event: { data: string }) => void) | undefined;
302
-
303
- fetchImpl = async (url) => {
304
- const urlStr = String(url);
305
- if (urlStr.includes("/json/list")) {
306
- return new Response(
307
- JSON.stringify([
308
- {
309
- type: "page",
310
- webSocketDebuggerUrl: "ws://localhost:9222/devtools/page/ABC",
311
- },
312
- ]),
313
- );
314
- }
315
- return new Response("{}", { status: 200 });
316
- };
317
-
318
- const OriginalWebSocket = globalThis.WebSocket;
319
- globalThis.WebSocket = class MockWebSocket {
320
- constructor(_url: string) {}
321
- addEventListener(event: string, handler: (...args: unknown[]) => void) {
322
- if (event === "open") wsOnOpen = handler as () => void;
323
- if (event === "message")
324
- wsOnMessage = handler as (event: { data: string }) => void;
325
- }
326
- send(data: string) {
327
- const msg = JSON.parse(data);
328
- if (msg.method === "Browser.getWindowForTarget") {
329
- setTimeout(() => {
330
- wsOnMessage?.({
331
- data: JSON.stringify({ id: 1, result: { windowId: 42 } }),
332
- });
333
- }, 0);
334
- } else if (msg.method === "Browser.setWindowBounds") {
335
- setTimeout(() => {
336
- wsOnMessage?.({
337
- data: JSON.stringify({
338
- id: 2,
339
- error: { message: "No window with given id" },
340
- }),
341
- });
342
- }, 0);
343
- }
344
- }
345
- close() {}
346
- } as unknown as typeof WebSocket;
347
-
348
- const promise = minimizeChromeWindow();
349
- await new Promise((r) => setTimeout(r, 10));
350
- wsOnOpen?.();
351
-
352
- await expect(promise).rejects.toThrow("Browser.setWindowBounds failed");
353
-
354
- globalThis.WebSocket = OriginalWebSocket;
355
- });
356
- });
357
-
358
- describe("restoreChromeWindow", () => {
359
- test("sends restore command via WebSocket", async () => {
360
- const sentMessages: string[] = [];
361
- let wsOnOpen: (() => void) | undefined;
362
- let wsOnMessage: ((event: { data: string }) => void) | undefined;
363
-
364
- fetchImpl = async (url) => {
365
- const urlStr = String(url);
366
- if (urlStr.includes("/json/list")) {
367
- return new Response(
368
- JSON.stringify([
369
- {
370
- type: "page",
371
- webSocketDebuggerUrl: "ws://localhost:9222/devtools/page/ABC",
372
- },
373
- ]),
374
- );
375
- }
376
- return new Response("{}", { status: 200 });
377
- };
378
-
379
- const OriginalWebSocket = globalThis.WebSocket;
380
- globalThis.WebSocket = class MockWebSocket {
381
- constructor(_url: string) {}
382
- addEventListener(event: string, handler: (...args: unknown[]) => void) {
383
- if (event === "open") wsOnOpen = handler as () => void;
384
- if (event === "message")
385
- wsOnMessage = handler as (event: { data: string }) => void;
386
- }
387
- send(data: string) {
388
- sentMessages.push(data);
389
- const msg = JSON.parse(data);
390
- if (msg.method === "Browser.getWindowForTarget") {
391
- setTimeout(() => {
392
- wsOnMessage?.({
393
- data: JSON.stringify({ id: 1, result: { windowId: 42 } }),
394
- });
395
- }, 0);
396
- } else if (msg.method === "Browser.setWindowBounds") {
397
- setTimeout(() => {
398
- wsOnMessage?.({ data: JSON.stringify({ id: 2, result: {} }) });
399
- }, 0);
400
- }
401
- }
402
- close() {}
403
- } as unknown as typeof WebSocket;
404
-
405
- const promise = restoreChromeWindow();
406
- await new Promise((r) => setTimeout(r, 10));
407
- wsOnOpen?.();
408
- await promise;
409
-
410
- const boundsMsg = sentMessages.find((m) =>
411
- m.includes("Browser.setWindowBounds"),
412
- );
413
- expect(boundsMsg).toBeDefined();
414
- const parsed = JSON.parse(boundsMsg!);
415
- expect(parsed.params.bounds.windowState).toBe("normal");
416
-
417
- globalThis.WebSocket = OriginalWebSocket;
418
- });
419
- });