@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
@@ -14,11 +14,6 @@
14
14
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
15
15
 
16
16
  import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
17
- import {
18
- initPermissionModeStore,
19
- resetForTesting as resetPermissionModeStore,
20
- setHostAccess,
21
- } from "../permissions/permission-mode-store.js";
22
17
  import type { PermissionPrompter } from "../permissions/prompter.js";
23
18
  import { RiskLevel } from "../permissions/types.js";
24
19
  import { PermissionChecker } from "../tools/permission-checker.js";
@@ -47,6 +42,13 @@ mock.module("../hooks/manager.js", () => ({
47
42
  }),
48
43
  }));
49
44
 
45
+ const hostAccessByConversation = new Map<string, boolean>();
46
+
47
+ mock.module("../memory/conversation-crud.js", () => ({
48
+ getConversationHostAccess: (conversationId: string) =>
49
+ hostAccessByConversation.get(conversationId) ?? false,
50
+ }));
51
+
50
52
  // ---------------------------------------------------------------------------
51
53
  // Helpers
52
54
  // ---------------------------------------------------------------------------
@@ -96,13 +98,12 @@ const executionTarget: ExecutionTarget = "host";
96
98
 
97
99
  beforeEach(() => {
98
100
  _setOverridesForTesting({});
99
- resetPermissionModeStore();
100
- initPermissionModeStore();
101
+ hostAccessByConversation.clear();
101
102
  });
102
103
 
103
104
  afterEach(() => {
104
105
  _setOverridesForTesting({});
105
- resetPermissionModeStore();
106
+ hostAccessByConversation.clear();
106
107
  });
107
108
 
108
109
  // ---------------------------------------------------------------------------
