@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
@@ -1,7 +1,13 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
1
+ import { afterEach, describe, expect, jest, test } from "bun:test";
2
2
 
3
3
  const { HostFileProxy } = await import("../daemon/host-file-proxy.js");
4
4
 
5
+ // Minimal PNG header
6
+ const PNG_HEADER = Buffer.from([
7
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49,
8
+ 0x48, 0x44, 0x52,
9
+ ]);
10
+
5
11
  describe("HostFileProxy", () => {
6
12
  let proxy: InstanceType<typeof HostFileProxy>;
7
13
  let sentMessages: unknown[];
@@ -77,6 +83,39 @@ describe("HostFileProxy", () => {
77
83
  expect(result.content).toContain("ENOENT");
78
84
  });
79
85
 
86
+ test("rebuilds image tool results from proxied image payloads", async () => {
87
+ setup();
88
+
89
+ const resultPromise = proxy.request(
90
+ {
91
+ operation: "read",
92
+ path: "/Users/test/Desktop/screenshot.png",
93
+ },
94
+ "session-1",
95
+ );
96
+
97
+ const sent = sentMessages[0] as Record<string, unknown>;
98
+ const requestId = sent.requestId as string;
99
+
100
+ proxy.resolve(requestId, {
101
+ content: "Image loaded on host",
102
+ isError: false,
103
+ imageData: PNG_HEADER.toString("base64"),
104
+ });
105
+
106
+ const result = await resultPromise;
107
+ expect(result.isError).toBe(false);
108
+ expect(result.content).toContain("Image loaded");
109
+ expect(result.content).toContain("/Users/test/Desktop/screenshot.png");
110
+ expect(result.contentBlocks).toHaveLength(1);
111
+ expect(result.contentBlocks?.[0]).toMatchObject({
112
+ type: "image",
113
+ source: {
114
+ media_type: "image/png",
115
+ },
116
+ });
117
+ });
118
+
80
119
  test("handles write operations", async () => {
81
120
  setup();
82
121
 
@@ -377,6 +416,151 @@ describe("HostFileProxy", () => {
377
416
  });
378
417
  });
379
418
 
419
+ describe("abort listener lifecycle", () => {
420
+ // Helper that wraps an AbortSignal to observe add/removeEventListener
421
+ // invocations without tripping over tsc's strict overload matching on
422
+ // AbortSignal itself.
423
+ type Spied = {
424
+ signal: AbortSignal;
425
+ addCalls: string[];
426
+ removeCalls: string[];
427
+ };
428
+ function spySignal(source: AbortSignal): Spied {
429
+ const addCalls: string[] = [];
430
+ const removeCalls: string[] = [];
431
+
432
+ const s = source as any;
433
+ const origAdd = source.addEventListener.bind(source);
434
+ const origRemove = source.removeEventListener.bind(source);
435
+ s.addEventListener = (
436
+ type: string,
437
+
438
+ ...rest: any[]
439
+ ) => {
440
+ addCalls.push(type);
441
+
442
+ return (origAdd as any)(type, ...rest);
443
+ };
444
+ s.removeEventListener = (
445
+ type: string,
446
+
447
+ ...rest: any[]
448
+ ) => {
449
+ removeCalls.push(type);
450
+
451
+ return (origRemove as any)(type, ...rest);
452
+ };
453
+ return { signal: source, addCalls, removeCalls };
454
+ }
455
+
456
+ test("removes abort listener from signal after resolve completes", async () => {
457
+ setup();
458
+ const controller = new AbortController();
459
+ const spy = spySignal(controller.signal);
460
+
461
+ const resultPromise = proxy.request(
462
+ { operation: "read", path: "/tmp/test.txt" },
463
+ "session-1",
464
+ spy.signal,
465
+ );
466
+
467
+ expect(spy.addCalls).toEqual(["abort"]);
468
+ expect(spy.removeCalls).toEqual([]);
469
+
470
+ const requestId = (sentMessages[0] as Record<string, unknown>)
471
+ .requestId as string;
472
+ proxy.resolve(requestId, { content: "file contents", isError: false });
473
+ await resultPromise;
474
+
475
+ // Listener is detached after normal completion.
476
+ expect(spy.removeCalls).toEqual(["abort"]);
477
+
478
+ // Subsequent aborts are harmless no-ops (no side effects on the proxy).
479
+ controller.abort();
480
+ // No additional emitted envelopes from the late abort.
481
+ expect(sentMessages).toHaveLength(1);
482
+ });
483
+
484
+ test("removes abort listener from signal on timer timeout", async () => {
485
+ setup();
486
+
487
+ jest.useFakeTimers();
488
+ try {
489
+ const controller = new AbortController();
490
+ const spy = spySignal(controller.signal);
491
+
492
+ const resultPromise = proxy.request(
493
+ { operation: "read", path: "/tmp/slow.txt" },
494
+ "session-1",
495
+ spy.signal,
496
+ );
497
+
498
+ expect(spy.addCalls).toEqual(["abort"]);
499
+ expect(spy.removeCalls).toEqual([]);
500
+
501
+ const requestId = (sentMessages[0] as Record<string, unknown>)
502
+ .requestId as string;
503
+ expect(proxy.hasPendingRequest(requestId)).toBe(true);
504
+
505
+ // Advance past the 30s internal timeout.
506
+ jest.advanceTimersByTime(31 * 1000);
507
+
508
+ const result = await resultPromise;
509
+ expect(result.isError).toBe(true);
510
+ expect(result.content).toContain("Host file proxy timed out");
511
+ expect(proxy.hasPendingRequest(requestId)).toBe(false);
512
+
513
+ // Listener is detached after the timer fires.
514
+ expect(spy.removeCalls).toEqual(["abort"]);
515
+
516
+ // Subsequent aborts should be harmless — no cancel emitted.
517
+ controller.abort();
518
+ expect(sentMessages).toHaveLength(1);
519
+ } finally {
520
+ jest.useRealTimers();
521
+ }
522
+ });
523
+ });
524
+
525
+ describe("sender throws synchronously", () => {
526
+ test("rejects the promise, clears pending state and timer, invokes onInternalResolve", async () => {
527
+ const resolvedIds: string[] = [];
528
+ sentMessages = [];
529
+ sendToClient = () => {
530
+ throw new Error("transport down");
531
+ };
532
+ proxy = new HostFileProxy(sendToClient, (id) => resolvedIds.push(id));
533
+
534
+ const resultPromise = proxy.request(
535
+ { operation: "read", path: "/tmp/test.txt" },
536
+ "session-1",
537
+ );
538
+
539
+ await expect(resultPromise).rejects.toThrow("transport down");
540
+
541
+ // The internal resolve should fire exactly once as part of cleanup.
542
+ expect(resolvedIds).toHaveLength(1);
543
+
544
+ // Issue a new request on a fresh (non-throwing) sender and verify
545
+ // the proxy is still functional — no stale timers or bookkeeping
546
+ // from the failed request.
547
+ sentMessages = [];
548
+ proxy.updateSender((msg) => sentMessages.push(msg), true);
549
+ const okPromise = proxy.request(
550
+ { operation: "read", path: "/tmp/ok.txt" },
551
+ "session-1",
552
+ );
553
+ expect(sentMessages).toHaveLength(1);
554
+ const okRequestId = (sentMessages[0] as Record<string, unknown>)
555
+ .requestId as string;
556
+ expect(proxy.hasPendingRequest(okRequestId)).toBe(true);
557
+ proxy.resolve(okRequestId, { content: "ok", isError: false });
558
+ const okResult = await okPromise;
559
+ expect(okResult.content).toBe("ok");
560
+ expect(okResult.isError).toBe(false);
561
+ });
562
+ });
563
+
380
564
  describe("onInternalResolve callback", () => {
381
565
  test("fires on abort", async () => {
382
566
  const resolvedIds: string[] = [];
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterEach, describe, expect, test } from "bun:test";
5
5
 
6
+ import type { HostFileInput } from "../daemon/host-file-proxy.js";
6
7
  import { hostFileReadTool } from "../tools/host-filesystem/read.js";
7
8
  import type { ToolContext } from "../tools/types.js";
8
9
 
@@ -165,6 +166,57 @@ describe("host_file_read tool", () => {
165
166
  });
166
167
 
167
168
  describe("host_file_read image support", () => {
169
+ test("uses host proxy for image reads when available", async () => {
170
+ const requests: Array<{
171
+ input: HostFileInput;
172
+ conversationId: string;
173
+ signal?: AbortSignal;
174
+ }> = [];
175
+ const proxyContext: ToolContext = {
176
+ ...makeContext(),
177
+ hostFileProxy: {
178
+ isAvailable: () => true,
179
+ request: async (input, conversationId, signal) => {
180
+ requests.push({ input, conversationId, signal });
181
+ return {
182
+ content: "Image loaded: /host/screenshot.png",
183
+ isError: false,
184
+ contentBlocks: [
185
+ {
186
+ type: "image",
187
+ source: {
188
+ type: "base64",
189
+ media_type: "image/png",
190
+ data: PNG_HEADER.toString("base64"),
191
+ },
192
+ },
193
+ ],
194
+ };
195
+ },
196
+ } as ToolContext["hostFileProxy"],
197
+ };
198
+
199
+ const result = await hostFileReadTool.execute(
200
+ { path: "/host/screenshot.png" },
201
+ proxyContext,
202
+ );
203
+
204
+ expect(result.isError).toBe(false);
205
+ expect(result.contentBlocks).toHaveLength(1);
206
+ expect(requests).toEqual([
207
+ {
208
+ input: {
209
+ operation: "read",
210
+ path: "/host/screenshot.png",
211
+ offset: undefined,
212
+ limit: undefined,
213
+ },
214
+ conversationId: "test-conversation",
215
+ signal: undefined,
216
+ },
217
+ ]);
218
+ });
219
+
168
220
  test("returns image content block for .png file", async () => {
169
221
  const dir = makeTempDir();
170
222
  const filePath = join(dir, "screenshot.png");
@@ -0,0 +1,165 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import type { HostProxyInterfaceId, InterfaceId } from "../channels/types.js";
4
+ import { supportsHostProxy } from "../channels/types.js";
5
+ import type {
6
+ ConversationTransportMetadata,
7
+ HostProxyTransportMetadata,
8
+ NonHostProxyTransportMetadata,
9
+ } from "../daemon/message-types/conversations.js";
10
+ import { isHostProxyTransport } from "../daemon/message-types/conversations.js";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // supportsHostProxy — runtime behavior
14
+ // ---------------------------------------------------------------------------
15
+
16
+ describe("supportsHostProxy (runtime)", () => {
17
+ test("no-arg form returns true for host-proxy interfaces", () => {
18
+ expect(supportsHostProxy("macos")).toBe(true);
19
+ });
20
+
21
+ test("no-arg form returns false for interfaces without host-proxy support", () => {
22
+ const nonHostProxyIds: InterfaceId[] = [
23
+ "ios",
24
+ "cli",
25
+ "telegram",
26
+ "phone",
27
+ "vellum",
28
+ "whatsapp",
29
+ "slack",
30
+ "email",
31
+ "chrome-extension",
32
+ ];
33
+ for (const id of nonHostProxyIds) {
34
+ expect(supportsHostProxy(id)).toBe(false);
35
+ }
36
+ });
37
+
38
+ test("capability form grants host_browser to chrome-extension", () => {
39
+ expect(supportsHostProxy("chrome-extension", "host_browser")).toBe(true);
40
+ expect(supportsHostProxy("chrome-extension", "host_bash")).toBe(false);
41
+ expect(supportsHostProxy("chrome-extension", "host_file")).toBe(false);
42
+ expect(supportsHostProxy("chrome-extension", "host_cu")).toBe(false);
43
+ });
44
+
45
+ test("capability form grants host_bash/file/cu to macOS but not host_browser", () => {
46
+ expect(supportsHostProxy("macos", "host_bash")).toBe(true);
47
+ expect(supportsHostProxy("macos", "host_file")).toBe(true);
48
+ expect(supportsHostProxy("macos", "host_cu")).toBe(true);
49
+ expect(supportsHostProxy("macos", "host_browser")).toBe(false);
50
+ });
51
+
52
+ test("capability form rejects everything for non-host-proxy interfaces", () => {
53
+ expect(supportsHostProxy("ios", "host_bash")).toBe(false);
54
+ expect(supportsHostProxy("cli", "host_file")).toBe(false);
55
+ expect(supportsHostProxy("telegram", "host_browser")).toBe(false);
56
+ });
57
+ });
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // supportsHostProxy — type predicate (compile-time contract)
61
+ // ---------------------------------------------------------------------------
62
+
63
+ describe("supportsHostProxy (type predicate)", () => {
64
+ test("no-arg form narrows InterfaceId to HostProxyInterfaceId", () => {
65
+ const id: InterfaceId = "macos";
66
+ if (supportsHostProxy(id)) {
67
+ // Inside this branch, TypeScript narrows `id` to HostProxyInterfaceId.
68
+ // If the overload were wrong, this assignment would fail to type-check
69
+ // and the test file wouldn't compile.
70
+ const narrowed: HostProxyInterfaceId = id;
71
+ expect(narrowed).toBe("macos");
72
+ } else {
73
+ throw new Error("expected narrowing branch to be taken for macos");
74
+ }
75
+ });
76
+
77
+ test("narrowing reaches through discriminated transport union", () => {
78
+ // Build a value typed as the full union so TypeScript can't cheat.
79
+ const transport: ConversationTransportMetadata = {
80
+ channelId: "vellum",
81
+ interfaceId: "macos",
82
+ hostHomeDir: "/Users/alice",
83
+ hostUsername: "alice",
84
+ };
85
+
86
+ if (transport.interfaceId && supportsHostProxy(transport.interfaceId)) {
87
+ // Narrowing the discriminant narrows the union member — after this
88
+ // check, `transport` should be HostProxyTransportMetadata and the
89
+ // host-env fields are directly accessible.
90
+ const narrowed: HostProxyTransportMetadata = transport;
91
+ expect(narrowed.hostHomeDir).toBe("/Users/alice");
92
+ expect(narrowed.hostUsername).toBe("alice");
93
+ } else {
94
+ throw new Error("expected host-proxy branch for macos transport");
95
+ }
96
+ });
97
+
98
+ test("non-host-proxy branch narrows to NonHostProxyTransportMetadata", () => {
99
+ const transport: ConversationTransportMetadata = {
100
+ channelId: "vellum",
101
+ interfaceId: "ios",
102
+ };
103
+
104
+ if (transport.interfaceId && supportsHostProxy(transport.interfaceId)) {
105
+ throw new Error("expected non-host-proxy branch for ios transport");
106
+ } else {
107
+ // `transport` is NonHostProxyTransportMetadata here.
108
+ const narrowed: NonHostProxyTransportMetadata = transport;
109
+ expect(narrowed.interfaceId).toBe("ios");
110
+ }
111
+ });
112
+ });
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // isHostProxyTransport — type guard on ConversationTransportMetadata
116
+ // ---------------------------------------------------------------------------
117
+
118
+ describe("isHostProxyTransport", () => {
119
+ test("returns true for macOS transport and narrows to HostProxyTransportMetadata", () => {
120
+ const transport: ConversationTransportMetadata = {
121
+ channelId: "vellum",
122
+ interfaceId: "macos",
123
+ hostHomeDir: "/Users/alice",
124
+ hostUsername: "alice",
125
+ };
126
+
127
+ expect(isHostProxyTransport(transport)).toBe(true);
128
+
129
+ if (isHostProxyTransport(transport)) {
130
+ const narrowed: HostProxyTransportMetadata = transport;
131
+ expect(narrowed.hostHomeDir).toBe("/Users/alice");
132
+ expect(narrowed.hostUsername).toBe("alice");
133
+ } else {
134
+ throw new Error("narrowing branch not taken");
135
+ }
136
+ });
137
+
138
+ test("returns false for every non-host-proxy interface", () => {
139
+ const nonHostProxyIds: Array<Exclude<InterfaceId, HostProxyInterfaceId>> = [
140
+ "ios",
141
+ "cli",
142
+ "telegram",
143
+ "phone",
144
+ "vellum",
145
+ "whatsapp",
146
+ "slack",
147
+ "email",
148
+ "chrome-extension",
149
+ ];
150
+ for (const interfaceId of nonHostProxyIds) {
151
+ const transport: ConversationTransportMetadata = {
152
+ channelId: "vellum",
153
+ interfaceId,
154
+ };
155
+ expect(isHostProxyTransport(transport)).toBe(false);
156
+ }
157
+ });
158
+
159
+ test("returns false when interfaceId is absent", () => {
160
+ const transport: ConversationTransportMetadata = {
161
+ channelId: "vellum",
162
+ };
163
+ expect(isHostProxyTransport(transport)).toBe(false);
164
+ });
165
+ });
@@ -36,7 +36,6 @@ const mockConfig = {
36
36
  entropyThreshold: 4.0,
37
37
  },
38
38
  auditLog: { retentionDays: 0 },
39
- sandbox: { enabled: true },
40
39
  };
41
40
 
42
41
  // Track whether wrapCommand was ever called — host_bash must never invoke it
@@ -149,10 +148,6 @@ describe("host_bash tool", () => {
149
148
  const dir = mkdtempSync(join(tmpdir(), "host-shell-plain-"));
150
149
  testDirs.push(dir);
151
150
 
152
- // Verify the tool executes successfully even when sandbox is enabled in config,
153
- // proving it bypasses the sandbox entirely
154
- expect(mockConfig.sandbox.enabled).toBe(true);
155
-
156
151
  spawnCalls.length = 0;
157
152
 
158
153
  const result = await hostShellTool.execute(
@@ -236,10 +231,7 @@ describe("host_bash — baseline: no sandbox isolation", () => {
236
231
  expect(spawnCalls[0].args[2]).toBe("ls -la /tmp");
237
232
  });
238
233
 
239
- test("sandbox config being enabled does not affect host_bash", async () => {
240
- // The mock config has sandbox.enabled = true
241
- expect(mockConfig.sandbox.enabled).toBe(true);
242
-
234
+ test("host_bash always spawns plain bash without wrapCommand", async () => {
243
235
  const dir = mkdtempSync(join(tmpdir(), "host-shell-sandbox-cfg-"));
244
236
  testDirs.push(dir);
245
237
 
@@ -255,9 +247,7 @@ describe("host_bash — baseline: no sandbox isolation", () => {
255
247
  );
256
248
 
257
249
  expect(result.isError).toBe(false);
258
- // Must never call wrapCommand regardless of config
259
250
  expect(wrapCommandCallCount).toBe(0);
260
- // Must still spawn plain bash
261
251
  expect(spawnCalls[0].command).toBe("bash");
262
252
  });
263
253
  });
@@ -178,6 +178,7 @@ function makeConversation(overrides: Record<string, unknown> = {}) {
178
178
  setTrustContext: () => {},
179
179
  updateClient: () => {},
180
180
  setHostBashProxy: () => {},
181
+ setHostBrowserProxy: () => {},
181
182
  setHostFileProxy: () => {},
182
183
  setHostCuProxy: () => {},
183
184
  addPreactivatedSkillId: () => {},
@@ -21,17 +21,16 @@ mock.module("../config/loader.js", () => ({
21
21
  }));
22
22
 
23
23
  mock.module("../oauth/oauth-store.js", () => ({
24
- isProviderConnected: (providerKey: string) =>
25
- connectedProviders.has(providerKey),
26
- getConnectionByProvider: (providerKey: string) =>
27
- connectedProviders.has(providerKey)
28
- ? { id: `conn-${providerKey}`, status: "active" }
24
+ isProviderConnected: (provider: string) => connectedProviders.has(provider),
25
+ getConnectionByProvider: (provider: string) =>
26
+ connectedProviders.has(provider)
27
+ ? { id: `conn-${provider}`, status: "active" }
29
28
  : undefined,
30
29
  }));
31
30
 
32
31
  /** Mark a provider as fully connected (active row + access token). */
33
- function setOAuthConnected(providerKey: string): void {
34
- connectedProviders.add(providerKey);
32
+ function setOAuthConnected(provider: string): void {
33
+ connectedProviders.add(provider);
35
34
  }
36
35
 
37
36
  const { getIntegrationSummary, formatIntegrationSummary, hasCapability } =
@@ -84,7 +84,11 @@ describe("handleListMessages tool_result merging", () => {
84
84
  conv.id,
85
85
  "user",
86
86
  JSON.stringify([
87
- { type: "tool_result", tool_use_id: "tu1", content: "file1.txt\nfile2.txt" },
87
+ {
88
+ type: "tool_result",
89
+ tool_use_id: "tu1",
90
+ content: "file1.txt\nfile2.txt",
91
+ },
88
92
  ]),
89
93
  );
90
94
 
@@ -117,7 +121,12 @@ describe("handleListMessages tool_result merging", () => {
117
121
  JSON.stringify([
118
122
  { type: "tool_use", id: "tu1", name: "bash", input: { command: "ls" } },
119
123
  { type: "text", text: "and also" },
120
- { type: "tool_use", id: "tu2", name: "file_read", input: { path: "/tmp/a" } },
124
+ {
125
+ type: "tool_use",
126
+ id: "tu2",
127
+ name: "file_read",
128
+ input: { path: "/tmp/a" },
129
+ },
121
130
  ]),
122
131
  );
123
132
  await addMessage(
@@ -176,7 +185,11 @@ describe("handleListMessages tool_result merging", () => {
176
185
  conv.id,
177
186
  "user",
178
187
  JSON.stringify([
179
- { type: "tool_result", tool_use_id: "tu_orphan", content: "stale result" },
188
+ {
189
+ type: "tool_result",
190
+ tool_use_id: "tu_orphan",
191
+ content: "stale result",
192
+ },
180
193
  ]),
181
194
  );
182
195
  await addMessage(
@@ -228,7 +241,12 @@ describe("handleListMessages tool_result merging", () => {
228
241
  "assistant",
229
242
  JSON.stringify([
230
243
  { type: "text", text: "Now reading:" },
231
- { type: "tool_use", id: "tu2", name: "file_read", input: { path: "/x" } },
244
+ {
245
+ type: "tool_use",
246
+ id: "tu2",
247
+ name: "file_read",
248
+ input: { path: "/x" },
249
+ },
232
250
  ]),
233
251
  );
234
252
  await addMessage(
@@ -248,17 +266,19 @@ describe("handleListMessages tool_result merging", () => {
248
266
  const response = handleListMessages(createTestUrl(conv.id), null);
249
267
  const body = (await response.json()) as { messages: MessagePayload[] };
250
268
 
251
- // user("list files"), assistant(bash), assistant(file_read), user("thanks")
252
- expect(body.messages).toHaveLength(4);
269
+ // Consecutive assistant messages are merged at query time so the client
270
+ // sees one grouped message (matching the streaming path behavior).
271
+ // user("list files"), merged-assistant(bash + file_read), user("thanks")
272
+ expect(body.messages).toHaveLength(3);
253
273
  expect(body.messages[0].role).toBe("user");
254
274
  expect(body.messages[1].role).toBe("assistant");
275
+ expect(body.messages[1].toolCalls).toHaveLength(2);
255
276
  expect(body.messages[1].toolCalls![0].name).toBe("bash");
256
277
  expect(body.messages[1].toolCalls![0].result).toBe("files");
257
- expect(body.messages[2].role).toBe("assistant");
258
- expect(body.messages[2].toolCalls![0].name).toBe("file_read");
259
- expect(body.messages[2].toolCalls![0].result).toBe("file data");
260
- expect(body.messages[3].role).toBe("user");
261
- expect(body.messages[3].content).toBe("thanks");
278
+ expect(body.messages[1].toolCalls![1].name).toBe("file_read");
279
+ expect(body.messages[1].toolCalls![1].result).toBe("file data");
280
+ expect(body.messages[2].role).toBe("user");
281
+ expect(body.messages[2].content).toBe("thanks");
262
282
  });
263
283
 
264
284
  test("tool_result with is_error propagates error status", async () => {
@@ -272,7 +292,12 @@ describe("handleListMessages tool_result merging", () => {
272
292
  conv.id,
273
293
  "assistant",
274
294
  JSON.stringify([
275
- { type: "tool_use", id: "tu1", name: "bash", input: { command: "fail" } },
295
+ {
296
+ type: "tool_use",
297
+ id: "tu1",
298
+ name: "bash",
299
+ input: { command: "fail" },
300
+ },
276
301
  ]),
277
302
  );
278
303
  await addMessage(
@@ -13,6 +13,7 @@ mock.module("../inbound/platform-callback-registration.js", () => ({
13
13
  }));
14
14
 
15
15
  const { McpClient } = await import("../mcp/client.js");
16
+ const { McpOAuthProvider } = await import("../mcp/mcp-oauth-provider.js");
16
17
 
17
18
  /**
18
19
  * Mimics the SDK's StreamableHTTPError which has a `.code` property
@@ -67,7 +68,7 @@ describe("McpClient auth error detection", () => {
67
68
  expect(client.isConnected).toBe(false);
68
69
  });
69
70
 
70
- test("rethrows non-auth StreamableHTTPError for HTTP transports", async () => {
71
+ test("swallows non-auth StreamableHTTPError (connect never throws)", async () => {
71
72
  const client = new McpClient("test-server");
72
73
 
73
74
  (client as any).createTransport = () => ({});
@@ -78,9 +79,9 @@ describe("McpClient auth error detection", () => {
78
79
  close: async () => {},
79
80
  };
80
81
 
81
- await expect(client.connect(httpTransport)).rejects.toThrow(
82
- "Internal Server Error",
83
- );
82
+ // Non-auth errors are logged but never propagated — daemon keeps running
83
+ await client.connect(httpTransport);
84
+ expect(client.isConnected).toBe(false);
84
85
  });
85
86
 
86
87
  test("treats error message containing 'unauthorized' as auth error", async () => {
@@ -97,4 +98,39 @@ describe("McpClient auth error detection", () => {
97
98
  await client.connect(httpTransport);
98
99
  expect(client.isConnected).toBe(false);
99
100
  });
101
+
102
+ test("treats SDK fetchToken 'authorizationCode is required' error as auth error", async () => {
103
+ const client = new McpClient("test-server");
104
+
105
+ (client as any).createTransport = () => ({});
106
+ (client as any).client = {
107
+ connect: () => {
108
+ throw new Error(
109
+ "Either provider.prepareTokenRequest() or authorizationCode is required",
110
+ );
111
+ },
112
+ close: async () => {},
113
+ };
114
+
115
+ await client.connect(httpTransport);
116
+ expect(client.isConnected).toBe(false);
117
+ });
118
+ });
119
+
120
+ describe("McpOAuthProvider redirectUrl", () => {
121
+ test("redirectUrl is undefined until startCallbackServer() is called", () => {
122
+ const nonInteractive = new McpOAuthProvider(
123
+ "test-server",
124
+ "https://example.com/mcp",
125
+ /* interactive */ false,
126
+ );
127
+ expect(nonInteractive.redirectUrl).toBeUndefined();
128
+
129
+ const interactive = new McpOAuthProvider(
130
+ "test-server",
131
+ "https://example.com/mcp",
132
+ /* interactive */ true,
133
+ );
134
+ expect(interactive.redirectUrl).toBeUndefined();
135
+ });
100
136
  });