@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
@@ -0,0 +1,161 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { v4 as uuid } from "uuid";
4
+
5
+ mock.module("../util/logger.js", () => ({
6
+ getLogger: () =>
7
+ new Proxy({} as Record<string, unknown>, {
8
+ get: () => () => {},
9
+ }),
10
+ }));
11
+
12
+ import { maybeEnqueueConversationStartersJob } from "../memory/conversation-starters-cadence.js";
13
+ import { getSqlite, initializeDb } from "../memory/db.js";
14
+
15
+ initializeDb();
16
+
17
+ function clearTables() {
18
+ getSqlite().run("DELETE FROM memory_graph_nodes");
19
+ getSqlite().run("DELETE FROM memory_jobs");
20
+ getSqlite().run("DELETE FROM memory_checkpoints");
21
+ }
22
+
23
+ function insertMemoryNode(scopeId = "default") {
24
+ const now = Date.now();
25
+ getSqlite().run(
26
+ `INSERT INTO memory_graph_nodes (
27
+ id, content, type, created, last_accessed, last_consolidated,
28
+ emotional_charge, fidelity, confidence, significance,
29
+ stability, reinforcement_count, last_reinforced,
30
+ source_conversations, source_type, scope_id
31
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'vivid', 0.8, 0.5, 14, 0, ?, '[]', 'inferred', ?)`,
32
+ [
33
+ uuid(),
34
+ "test statement",
35
+ "semantic",
36
+ now,
37
+ now,
38
+ now,
39
+ '{"valence":0,"intensity":0.1,"decayCurve":"linear","decayRate":0.05,"originalIntensity":0.1}',
40
+ now,
41
+ scopeId,
42
+ ],
43
+ );
44
+ }
45
+
46
+ function setCheckpoint(key: string, value: string) {
47
+ getSqlite().run(
48
+ `INSERT OR REPLACE INTO memory_checkpoints (key, value, updated_at) VALUES (?, ?, ?)`,
49
+ [key, value, Date.now()],
50
+ );
51
+ }
52
+
53
+ function getPendingJobs(): Array<{ type: string }> {
54
+ return getSqlite()
55
+ .prepare(
56
+ `SELECT type FROM memory_jobs WHERE type = 'generate_conversation_starters' AND status = 'pending'`,
57
+ )
58
+ .all() as Array<{ type: string }>;
59
+ }
60
+
61
+ beforeEach(() => {
62
+ clearTables();
63
+ });
64
+
65
+ describe("maybeEnqueueConversationStartersJob", () => {
66
+ test("no-op when zero memory nodes", () => {
67
+ maybeEnqueueConversationStartersJob("default");
68
+ expect(getPendingJobs()).toHaveLength(0);
69
+ });
70
+
71
+ test("enqueues when threshold exceeded (<=10 nodes, threshold=1)", () => {
72
+ insertMemoryNode();
73
+ insertMemoryNode();
74
+
75
+ maybeEnqueueConversationStartersJob("default");
76
+ expect(getPendingJobs()).toHaveLength(1);
77
+ });
78
+
79
+ test("no-op when delta below threshold", () => {
80
+ for (let i = 0; i < 5; i++) insertMemoryNode();
81
+
82
+ // Set checkpoint to current count — no new items since last gen
83
+ setCheckpoint("conversation_starters:item_count_at_last_gen:default", "5");
84
+
85
+ maybeEnqueueConversationStartersJob("default");
86
+ expect(getPendingJobs()).toHaveLength(0);
87
+ });
88
+
89
+ test("uses higher threshold for larger memory counts (>50 nodes, threshold=10)", () => {
90
+ for (let i = 0; i < 55; i++) insertMemoryNode();
91
+
92
+ // Set checkpoint so delta is only 4 (below threshold of 10)
93
+ setCheckpoint(
94
+ "conversation_starters:item_count_at_last_gen:default",
95
+ "51",
96
+ );
97
+
98
+ maybeEnqueueConversationStartersJob("default");
99
+ expect(getPendingJobs()).toHaveLength(0);
100
+
101
+ // Add more to exceed threshold
102
+ for (let i = 0; i < 6; i++) insertMemoryNode();
103
+
104
+ maybeEnqueueConversationStartersJob("default");
105
+ expect(getPendingJobs()).toHaveLength(1);
106
+ });
107
+
108
+ test("dedup prevents double-enqueue", () => {
109
+ insertMemoryNode();
110
+ insertMemoryNode();
111
+
112
+ maybeEnqueueConversationStartersJob("default");
113
+ expect(getPendingJobs()).toHaveLength(1);
114
+
115
+ // Call again — should not create a second job
116
+ maybeEnqueueConversationStartersJob("default");
117
+ expect(getPendingJobs()).toHaveLength(1);
118
+ });
119
+
120
+ test("enqueues when active memory count drops below the last-generation checkpoint", () => {
121
+ // Start with 5 nodes and set checkpoint to 5
122
+ for (let i = 0; i < 5; i++) insertMemoryNode();
123
+ setCheckpoint("conversation_starters:item_count_at_last_gen:default", "5");
124
+
125
+ // Simulate pruning: mark 3 nodes as gone (reducing totalActive to 2)
126
+ const ids = (
127
+ getSqlite()
128
+ .prepare(`SELECT id FROM memory_graph_nodes LIMIT 3`)
129
+ .all() as Array<{ id: string }>
130
+ ).map((r) => r.id);
131
+ for (const id of ids) {
132
+ getSqlite().run(
133
+ `UPDATE memory_graph_nodes SET fidelity = 'gone' WHERE id = ?`,
134
+ [id],
135
+ );
136
+ }
137
+
138
+ // The checkpoint is now ahead of the active memory count. This should
139
+ // enqueue a refresh immediately so stale starters can recover.
140
+ maybeEnqueueConversationStartersJob("default");
141
+ expect(getPendingJobs()).toHaveLength(1);
142
+ });
143
+
144
+ test("scopes are independent", () => {
145
+ insertMemoryNode("scope-a");
146
+ insertMemoryNode("scope-b");
147
+
148
+ maybeEnqueueConversationStartersJob("scope-a");
149
+ maybeEnqueueConversationStartersJob("scope-b");
150
+
151
+ const jobs = getSqlite()
152
+ .prepare(
153
+ `SELECT payload FROM memory_jobs WHERE type = 'generate_conversation_starters' AND status = 'pending'`,
154
+ )
155
+ .all() as Array<{ payload: string }>;
156
+
157
+ expect(jobs).toHaveLength(2);
158
+ const payloads = jobs.map((j) => JSON.parse(j.payload).scopeId).sort();
159
+ expect(payloads).toEqual(["scope-a", "scope-b"]);
160
+ });
161
+ });
@@ -1,5 +1,8 @@
1
+ import { Database } from "bun:sqlite";
1
2
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
3
 
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
3
6
  mock.module("../util/logger.js", () => ({
4
7
  getLogger: () =>
5
8
  new Proxy({} as Record<string, unknown>, {
@@ -19,16 +22,28 @@ import {
19
22
  createConversation,
20
23
  deleteLastExchange,
21
24
  getConversation,
25
+ getConversationHostAccess,
22
26
  getConversationMemoryScopeId,
23
27
  getConversationType,
24
28
  getMessages,
29
+ updateConversationHostAccess,
25
30
  } from "../memory/conversation-crud.js";
26
31
  import { isLastUserMessageToolResult } from "../memory/conversation-queries.js";
27
32
  import { getDb, initializeDb } from "../memory/db.js";
33
+ import { getSqliteFrom } from "../memory/db-connection.js";
34
+ import { migrateConversationHostAccess } from "../memory/migrations/217-conversation-host-access.js";
35
+ import * as schema from "../memory/schema.js";
28
36
 
29
37
  // Initialize db once before all tests
30
38
  initializeDb();
31
39
 
40
+ function createMigrationTestDb() {
41
+ const sqlite = new Database(":memory:");
42
+ sqlite.exec("PRAGMA journal_mode=WAL");
43
+ sqlite.exec("PRAGMA foreign_keys = ON");
44
+ return drizzle(sqlite, { schema });
45
+ }
46
+
32
47
  describe("deleteLastExchange", () => {
33
48
  beforeEach(() => {
34
49
  // Reset database between tests by dropping and recreating tables
@@ -406,6 +421,7 @@ describe("conversation metadata defaults", () => {
406
421
  expect(loaded).not.toBeNull();
407
422
  expect(loaded!.conversationType).toBe("standard");
408
423
  expect(loaded!.memoryScopeId).toBe("default");
424
+ expect(loaded!.hostAccess).toBe(0);
409
425
  });
410
426
 
411
427
  test("existing conversations without explicit values get defaults via migration", () => {
@@ -422,6 +438,7 @@ describe("conversation metadata defaults", () => {
422
438
  expect(loaded).not.toBeNull();
423
439
  expect(loaded!.conversationType).toBe("standard");
424
440
  expect(loaded!.memoryScopeId).toBe("default");
441
+ expect(loaded!.hostAccess).toBe(0);
425
442
  });
426
443
  });
427
444
 
@@ -506,6 +523,184 @@ describe("conversation metadata read helpers", () => {
506
523
  test("getConversationMemoryScopeId returns default for missing conversation", () => {
507
524
  expect(getConversationMemoryScopeId("nonexistent-id")).toBe("default");
508
525
  });
526
+
527
+ test("getConversationHostAccess returns false by default", () => {
528
+ const conv = createConversation("test");
529
+ expect(getConversationHostAccess(conv.id)).toBe(false);
530
+ });
531
+
532
+ test("getConversationHostAccess returns false for missing conversation", () => {
533
+ expect(getConversationHostAccess("nonexistent-id")).toBe(false);
534
+ });
535
+ });
536
+
537
+ describe("conversation host access persistence", () => {
538
+ beforeEach(() => {
539
+ const db = getDb();
540
+ db.run(`DELETE FROM messages`);
541
+ db.run(`DELETE FROM conversations`);
542
+ });
543
+
544
+ test("new conversations default host access to disabled", () => {
545
+ const conv = createConversation("test");
546
+ const loaded = getConversation(conv.id);
547
+
548
+ expect(conv.hostAccess).toBe(0);
549
+ expect(loaded).not.toBeNull();
550
+ expect(loaded!.hostAccess).toBe(0);
551
+ expect(getConversationHostAccess(conv.id)).toBe(false);
552
+ });
553
+
554
+ test("updateConversationHostAccess persists mutations", () => {
555
+ const conv = createConversation("test");
556
+
557
+ updateConversationHostAccess(conv.id, true);
558
+ expect(getConversationHostAccess(conv.id)).toBe(true);
559
+ expect(getConversation(conv.id)?.hostAccess).toBe(1);
560
+
561
+ updateConversationHostAccess(conv.id, false);
562
+ expect(getConversationHostAccess(conv.id)).toBe(false);
563
+ expect(getConversation(conv.id)?.hostAccess).toBe(0);
564
+ });
565
+ });
566
+
567
+ describe("conversation host access migration", () => {
568
+ function bootstrapPreHostAccessConversations(raw: Database): void {
569
+ raw.exec(/*sql*/ `
570
+ CREATE TABLE memory_checkpoints (
571
+ key TEXT PRIMARY KEY,
572
+ value TEXT NOT NULL,
573
+ updated_at INTEGER NOT NULL
574
+ )
575
+ `);
576
+
577
+ raw.exec(/*sql*/ `
578
+ CREATE TABLE conversations (
579
+ id TEXT PRIMARY KEY,
580
+ title TEXT,
581
+ created_at INTEGER NOT NULL,
582
+ updated_at INTEGER NOT NULL,
583
+ total_input_tokens INTEGER NOT NULL DEFAULT 0,
584
+ total_output_tokens INTEGER NOT NULL DEFAULT 0,
585
+ total_estimated_cost REAL NOT NULL DEFAULT 0,
586
+ context_summary TEXT,
587
+ context_compacted_message_count INTEGER NOT NULL DEFAULT 0,
588
+ context_compacted_at INTEGER,
589
+ conversation_type TEXT NOT NULL DEFAULT 'standard',
590
+ source TEXT NOT NULL DEFAULT 'user',
591
+ memory_scope_id TEXT NOT NULL DEFAULT 'default',
592
+ origin_channel TEXT,
593
+ origin_interface TEXT,
594
+ fork_parent_conversation_id TEXT,
595
+ fork_parent_message_id TEXT,
596
+ is_auto_title INTEGER NOT NULL DEFAULT 1,
597
+ schedule_job_id TEXT,
598
+ last_message_at INTEGER
599
+ )
600
+ `);
601
+ }
602
+
603
+ test("migration adds host access with disabled default for existing rows", () => {
604
+ const db = createMigrationTestDb();
605
+ const raw = getSqliteFrom(db);
606
+ const now = Date.now();
607
+
608
+ bootstrapPreHostAccessConversations(raw);
609
+ raw.exec(/*sql*/ `
610
+ INSERT INTO conversations (
611
+ id,
612
+ title,
613
+ created_at,
614
+ updated_at,
615
+ conversation_type,
616
+ source,
617
+ memory_scope_id,
618
+ is_auto_title
619
+ ) VALUES (
620
+ 'conv-upgrade',
621
+ 'Existing conversation',
622
+ ${now},
623
+ ${now},
624
+ 'standard',
625
+ 'user',
626
+ 'default',
627
+ 1
628
+ )
629
+ `);
630
+
631
+ migrateConversationHostAccess(db);
632
+
633
+ const row = raw
634
+ .query(
635
+ `SELECT id, title, host_access FROM conversations WHERE id = 'conv-upgrade'`,
636
+ )
637
+ .get() as {
638
+ id: string;
639
+ title: string | null;
640
+ host_access: number;
641
+ } | null;
642
+
643
+ expect(row).toEqual({
644
+ id: "conv-upgrade",
645
+ title: "Existing conversation",
646
+ host_access: 0,
647
+ });
648
+
649
+ const checkpoint = raw
650
+ .query(
651
+ `SELECT value FROM memory_checkpoints WHERE key = 'migration_conversation_host_access_v1'`,
652
+ )
653
+ .get() as { value: string } | null;
654
+ expect(checkpoint?.value).toBe("1");
655
+ });
656
+
657
+ test("re-running the migration preserves existing host access values", () => {
658
+ const db = createMigrationTestDb();
659
+ const raw = getSqliteFrom(db);
660
+ const now = Date.now();
661
+
662
+ bootstrapPreHostAccessConversations(raw);
663
+ raw.exec(/*sql*/ `
664
+ INSERT INTO conversations (
665
+ id,
666
+ title,
667
+ created_at,
668
+ updated_at,
669
+ conversation_type,
670
+ source,
671
+ memory_scope_id,
672
+ is_auto_title
673
+ ) VALUES (
674
+ 'conv-rerun',
675
+ 'Existing conversation',
676
+ ${now},
677
+ ${now},
678
+ 'standard',
679
+ 'user',
680
+ 'default',
681
+ 1
682
+ )
683
+ `);
684
+
685
+ migrateConversationHostAccess(db);
686
+ raw.exec(
687
+ `UPDATE conversations SET host_access = 1 WHERE id = 'conv-rerun'`,
688
+ );
689
+
690
+ expect(() => migrateConversationHostAccess(db)).not.toThrow();
691
+
692
+ const row = raw
693
+ .query(`SELECT host_access FROM conversations WHERE id = 'conv-rerun'`)
694
+ .get() as { host_access: number } | null;
695
+ const checkpoint = raw
696
+ .query(
697
+ `SELECT value FROM memory_checkpoints WHERE key = 'migration_conversation_host_access_v1'`,
698
+ )
699
+ .get() as { value: string } | null;
700
+
701
+ expect(row).toEqual({ host_access: 1 });
702
+ expect(checkpoint?.value).toBe("1");
703
+ });
509
704
  });
510
705
 
511
706
  // ---------------------------------------------------------------------------
@@ -269,6 +269,199 @@ describe("Conversation workspace cache state", () => {
269
269
  expect(conversation.isWorkspaceTopLevelDirty()).toBe(false);
270
270
  });
271
271
 
272
+ test("renders client-reported host env when set on the conversation", () => {
273
+ conversation.hostHomeDir = "/Users/alice";
274
+ conversation.hostUsername = "alice";
275
+ conversation.refreshWorkspaceTopLevelContextIfNeeded();
276
+
277
+ const block = conversation.getWorkspaceTopLevelContext();
278
+ expect(block).not.toBeNull();
279
+ expect(block!).toContain("Host home directory: /Users/alice");
280
+ expect(block!).toContain("Host username: alice");
281
+ });
282
+
283
+ test("falls back to daemon os info when client host env is absent", async () => {
284
+ const { homedir, userInfo } = await import("node:os");
285
+ conversation.refreshWorkspaceTopLevelContextIfNeeded();
286
+
287
+ const block = conversation.getWorkspaceTopLevelContext();
288
+ expect(block).not.toBeNull();
289
+ expect(block!).toContain(`Host home directory: ${homedir()}`);
290
+ expect(block!).toContain(`Host username: ${userInfo().username}`);
291
+ });
292
+
293
+ test("re-renders with updated host env after marking dirty", () => {
294
+ conversation.hostHomeDir = "/Users/alice";
295
+ conversation.hostUsername = "alice";
296
+ conversation.refreshWorkspaceTopLevelContextIfNeeded();
297
+ expect(conversation.getWorkspaceTopLevelContext()!).toContain(
298
+ "Host home directory: /Users/alice",
299
+ );
300
+
301
+ conversation.hostHomeDir = "/Users/bob";
302
+ conversation.hostUsername = "bob";
303
+ conversation.markWorkspaceTopLevelDirty();
304
+ conversation.refreshWorkspaceTopLevelContextIfNeeded();
305
+
306
+ const block = conversation.getWorkspaceTopLevelContext();
307
+ expect(block).not.toBeNull();
308
+ expect(block!).toContain("Host home directory: /Users/bob");
309
+ expect(block!).toContain("Host username: bob");
310
+ expect(block!).not.toContain("Host home directory: /Users/alice");
311
+ expect(block!).not.toContain("Host username: alice");
312
+ });
313
+
314
+ test("falls back to os info after clearing macOS host env (cross-interface reuse)", async () => {
315
+ const { homedir, userInfo } = await import("node:os");
316
+
317
+ // Simulate a macOS turn populating host env.
318
+ conversation.hostHomeDir = "/Users/alice";
319
+ conversation.hostUsername = "alice";
320
+ conversation.refreshWorkspaceTopLevelContextIfNeeded();
321
+ expect(conversation.getWorkspaceTopLevelContext()!).toContain(
322
+ "Host home directory: /Users/alice",
323
+ );
324
+
325
+ // Simulate a subsequent non-macOS turn (iOS, CLI, channel) on the same
326
+ // conversation clearing the host env — without the clear, the stale
327
+ // macOS paths would leak into the next render.
328
+ conversation.hostHomeDir = undefined;
329
+ conversation.hostUsername = undefined;
330
+ conversation.markWorkspaceTopLevelDirty();
331
+ conversation.refreshWorkspaceTopLevelContextIfNeeded();
332
+
333
+ const block = conversation.getWorkspaceTopLevelContext();
334
+ expect(block).not.toBeNull();
335
+ expect(block!).toContain(`Host home directory: ${homedir()}`);
336
+ expect(block!).toContain(`Host username: ${userInfo().username}`);
337
+ expect(block!).not.toContain("Host home directory: /Users/alice");
338
+ expect(block!).not.toContain("Host username: alice");
339
+ });
340
+
341
+ // -------------------------------------------------------------------------
342
+ // applyHostEnvFromTransport — capability-gated setter
343
+ // -------------------------------------------------------------------------
344
+
345
+ test("applyHostEnvFromTransport populates fields for host-proxy transports", () => {
346
+ conversation.applyHostEnvFromTransport({
347
+ channelId: "vellum",
348
+ interfaceId: "macos",
349
+ hostHomeDir: "/Users/alice",
350
+ hostUsername: "alice",
351
+ });
352
+
353
+ expect(conversation.hostHomeDir).toBe("/Users/alice");
354
+ expect(conversation.hostUsername).toBe("alice");
355
+ expect(conversation.isWorkspaceTopLevelDirty()).toBe(true);
356
+
357
+ conversation.refreshWorkspaceTopLevelContextIfNeeded();
358
+ const block = conversation.getWorkspaceTopLevelContext();
359
+ expect(block!).toContain("Host home directory: /Users/alice");
360
+ expect(block!).toContain("Host username: alice");
361
+ });
362
+
363
+ test("applyHostEnvFromTransport clears fields for non-host-proxy transports", () => {
364
+ // Seed with a host-proxy turn.
365
+ conversation.applyHostEnvFromTransport({
366
+ channelId: "vellum",
367
+ interfaceId: "macos",
368
+ hostHomeDir: "/Users/alice",
369
+ hostUsername: "alice",
370
+ });
371
+ expect(conversation.hostHomeDir).toBe("/Users/alice");
372
+
373
+ // Apply a non-host-proxy transport — should clear the stored values so
374
+ // the next render doesn't leak them from a cross-interface reuse.
375
+ conversation.applyHostEnvFromTransport({
376
+ channelId: "vellum",
377
+ interfaceId: "ios",
378
+ });
379
+
380
+ expect(conversation.hostHomeDir).toBeUndefined();
381
+ expect(conversation.hostUsername).toBeUndefined();
382
+ expect(conversation.isWorkspaceTopLevelDirty()).toBe(true);
383
+ });
384
+
385
+ test("applyHostEnvFromTransport clears fields for chrome-extension (browser-only)", () => {
386
+ // chrome-extension supports only host_browser — the no-arg supportsHostProxy
387
+ // returns false for it, so the gate treats it as a non-host-proxy transport
388
+ // for the purposes of host env (no local filesystem to address).
389
+ conversation.applyHostEnvFromTransport({
390
+ channelId: "vellum",
391
+ interfaceId: "macos",
392
+ hostHomeDir: "/Users/alice",
393
+ hostUsername: "alice",
394
+ });
395
+
396
+ conversation.applyHostEnvFromTransport({
397
+ channelId: "vellum",
398
+ interfaceId: "chrome-extension",
399
+ });
400
+
401
+ expect(conversation.hostHomeDir).toBeUndefined();
402
+ expect(conversation.hostUsername).toBeUndefined();
403
+ });
404
+
405
+ test("applyHostEnvFromTransport handles transport with no interfaceId", () => {
406
+ // Seed and then apply a transport without an interfaceId (legacy/channel
407
+ // paths may omit it). The gate should clear any stored host env.
408
+ conversation.applyHostEnvFromTransport({
409
+ channelId: "vellum",
410
+ interfaceId: "macos",
411
+ hostHomeDir: "/Users/alice",
412
+ hostUsername: "alice",
413
+ });
414
+
415
+ conversation.applyHostEnvFromTransport({
416
+ channelId: "vellum",
417
+ });
418
+
419
+ expect(conversation.hostHomeDir).toBeUndefined();
420
+ expect(conversation.hostUsername).toBeUndefined();
421
+ });
422
+
423
+ test("applyHostEnvFromTransport does not mark dirty when values are unchanged", () => {
424
+ conversation.applyHostEnvFromTransport({
425
+ channelId: "vellum",
426
+ interfaceId: "macos",
427
+ hostHomeDir: "/Users/alice",
428
+ hostUsername: "alice",
429
+ });
430
+ // Render once so the dirty flag clears.
431
+ conversation.refreshWorkspaceTopLevelContextIfNeeded();
432
+ expect(conversation.isWorkspaceTopLevelDirty()).toBe(false);
433
+
434
+ // Re-apply the same values — dirty flag should remain false so we don't
435
+ // thrash the cached workspace block on every message.
436
+ conversation.applyHostEnvFromTransport({
437
+ channelId: "vellum",
438
+ interfaceId: "macos",
439
+ hostHomeDir: "/Users/alice",
440
+ hostUsername: "alice",
441
+ });
442
+ expect(conversation.isWorkspaceTopLevelDirty()).toBe(false);
443
+ });
444
+
445
+ test("applyHostEnvFromTransport marks dirty when macOS values change", () => {
446
+ conversation.applyHostEnvFromTransport({
447
+ channelId: "vellum",
448
+ interfaceId: "macos",
449
+ hostHomeDir: "/Users/alice",
450
+ hostUsername: "alice",
451
+ });
452
+ conversation.refreshWorkspaceTopLevelContextIfNeeded();
453
+ expect(conversation.isWorkspaceTopLevelDirty()).toBe(false);
454
+
455
+ // New values — should mark dirty so the next render picks them up.
456
+ conversation.applyHostEnvFromTransport({
457
+ channelId: "vellum",
458
+ interfaceId: "macos",
459
+ hostHomeDir: "/Users/bob",
460
+ hostUsername: "bob",
461
+ });
462
+ expect(conversation.isWorkspaceTopLevelDirty()).toBe(true);
463
+ });
464
+
272
465
  test("workspace hints follow the resolved legacy directory when canonical is absent", () => {
273
466
  const workspaceRoot = mkdtempSync(
274
467
  join(tmpdir(), "conversation-workspace-cache-state-"),
@@ -12,7 +12,7 @@
12
12
  * 7. Error handling: record_grant RPC failure returns error outcome.
13
13
  */
14
14
 
15
- import { describe, expect, test } from "bun:test";
15
+ import { beforeEach, describe, expect, test } from "bun:test";
16
16
 
17
17
  import type {
18
18
  ApprovalRequired,
@@ -22,6 +22,7 @@ import type {
22
22
  RecordGrantResponse,
23
23
  } from "@vellumai/ces-contracts";
24
24
 
25
+ import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
25
26
  import { bridgeCesApproval } from "../credential-execution/approval-bridge.js";
26
27
  import type { CesClient } from "../credential-execution/client.js";
27
28
  import type { PermissionPrompter } from "../permissions/prompter.js";
@@ -166,6 +167,36 @@ function makeCesClient(
166
167
  // ---------------------------------------------------------------------------
167
168
 
168
169
  describe("CES approval bridge", () => {
170
+ beforeEach(() => {
171
+ _setOverridesForTesting({});
172
+ });
173
+
174
+ test("suppresses deterministic CES approval prompts and auto-allows under v2", async () => {
175
+ _setOverridesForTesting({ "permission-controls-v2": true });
176
+
177
+ const prompter = makePrompter("allow");
178
+ const cesClient = makeCesClient();
179
+
180
+ const result = await bridgeCesApproval(
181
+ makeApprovalRequired(),
182
+ prompter,
183
+ cesClient,
184
+ { isInteractive: true, conversationId: "session-1" },
185
+ );
186
+
187
+ expect(result.outcome).toBe("approved");
188
+ if (result.outcome === "approved") {
189
+ expect(result.userDecision).toBe("allow");
190
+ expect(result.grantId).toBe("grant-001");
191
+ }
192
+ expect(prompter.promptCalls).toHaveLength(0);
193
+ expect(cesClient.recordGrantCalls).toHaveLength(1);
194
+ expect(cesClient.recordGrantCalls[0]?.decision.decision).toBe("approved");
195
+ expect(cesClient.recordGrantCalls[0]?.decision.grantType).toBe(
196
+ "allow_once",
197
+ );
198
+ });
199
+
169
200
  describe("single-use approval", () => {
170
201
  test("allow decision commits grant to CES and returns grantId", async () => {
171
202
  const prompter = makePrompter("allow");
@@ -194,6 +194,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
194
194
  "oauth/token-persistence.ts", // OAuth token persistence (set/delete tokens)
195
195
  "oauth/connection-resolver.ts", // resolve OAuthConnection from oauth-store (access_token lookup)
196
196
  "runtime/routes/secret-routes.ts", // HTTP secret management routes (set/delete secrets)
197
+ "runtime/routes/migration-routes.ts", // migration import credential restore
197
198
  "daemon/conversation-messaging.ts", // credential storage during session messaging
198
199
  "runtime/routes/settings-routes.ts", // settings routes OAuth credential lookup (client_secret)
199
200
  "oauth/oauth-store.ts", // OAuth provider disconnect (delete stored tokens)
@@ -60,11 +60,11 @@ let slackChannelConfigCalls: Array<{
60
60
  }> = [];
61
61
 
62
62
  mock.module("../oauth/manual-token-connection.js", () => ({
63
- syncManualTokenConnection: async (providerKey: string) => {
63
+ syncManualTokenConnection: async (provider: string) => {
64
64
  const { credentialKey } = await import("../security/credential-key.js");
65
65
  const { getSecureKeyAsync } = await import("../security/secure-keys.js");
66
66
 
67
- if (providerKey === "slack_channel") {
67
+ if (provider === "slack_channel") {
68
68
  const hasBotToken = !!(await getSecureKeyAsync(
69
69
  credentialKey("slack_channel", "bot_token"),
70
70
  ));
@@ -72,9 +72,9 @@ mock.module("../oauth/manual-token-connection.js", () => ({
72
72
  credentialKey("slack_channel", "app_token"),
73
73
  ));
74
74
  if (hasBotToken && hasAppToken) {
75
- manualConnectionStore[providerKey] = "active";
75
+ manualConnectionStore[provider] = "active";
76
76
  } else {
77
- delete manualConnectionStore[providerKey];
77
+ delete manualConnectionStore[provider];
78
78
  }
79
79
  }
80
80
  },