@@ -125,7 +126,7 @@ describe("permission-checker host-access gate (v2)", () => {
125
126
 
126
127
  describe("host tools with hostAccess=false", () => {
127
128
  beforeEach(() => {
128
- setHostAccess(false);
129
+ hostAccessByConversation.set("test-conv", false);
129
130
  });
130
131
 
131
132
  for (const toolName of HOST_TOOL_NAMES) {
@@ -151,6 +152,12 @@ describe("permission-checker host-access gate (v2)", () => {
151
152
 
152
153
  // The prompter should have been called (interactive dialog)
153
154
  expect(promptSpy).toHaveBeenCalled();
155
+ const call = promptSpy.mock.calls[0] as unknown as unknown[];
156
+ expect(call[3]).toEqual([]);
157
+ expect(call[4]).toEqual([]);
158
+ expect(call[8]).toBe(false);
159
+ expect(call[10]).toBeUndefined();
160
+ expect(call[12]).toBe(true);
154
161
  // Since the mock prompter returns "allow", the result should be allowed
155
162
  expect(result.allowed).toBe(true);
156
163
  expect(result.decision).toBe("allow");
@@ -185,7 +192,7 @@ describe("permission-checker host-access gate (v2)", () => {
185
192
 
186
193
  describe("host tools with hostAccess=true", () => {
187
194
  beforeEach(() => {
188
- setHostAccess(true);
195
+ hostAccessByConversation.set("test-conv", true);
189
196
  });
190
197
 
191
198
  for (const toolName of HOST_TOOL_NAMES) {
@@ -217,7 +224,7 @@ describe("permission-checker host-access gate (v2)", () => {
217
224
  "web_search",
218
225
  ]) {
219
226
  test(`${toolName} is auto-allowed regardless of hostAccess`, async () => {
220
- setHostAccess(false);
227
+ hostAccessByConversation.set("test-conv", false);
221
228
  const checker = new PermissionChecker(makePrompter());
222
229
  const result = await checker.checkPermission(
223
230
  toolName,
@@ -239,7 +246,7 @@ describe("permission-checker host-access gate (v2)", () => {
239
246
 
240
247
  describe("requireFreshApproval bypasses v2 auto-allow", () => {
241
248
  beforeEach(() => {
242
- setHostAccess(true);
249
+ hostAccessByConversation.set("test-conv", true);
243
250
  });
244
251
 
245
252
  test("host tool with requireFreshApproval falls through to prompter", async () => {
@@ -263,6 +270,12 @@ describe("permission-checker host-access gate (v2)", () => {
263
270
  );
264
271
 
265
272
  expect(promptSpy).toHaveBeenCalled();
273
+ const call = promptSpy.mock.calls[0] as unknown as unknown[];
274
+ expect(call[3]).toEqual([]);
275
+ expect(call[4]).toEqual([]);
276
+ expect(call[8]).toBe(false);
277
+ expect(call[10]).toBeUndefined();
278
+ expect(call[12]).toBe(false);
266
279
  expect(result.allowed).toBe(true);
267
280
  expect(result.decision).toBe("allow");
268
281
  });
@@ -288,6 +301,12 @@ describe("permission-checker host-access gate (v2)", () => {
288
301
  );
289
302
 
290
303
  expect(promptSpy).toHaveBeenCalled();
304
+ const call = promptSpy.mock.calls[0] as unknown as unknown[];
305
+ expect(call[3]).toEqual([]);
306
+ expect(call[4]).toEqual([]);
307
+ expect(call[8]).toBe(false);
308
+ expect(call[10]).toBeUndefined();
309
+ expect(call[12]).toBe(false);
291
310
  expect(result.allowed).toBe(true);
292
311
  expect(result.decision).toBe("allow");
293
312
  });
@@ -295,7 +314,7 @@ describe("permission-checker host-access gate (v2)", () => {
295
314
 
296
315
  describe("non-interactive guardian session with hostAccess=false", () => {
297
316
  beforeEach(() => {
298
- setHostAccess(false);
317
+ hostAccessByConversation.set("test-conv", false);
299
318
  });
300
319
 
301
320
  test("host tool is NOT auto-approved (denies instead of guardian_auto_approve)", async () => {
@@ -329,7 +348,7 @@ describe("permission-checker host-access gate (v2)", () => {
329
348
 
330
349
  describe("forcePromptSideEffects bypasses v2 auto-allow for side-effect tools", () => {
331
350
  beforeEach(() => {
332
- setHostAccess(true);
351
+ hostAccessByConversation.set("test-conv", true);
333
352
  });
334
353
 
335
354
  test("host side-effect tool with forcePromptSideEffects falls through to prompter", async () => {
@@ -401,6 +420,47 @@ describe("permission-checker host-access gate (v2)", () => {
401
420
  expect(result.decision).toBe("allow");
402
421
  });
403
422
  });
423
+
424
+ test("host access is evaluated per conversation", async () => {
425
+ hostAccessByConversation.set("allow-conv", true);
426
+ hostAccessByConversation.set("deny-conv", false);
427
+
428
+ const promptSpy = mock(() =>
429
+ Promise.resolve({ decision: "deny" as const }),
430
+ );
431
+ const checker = new PermissionChecker({
432
+ prompt: promptSpy,
433
+ } as unknown as PermissionPrompter);
434
+
435
+ const allowed = await checker.checkPermission(
436
+ "host_bash",
437
+ {},
438
+ makeTool("host_bash"),
439
+ makeContext({ conversationId: "allow-conv" }),
440
+ executionTarget,
441
+ noopEmit,
442
+ noopSanitize,
443
+ Date.now(),
444
+ noopDiff,
445
+ );
446
+ const denied = await checker.checkPermission(
447
+ "host_bash",
448
+ {},
449
+ makeTool("host_bash"),
450
+ makeContext({ conversationId: "deny-conv" }),
451
+ executionTarget,
452
+ noopEmit,
453
+ noopSanitize,
454
+ Date.now(),
455
+ noopDiff,
456
+ );
457
+
458
+ expect(allowed.allowed).toBe(true);
459
+ expect(allowed.decision).toBe("allow");
460
+ expect(denied.allowed).toBe(false);
461
+ expect(denied.decision).toBe("deny");
462
+ expect(promptSpy).toHaveBeenCalledTimes(1);
463
+ });
404
464
  });
405
465
 
406
466
  describe("when permission-controls-v2 flag is OFF", () => {
@@ -6,51 +6,28 @@ import {
6
6
  PermissionModeSchema,
7
7
  } from "../permissions/permission-mode.js";
8
8
 
9
- // ---------------------------------------------------------------------------
10
- // Tests: PermissionModeSchema
11
- // ---------------------------------------------------------------------------
12
-
13
9
  describe("PermissionModeSchema", () => {
14
- test("parses empty object with correct defaults", () => {
15
- const result = PermissionModeSchema.parse({});
16
- expect(result.askBeforeActing).toBe(true);
17
- expect(result.hostAccess).toBe(false);
10
+ test("parses empty object with the host-access default", () => {
11
+ expect(PermissionModeSchema.parse({})).toEqual({ hostAccess: false });
18
12
  });
19
13
 
20
14
  test("DEFAULT_PERMISSION_MODE matches schema defaults", () => {
21
- const parsed = PermissionModeSchema.parse({});
22
- expect(parsed).toEqual(DEFAULT_PERMISSION_MODE);
15
+ expect(PermissionModeSchema.parse({})).toEqual(DEFAULT_PERMISSION_MODE);
23
16
  });
24
17
 
25
- test("accepts explicit true/true", () => {
26
- const result = PermissionModeSchema.parse({
27
- askBeforeActing: true,
18
+ test("accepts explicit host access values", () => {
19
+ expect(PermissionModeSchema.parse({ hostAccess: true })).toEqual({
28
20
  hostAccess: true,
29
21
  });
30
- expect(result.askBeforeActing).toBe(true);
31
- expect(result.hostAccess).toBe(true);
32
- });
33
-
34
- test("accepts explicit false/false", () => {
35
- const result = PermissionModeSchema.parse({
36
- askBeforeActing: false,
22
+ expect(PermissionModeSchema.parse({ hostAccess: false })).toEqual({
37
23
  hostAccess: false,
38
24
  });
39
- expect(result.askBeforeActing).toBe(false);
40
- expect(result.hostAccess).toBe(false);
41
25
  });
42
26
 
43
27
  test("round-trips through JSON serialization", () => {
44
- const original = { askBeforeActing: false, hostAccess: true };
28
+ const original = { hostAccess: true };
45
29
  const json = JSON.stringify(original);
46
- const parsed = PermissionModeSchema.parse(JSON.parse(json));
47
- expect(parsed).toEqual(original);
48
- });
49
-
50
- test("rejects non-boolean askBeforeActing", () => {
51
- expect(() =>
52
- PermissionModeSchema.parse({ askBeforeActing: "yes" }),
53
- ).toThrow();
30
+ expect(PermissionModeSchema.parse(JSON.parse(json))).toEqual(original);
54
31
  });
55
32
 
56
33
  test("rejects non-boolean hostAccess", () => {
@@ -58,44 +35,39 @@ describe("PermissionModeSchema", () => {
58
35
  });
59
36
  });
60
37
 
61
- // ---------------------------------------------------------------------------
62
- // Tests: PermissionsConfigSchema (permissionMode fields)
63
- // ---------------------------------------------------------------------------
64
-
65
- describe("PermissionsConfigSchema permissionMode fields", () => {
66
- test("defaults askBeforeActing to true and hostAccess to false", () => {
67
- const result = PermissionsConfigSchema.parse({});
68
- expect(result.askBeforeActing).toBe(true);
69
- expect(result.hostAccess).toBe(false);
38
+ describe("PermissionsConfigSchema", () => {
39
+ test("defaults to workspace mode with host access disabled", () => {
40
+ expect(PermissionsConfigSchema.parse({})).toEqual({
41
+ mode: "workspace",
42
+ hostAccess: false,
43
+ });
70
44
  });
71
45
 
72
- test("preserves existing mode field alongside new fields", () => {
73
- const result = PermissionsConfigSchema.parse({ mode: "strict" });
74
- expect(result.mode).toBe("strict");
75
- expect(result.askBeforeActing).toBe(true);
76
- expect(result.hostAccess).toBe(false);
46
+ test("preserves the mode field alongside hostAccess", () => {
47
+ expect(PermissionsConfigSchema.parse({ mode: "strict" })).toEqual({
48
+ mode: "strict",
49
+ hostAccess: false,
50
+ });
77
51
  });
78
52
 
79
- test("accepts overridden values for new fields", () => {
80
- const result = PermissionsConfigSchema.parse({
53
+ test("accepts overridden hostAccess values", () => {
54
+ expect(
55
+ PermissionsConfigSchema.parse({
56
+ mode: "workspace",
57
+ hostAccess: true,
58
+ }),
59
+ ).toEqual({
81
60
  mode: "workspace",
82
- askBeforeActing: false,
83
61
  hostAccess: true,
84
62
  });
85
- expect(result.mode).toBe("workspace");
86
- expect(result.askBeforeActing).toBe(false);
87
- expect(result.hostAccess).toBe(true);
88
63
  });
89
64
 
90
- test("round-trips new fields through JSON serialization", () => {
65
+ test("round-trips hostAccess through JSON serialization", () => {
91
66
  const input = {
92
67
  mode: "workspace" as const,
93
- askBeforeActing: false,
94
68
  hostAccess: true,
95
69
  };
96
70
  const json = JSON.stringify(input);
97
- const parsed = PermissionsConfigSchema.parse(JSON.parse(json));
98
- expect(parsed.askBeforeActing).toBe(false);
99
- expect(parsed.hostAccess).toBe(true);
71
+ expect(PermissionsConfigSchema.parse(JSON.parse(json))).toEqual(input);
100
72
  });
101
73
  });
@@ -69,6 +69,25 @@ describe("platform callback registration", () => {
69
69
  expect(context.authHeader).toBe("Api-Key ast-managed-key");
70
70
  });
71
71
 
72
+ test("self-hosted assistant with stored credentials is enabled without IS_PLATFORM", async () => {
73
+ mockIsPlatform = false;
74
+ mockSecureKeys[credentialKey("vellum", "platform_base_url")] =
75
+ "https://platform.example.com";
76
+ mockSecureKeys[credentialKey("vellum", "platform_assistant_id")] =
77
+ "22222222-3333-4444-8555-666666666666";
78
+ mockSecureKeys[credentialKey("vellum", "assistant_api_key")] =
79
+ "ast-self-hosted-key";
80
+
81
+ const context = await resolvePlatformCallbackRegistrationContext();
82
+
83
+ expect(context.enabled).toBe(true);
84
+ expect(context.isPlatform).toBe(false);
85
+ expect(context.platformBaseUrl).toBe("https://platform.example.com");
86
+ expect(context.assistantId).toBe("22222222-3333-4444-8555-666666666666");
87
+ expect(context.hasAssistantApiKey).toBe(true);
88
+ expect(context.authHeader).toBe("Api-Key ast-self-hosted-key");
89
+ });
90
+
72
91
  test("registerCallbackRoute falls back to assistant API key auth", async () => {
73
92
  mockSecureKeys[credentialKey("vellum", "platform_base_url")] =
74
93
  "https://platform.example.com";
@@ -0,0 +1,296 @@
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
+
6
+ import {
7
+ buildTruncatedContent,
8
+ derefToolResultReReads,
9
+ getToolResultFilePath,
10
+ postTurnTruncateToolResults,
11
+ REREAD_STUB,
12
+ TARGET_CHARS,
13
+ THRESHOLD_CHARS,
14
+ TOOL_RESULT_DIR,
15
+ TRUNCATION_MARKER,
16
+ } from "../context/post-turn-tool-result-truncation.js";
17
+ import type { ContentBlock, Message } from "../providers/types.js";
18
+
19
+ function makeToolResult(
20
+ content: string,
21
+ toolUseId = "tool_use_1",
22
+ is_error = false,
23
+ ): ContentBlock {
24
+ return {
25
+ type: "tool_result" as const,
26
+ tool_use_id: toolUseId,
27
+ content,
28
+ ...(is_error ? { is_error: true } : {}),
29
+ };
30
+ }
31
+
32
+ function makeMessages(blocks: ContentBlock[]): Message[] {
33
+ return [{ role: "user", content: blocks }];
34
+ }
35
+
36
+ describe("postTurnTruncateToolResults", () => {
37
+ let convDir: string;
38
+
39
+ beforeEach(() => {
40
+ convDir = mkdtempSync(join(tmpdir(), "tool-result-trunc-"));
41
+ });
42
+
43
+ afterEach(() => {
44
+ rmSync(convDir, { recursive: true, force: true });
45
+ });
46
+
47
+ test("result below threshold is returned unchanged, no file written", () => {
48
+ const shortContent = "a".repeat(THRESHOLD_CHARS);
49
+ const messages = makeMessages([makeToolResult(shortContent)]);
50
+
51
+ const { messages: result, truncatedCount } =
52
+ postTurnTruncateToolResults(messages, { conversationDir: convDir });
53
+
54
+ expect(truncatedCount).toBe(0);
55
+ expect(result).toBe(messages); // same reference — no copy
56
+ expect(existsSync(join(convDir, TOOL_RESULT_DIR))).toBe(false);
57
+ });
58
+
59
+ test("result above threshold is truncated, file written with original content", () => {
60
+ const longContent = "x".repeat(THRESHOLD_CHARS + 1);
61
+ const toolUseId = "tool_use_abc";
62
+ const messages = makeMessages([makeToolResult(longContent, toolUseId)]);
63
+
64
+ const { messages: result, truncatedCount } =
65
+ postTurnTruncateToolResults(messages, { conversationDir: convDir });
66
+
67
+ expect(truncatedCount).toBe(1);
68
+
69
+ const block = result[0].content[0] as { type: "tool_result"; content: string };
70
+ expect(block.content).toContain(TRUNCATION_MARKER);
71
+ expect(block.content.length).toBeLessThan(longContent.length);
72
+
73
+ // Verify file on disk contains original content.
74
+ const filePath = getToolResultFilePath(convDir, toolUseId);
75
+ expect(existsSync(filePath)).toBe(true);
76
+ expect(readFileSync(filePath, "utf-8")).toBe(longContent);
77
+ });
78
+
79
+ test("error result above threshold is unchanged", () => {
80
+ const longContent = "e".repeat(THRESHOLD_CHARS + 100);
81
+ const messages = makeMessages([
82
+ makeToolResult(longContent, "tool_err", true),
83
+ ]);
84
+
85
+ const { messages: result, truncatedCount } =
86
+ postTurnTruncateToolResults(messages, { conversationDir: convDir });
87
+
88
+ expect(truncatedCount).toBe(0);
89
+ expect(result).toBe(messages);
90
+ });
91
+
92
+ test("already-truncated result is unchanged (idempotency)", () => {
93
+ // Simulate a result that was already truncated in a prior pass.
94
+ const alreadyTruncated =
95
+ "prefix..." +
96
+ `\n\n...(500 tokens omitted ${TRUNCATION_MARKER} /some/path.txt)\n\n` +
97
+ "...suffix".padEnd(THRESHOLD_CHARS + 1, "z");
98
+ const messages = makeMessages([
99
+ makeToolResult(alreadyTruncated, "tool_idempotent"),
100
+ ]);
101
+
102
+ const { messages: result, truncatedCount } =
103
+ postTurnTruncateToolResults(messages, { conversationDir: convDir });
104
+
105
+ expect(truncatedCount).toBe(0);
106
+ expect(result).toBe(messages);
107
+ });
108
+
109
+ test("multiple results in one turn are each evaluated independently", () => {
110
+ const short = "s".repeat(100);
111
+ const long1 = "a".repeat(THRESHOLD_CHARS + 1);
112
+ const long2 = "b".repeat(THRESHOLD_CHARS + 2);
113
+ const messages = makeMessages([
114
+ makeToolResult(short, "tool_short"),
115
+ makeToolResult(long1, "tool_long1"),
116
+ makeToolResult(long2, "tool_long2"),
117
+ ]);
118
+
119
+ const { messages: result, truncatedCount } =
120
+ postTurnTruncateToolResults(messages, { conversationDir: convDir });
121
+
122
+ expect(truncatedCount).toBe(2);
123
+
124
+ // Short result unchanged.
125
+ const b0 = result[0].content[0] as { type: "tool_result"; content: string };
126
+ expect(b0.content).toBe(short);
127
+
128
+ // Both long results truncated.
129
+ const b1 = result[0].content[1] as { type: "tool_result"; content: string };
130
+ const b2 = result[0].content[2] as { type: "tool_result"; content: string };
131
+ expect(b1.content).toContain(TRUNCATION_MARKER);
132
+ expect(b2.content).toContain(TRUNCATION_MARKER);
133
+ });
134
+
135
+ test("prefix/suffix split preserves first and last halves of TARGET_CHARS", () => {
136
+ // Build content where each char is its position modulo 10 so we can verify slicing.
137
+ const longContent = Array.from({ length: THRESHOLD_CHARS + 500 }, (_, i) =>
138
+ String(i % 10),
139
+ ).join("");
140
+
141
+ const filePath = "/tmp/fake-path.txt";
142
+ const stub = buildTruncatedContent(longContent, filePath);
143
+
144
+ const half = Math.floor(TARGET_CHARS / 2);
145
+ const expectedPrefix = longContent.slice(0, half);
146
+ const expectedSuffix = longContent.slice(-half);
147
+
148
+ expect(stub.startsWith(expectedPrefix)).toBe(true);
149
+ expect(stub.endsWith(expectedSuffix)).toBe(true);
150
+ expect(stub).toContain(TRUNCATION_MARKER);
151
+ expect(stub).toContain(filePath);
152
+ });
153
+
154
+ test("file path is deterministic for the same toolUseId", () => {
155
+ const id = "tool_use_deterministic";
156
+ const path1 = getToolResultFilePath("/some/dir", id);
157
+ const path2 = getToolResultFilePath("/some/dir", id);
158
+ expect(path1).toBe(path2);
159
+
160
+ // Different IDs produce different paths.
161
+ const path3 = getToolResultFilePath("/some/dir", "tool_use_other");
162
+ expect(path3).not.toBe(path1);
163
+ });
164
+ });
165
+
166
+ describe("derefToolResultReReads", () => {
167
+ function makeToolUse(
168
+ id: string,
169
+ name: string,
170
+ input: Record<string, unknown>,
171
+ ): ContentBlock {
172
+ return { type: "tool_use" as const, id, name, input };
173
+ }
174
+
175
+ test("file_read of .tool-results/ path: tool_result content replaced with REREAD_STUB", () => {
176
+ const toolUseId = "tu_reread_1";
177
+ const messages: Message[] = [
178
+ {
179
+ role: "assistant",
180
+ content: [
181
+ makeToolUse(toolUseId, "file_read", {
182
+ path: `/home/user/.vellum/workspace/conversations/abc/${TOOL_RESULT_DIR}/abc123.txt`,
183
+ }),
184
+ ],
185
+ },
186
+ {
187
+ role: "user",
188
+ content: [makeToolResult("full file contents here", toolUseId)],
189
+ },
190
+ ];
191
+
192
+ const { messages: result, dereferencedCount } =
193
+ derefToolResultReReads(messages);
194
+
195
+ expect(dereferencedCount).toBe(1);
196
+ const block = result[1].content[0] as { type: "tool_result"; content: string };
197
+ expect(block.content).toBe(REREAD_STUB);
198
+ });
199
+
200
+ test("file_read of normal path: tool_result unchanged", () => {
201
+ const toolUseId = "tu_normal_read";
202
+ const originalContent = "some file contents";
203
+ const messages: Message[] = [
204
+ {
205
+ role: "assistant",
206
+ content: [
207
+ makeToolUse(toolUseId, "file_read", {
208
+ path: "src/foo.ts",
209
+ }),
210
+ ],
211
+ },
212
+ {
213
+ role: "user",
214
+ content: [makeToolResult(originalContent, toolUseId)],
215
+ },
216
+ ];
217
+
218
+ const { messages: result, dereferencedCount } =
219
+ derefToolResultReReads(messages);
220
+
221
+ expect(dereferencedCount).toBe(0);
222
+ expect(result).toBe(messages); // same reference — no copy
223
+ const block = result[1].content[0] as { type: "tool_result"; content: string };
224
+ expect(block.content).toBe(originalContent);
225
+ });
226
+
227
+ test("non-file_read tool: tool_result unchanged even if output mentions .tool-results/", () => {
228
+ const toolUseId = "tu_bash";
229
+ const outputMentioningDir = `Found file at /home/user/${TOOL_RESULT_DIR}/abc.txt`;
230
+ const messages: Message[] = [
231
+ {
232
+ role: "assistant",
233
+ content: [
234
+ makeToolUse(toolUseId, "bash", {
235
+ command: "ls",
236
+ }),
237
+ ],
238
+ },
239
+ {
240
+ role: "user",
241
+ content: [makeToolResult(outputMentioningDir, toolUseId)],
242
+ },
243
+ ];
244
+
245
+ const { messages: result, dereferencedCount } =
246
+ derefToolResultReReads(messages);
247
+
248
+ expect(dereferencedCount).toBe(0);
249
+ expect(result).toBe(messages);
250
+ const block = result[1].content[0] as { type: "tool_result"; content: string };
251
+ expect(block.content).toBe(outputMentioningDir);
252
+ });
253
+
254
+ test("multiple re-reads in one turn: each deduplicated independently", () => {
255
+ const tu1 = "tu_multi_1";
256
+ const tu2 = "tu_multi_2";
257
+ const tuNormal = "tu_multi_normal";
258
+ const messages: Message[] = [
259
+ {
260
+ role: "assistant",
261
+ content: [
262
+ makeToolUse(tu1, "file_read", {
263
+ path: `/workspace/conv/${TOOL_RESULT_DIR}/aaa.txt`,
264
+ }),
265
+ makeToolUse(tu2, "file_read", {
266
+ path: `/workspace/conv/${TOOL_RESULT_DIR}/bbb.txt`,
267
+ }),
268
+ makeToolUse(tuNormal, "file_read", {
269
+ path: "src/bar.ts",
270
+ }),
271
+ ],
272
+ },
273
+ {
274
+ role: "user",
275
+ content: [
276
+ makeToolResult("re-read content 1", tu1),
277
+ makeToolResult("re-read content 2", tu2),
278
+ makeToolResult("normal read content", tuNormal),
279
+ ],
280
+ },
281
+ ];
282
+
283
+ const { messages: result, dereferencedCount } =
284
+ derefToolResultReReads(messages);
285
+
286
+ expect(dereferencedCount).toBe(2);
287
+
288
+ const b0 = result[1].content[0] as { type: "tool_result"; content: string };
289
+ const b1 = result[1].content[1] as { type: "tool_result"; content: string };
290
+ const b2 = result[1].content[2] as { type: "tool_result"; content: string };
291
+
292
+ expect(b0.content).toBe(REREAD_STUB);
293
+ expect(b1.content).toBe(REREAD_STUB);
294
+ expect(b2.content).toBe("normal read content"); // unchanged
295
+ });
296
+ });
@@ -1,5 +1,6 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
+ import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
3
4
  import type { ProxyApprovalRequest } from "../outbound-proxy/index.js";
4
5
 
5
6
  // ---------------------------------------------------------------------------
@@ -110,6 +111,23 @@ describe("createProxyApprovalCallback", () => {
110
111
  addRuleMock.mockClear();
111
112
  findHighestPriorityRuleMock.mockClear();
112
113
  findHighestPriorityRuleMock.mockReturnValue(null);
114
+ _setOverridesForTesting({});
115
+ });
116
+
117
+ test("suppresses network approval cards under v2 and auto-allows", async () => {
118
+ _setOverridesForTesting({ "permission-controls-v2": true });
119
+
120
+ const ctx = makeContext();
121
+ const prompterSendToClient = mock(() => {});
122
+ const prompter = new PermissionPrompter(prompterSendToClient);
123
+
124
+ const callback = createProxyApprovalCallback(prompter, ctx);
125
+ const result = await callback(makeAskMissingCredentialRequest());
126
+
127
+ expect(result).toBe(true);
128
+ expect(prompterSendToClient).not.toHaveBeenCalled();
129
+ expect(findHighestPriorityRuleMock).not.toHaveBeenCalled();
130
+ expect(addRuleMock).not.toHaveBeenCalled();
113
131
  });
114
132
 
115
133
  test("returns true when user allows an ask_missing_credential request", async () => {