@vellumai/assistant 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (463) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docker-entrypoint.sh +12 -2
  4. package/docs/architecture/memory.md +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  7. package/openapi.yaml +184 -69
  8. package/package.json +41 -41
  9. package/scripts/generate-openapi.ts +1 -2
  10. package/src/__tests__/acp-session.test.ts +43 -0
  11. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +1 -0
  13. package/src/__tests__/app-source-watcher.test.ts +37 -11
  14. package/src/__tests__/approval-routes-http.test.ts +178 -1
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  17. package/src/__tests__/browser-manager.test.ts +40 -27
  18. package/src/__tests__/catalog-files.test.ts +862 -0
  19. package/src/__tests__/channel-approvals.test.ts +53 -0
  20. package/src/__tests__/checker.test.ts +104 -170
  21. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  22. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  23. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  24. package/src/__tests__/config-schema.test.ts +125 -48
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  26. package/src/__tests__/context-overflow-approval.test.ts +21 -6
  27. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  28. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  29. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  30. package/src/__tests__/conversation-attachments.test.ts +80 -4
  31. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  32. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  33. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  34. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  35. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  36. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  37. package/src/__tests__/conversation-queue.test.ts +45 -2
  38. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  39. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  40. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  42. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  43. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  44. package/src/__tests__/conversation-store.test.ts +195 -0
  45. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  46. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -3
  47. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  48. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  49. package/src/__tests__/credential-vault.test.ts +152 -13
  50. package/src/__tests__/credentials-cli.test.ts +2 -2
  51. package/src/__tests__/date-context.test.ts +4 -4
  52. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  53. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  54. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  55. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  56. package/src/__tests__/gemini-provider.test.ts +2 -2
  57. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  58. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  59. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  60. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  61. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  62. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  63. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  64. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  65. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  66. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  67. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  68. package/src/__tests__/host-browser-routes.test.ts +198 -0
  69. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  70. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  71. package/src/__tests__/host-file-proxy.test.ts +185 -1
  72. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  73. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  74. package/src/__tests__/host-shell-tool.test.ts +1 -11
  75. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  76. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  77. package/src/__tests__/inline-command-runner.test.ts +7 -5
  78. package/src/__tests__/integration-status.test.ts +6 -7
  79. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  80. package/src/__tests__/log-export-workspace.test.ts +190 -0
  81. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  82. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  83. package/src/__tests__/mcp-health-check.test.ts +10 -3
  84. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  85. package/src/__tests__/migration-export-http.test.ts +61 -2
  86. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  87. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  88. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  89. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  90. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  91. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  92. package/src/__tests__/oauth-cli.test.ts +707 -60
  93. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  94. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  95. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  96. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  97. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  98. package/src/__tests__/oauth-store.test.ts +1386 -182
  99. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  100. package/src/__tests__/onboarding-template-contract.test.ts +74 -55
  101. package/src/__tests__/openai-provider.test.ts +2 -2
  102. package/src/__tests__/outlook-categories.test.ts +1 -1
  103. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  104. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  105. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  106. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  107. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  108. package/src/__tests__/outlook-trash.test.ts +1 -1
  109. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  110. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  111. package/src/__tests__/permission-mode.test.ts +28 -56
  112. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  113. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  114. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  115. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  116. package/src/__tests__/require-fresh-approval.test.ts +40 -3
  117. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  118. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  119. package/src/__tests__/schedule-routes.test.ts +162 -0
  120. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  121. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  122. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  123. package/src/__tests__/set-permission-mode.test.ts +13 -250
  124. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  125. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  126. package/src/__tests__/slack-channel-config.test.ts +12 -15
  127. package/src/__tests__/subagent-detail.test.ts +44 -2
  128. package/src/__tests__/subagent-disposal.test.ts +1 -0
  129. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  130. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  131. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  132. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  133. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  134. package/src/__tests__/subagent-tools.test.ts +1 -0
  135. package/src/__tests__/subagent-types.test.ts +1 -0
  136. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  137. package/src/__tests__/system-prompt.test.ts +72 -1
  138. package/src/__tests__/task-scheduler.test.ts +32 -6
  139. package/src/__tests__/telegram-config.test.ts +10 -13
  140. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  141. package/src/__tests__/terminal-tools.test.ts +11 -5
  142. package/src/__tests__/test-preload.ts +14 -0
  143. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  144. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  145. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  146. package/src/__tests__/tool-executor.test.ts +0 -1
  147. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  148. package/src/__tests__/top-level-renderer.test.ts +73 -1
  149. package/src/__tests__/transport-hints-queue.test.ts +62 -0
  150. package/src/__tests__/trust-store.test.ts +4 -4
  151. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  152. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  153. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  154. package/src/__tests__/workspace-policy.test.ts +2 -7
  155. package/src/acp/client-handler.ts +30 -4
  156. package/src/agent/loop.ts +12 -35
  157. package/src/approvals/guardian-request-resolvers.ts +21 -15
  158. package/src/browser-session/__tests__/manager.test.ts +297 -0
  159. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  160. package/src/browser-session/backends/extension.ts +26 -0
  161. package/src/browser-session/backends/local.ts +24 -0
  162. package/src/browser-session/events.ts +164 -0
  163. package/src/browser-session/index.ts +27 -0
  164. package/src/browser-session/manager.ts +159 -0
  165. package/src/browser-session/types.ts +28 -0
  166. package/src/channels/__tests__/types.test.ts +134 -0
  167. package/src/channels/types.ts +55 -0
  168. package/src/cli/__tests__/run-assistant-command.ts +34 -7
  169. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  170. package/src/cli/commands/browser-relay.ts +339 -409
  171. package/src/cli/commands/credentials.ts +3 -3
  172. package/src/cli/commands/default-action.ts +68 -1
  173. package/src/cli/commands/email.ts +18 -13
  174. package/src/cli/commands/mcp.ts +16 -4
  175. package/src/cli/commands/oauth/__tests__/connect.test.ts +68 -41
  176. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  177. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  178. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  179. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  180. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  181. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  182. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  183. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  184. package/src/cli/commands/oauth/apps.ts +7 -4
  185. package/src/cli/commands/oauth/connect.ts +16 -2
  186. package/src/cli/commands/oauth/disconnect.ts +1 -1
  187. package/src/cli/commands/oauth/providers.ts +200 -36
  188. package/src/cli/commands/oauth/shared.ts +5 -5
  189. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  190. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  191. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  192. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  193. package/src/cli/commands/platform/index.ts +107 -10
  194. package/src/cli/commands/usage.ts +10 -9
  195. package/src/cli/lib/daemon-credential-client.ts +4 -0
  196. package/src/cli/program.ts +10 -3
  197. package/src/config/assistant-feature-flags.ts +59 -55
  198. package/src/config/bundled-skills/app-builder/SKILL.md +33 -173
  199. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  200. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  201. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  202. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  203. package/src/config/bundled-skills/document/SKILL.md +4 -0
  204. package/src/config/bundled-skills/gmail/SKILL.md +12 -7
  205. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  206. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  207. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  208. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  209. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  210. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  211. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  212. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  213. package/src/config/env-registry.ts +14 -0
  214. package/src/config/env.ts +21 -0
  215. package/src/config/feature-flag-registry.json +46 -7
  216. package/src/config/loader.ts +56 -1
  217. package/src/config/sanitize-for-transfer.ts +47 -0
  218. package/src/config/schema.ts +46 -5
  219. package/src/config/schemas/host-browser.ts +66 -0
  220. package/src/config/schemas/memory-lifecycle.ts +1 -1
  221. package/src/config/schemas/memory-retrieval.ts +103 -0
  222. package/src/config/schemas/security.ts +0 -6
  223. package/src/config/schemas/services.ts +16 -0
  224. package/src/config/types.ts +0 -1
  225. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  226. package/src/context/window-manager.ts +19 -1
  227. package/src/credential-execution/approval-bridge.ts +49 -16
  228. package/src/credential-execution/managed-catalog.ts +3 -7
  229. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  230. package/src/daemon/app-source-watcher.ts +35 -0
  231. package/src/daemon/config-watcher.ts +6 -2
  232. package/src/daemon/context-overflow-approval.ts +5 -1
  233. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  234. package/src/daemon/conversation-agent-loop.ts +74 -19
  235. package/src/daemon/conversation-attachments.ts +40 -1
  236. package/src/daemon/conversation-messaging.ts +3 -0
  237. package/src/daemon/conversation-process.ts +66 -3
  238. package/src/daemon/conversation-queue-manager.ts +8 -0
  239. package/src/daemon/conversation-runtime-assembly.ts +159 -20
  240. package/src/daemon/conversation-surfaces.ts +78 -12
  241. package/src/daemon/conversation-tool-setup.ts +74 -11
  242. package/src/daemon/conversation-workspace.ts +12 -0
  243. package/src/daemon/conversation.ts +227 -11
  244. package/src/daemon/date-context.ts +10 -10
  245. package/src/daemon/first-greeting.ts +3 -2
  246. package/src/daemon/handlers/conversations.ts +9 -139
  247. package/src/daemon/handlers/shared.ts +65 -0
  248. package/src/daemon/handlers/skills.ts +232 -37
  249. package/src/daemon/host-bash-proxy.ts +48 -13
  250. package/src/daemon/host-browser-proxy.ts +191 -0
  251. package/src/daemon/host-cu-proxy.ts +36 -11
  252. package/src/daemon/host-file-proxy.ts +57 -9
  253. package/src/daemon/lifecycle.ts +86 -12
  254. package/src/daemon/message-protocol.ts +7 -0
  255. package/src/daemon/message-types/conversations.ts +59 -13
  256. package/src/daemon/message-types/host-browser.ts +100 -0
  257. package/src/daemon/message-types/messages.ts +5 -6
  258. package/src/daemon/message-types/notifications.ts +12 -0
  259. package/src/daemon/message-types/settings.ts +12 -0
  260. package/src/daemon/message-types/skills.ts +10 -0
  261. package/src/daemon/message-types/subagents.ts +2 -0
  262. package/src/daemon/server.ts +112 -35
  263. package/src/daemon/tool-side-effects.ts +6 -0
  264. package/src/daemon/transport-hints.ts +14 -0
  265. package/src/inbound/platform-callback-registration.ts +18 -17
  266. package/src/index.ts +1 -1
  267. package/src/mcp/client.ts +59 -24
  268. package/src/memory/app-store.ts +31 -1
  269. package/src/memory/conversation-crud.ts +38 -10
  270. package/src/memory/conversation-directories.ts +39 -0
  271. package/src/memory/conversation-group-migration.ts +65 -5
  272. package/src/memory/conversation-starters-cadence.ts +76 -0
  273. package/src/memory/conversation-title-service.ts +5 -2
  274. package/src/memory/db-init.ts +12 -0
  275. package/src/memory/embedding-backend.test.ts +75 -0
  276. package/src/memory/embedding-backend.ts +131 -5
  277. package/src/memory/embedding-gemini.test.ts +54 -0
  278. package/src/memory/embedding-gemini.ts +20 -9
  279. package/src/memory/embedding-local.ts +177 -18
  280. package/src/memory/graph/capability-seed.ts +3 -5
  281. package/src/memory/graph/consolidation.ts +10 -23
  282. package/src/memory/graph/extraction-job.ts +15 -0
  283. package/src/memory/graph/retriever.ts +40 -22
  284. package/src/memory/graph/store.test.ts +7 -3
  285. package/src/memory/graph/store.ts +47 -12
  286. package/src/memory/group-crud.ts +25 -9
  287. package/src/memory/llm-usage-store.ts +45 -4
  288. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  289. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  290. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  291. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  292. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  293. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  294. package/src/memory/migrations/index.ts +6 -0
  295. package/src/memory/migrations/registry.ts +8 -0
  296. package/src/memory/schema/conversations.ts +1 -0
  297. package/src/memory/schema/oauth.ts +18 -13
  298. package/src/messaging/provider.ts +1 -1
  299. package/src/notifications/broadcaster.ts +6 -0
  300. package/src/notifications/conversation-pairing.ts +12 -4
  301. package/src/notifications/emit-signal.ts +14 -0
  302. package/src/notifications/signal.ts +11 -0
  303. package/src/oauth/AGENTS.md +76 -0
  304. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  305. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  306. package/src/oauth/byo-connection.test.ts +8 -8
  307. package/src/oauth/byo-connection.ts +7 -7
  308. package/src/oauth/connect-orchestrator.ts +23 -21
  309. package/src/oauth/connect-types.ts +3 -3
  310. package/src/oauth/connection-resolver.test.ts +17 -4
  311. package/src/oauth/connection-resolver.ts +16 -16
  312. package/src/oauth/connection.ts +1 -1
  313. package/src/oauth/manual-token-connection.ts +13 -13
  314. package/src/oauth/oauth-store.ts +214 -100
  315. package/src/oauth/platform-connection.test.ts +5 -5
  316. package/src/oauth/platform-connection.ts +4 -4
  317. package/src/oauth/provider-serializer.ts +31 -5
  318. package/src/oauth/revoke.ts +76 -0
  319. package/src/oauth/seed-providers.ts +127 -87
  320. package/src/oauth/token-persistence.ts +1 -1
  321. package/src/permissions/checker.ts +3 -3
  322. package/src/permissions/defaults.ts +7 -8
  323. package/src/permissions/permission-mode.ts +4 -11
  324. package/src/permissions/prompter.ts +13 -3
  325. package/src/permissions/v2-consent-policy.ts +87 -0
  326. package/src/platform/client.ts +1 -1
  327. package/src/prompts/system-prompt.ts +18 -21
  328. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  329. package/src/prompts/templates/BOOTSTRAP.md +59 -96
  330. package/src/prompts/templates/SOUL.md +11 -11
  331. package/src/providers/anthropic/client.ts +1 -0
  332. package/src/providers/types.ts +1 -1
  333. package/src/runtime/AGENTS.md +23 -0
  334. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  335. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  336. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  337. package/src/runtime/assistant-event-hub.ts +24 -2
  338. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  339. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  341. package/src/runtime/auth/middleware.ts +98 -0
  342. package/src/runtime/auth/route-policy.ts +6 -7
  343. package/src/runtime/auth/token-service.ts +8 -0
  344. package/src/runtime/capability-tokens.ts +414 -0
  345. package/src/runtime/channel-approvals.ts +18 -5
  346. package/src/runtime/chrome-extension-registry.ts +332 -0
  347. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  348. package/src/runtime/guardian-decision-types.ts +7 -0
  349. package/src/runtime/http-server.ts +425 -70
  350. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  351. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  352. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  353. package/src/runtime/migrations/migration-transport.ts +6 -0
  354. package/src/runtime/migrations/migration-wizard.ts +22 -2
  355. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  356. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  357. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  358. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  359. package/src/runtime/pending-interactions.ts +29 -13
  360. package/src/runtime/routes/approval-routes.ts +90 -16
  361. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  362. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  363. package/src/runtime/routes/conversation-analysis-routes.ts +18 -5
  364. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  365. package/src/runtime/routes/conversation-routes.ts +308 -28
  366. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  367. package/src/runtime/routes/group-routes.ts +22 -8
  368. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  369. package/src/runtime/routes/host-browser-routes.ts +279 -0
  370. package/src/runtime/routes/host-file-routes.ts +9 -1
  371. package/src/runtime/routes/identity-routes.ts +259 -16
  372. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  373. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  374. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  375. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  376. package/src/runtime/routes/log-export-routes.ts +60 -25
  377. package/src/runtime/routes/memory-item-routes.ts +1 -7
  378. package/src/runtime/routes/migration-routes.ts +87 -2
  379. package/src/runtime/routes/oauth-apps.ts +15 -17
  380. package/src/runtime/routes/oauth-providers.ts +4 -0
  381. package/src/runtime/routes/schedule-routes.ts +24 -11
  382. package/src/runtime/routes/settings-routes.ts +9 -97
  383. package/src/runtime/routes/skills-routes.ts +52 -2
  384. package/src/runtime/routes/subagents-routes.ts +14 -10
  385. package/src/runtime/routes/usage-routes.ts +8 -7
  386. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  387. package/src/runtime/routes/workspace-routes.ts +8 -1
  388. package/src/runtime/routes/workspace-utils.ts +2 -0
  389. package/src/schedule/scheduler.ts +7 -5
  390. package/src/security/ces-credential-client.ts +20 -0
  391. package/src/security/ces-rpc-credential-backend.ts +17 -0
  392. package/src/security/credential-backend.ts +5 -0
  393. package/src/security/oauth2.ts +42 -25
  394. package/src/security/secure-keys.ts +118 -25
  395. package/src/security/token-manager.ts +23 -10
  396. package/src/skills/catalog-files.ts +492 -0
  397. package/src/skills/inline-command-runner.ts +12 -14
  398. package/src/subagent/manager.ts +131 -26
  399. package/src/subagent/types.ts +19 -0
  400. package/src/tools/apps/executors.ts +11 -2
  401. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  402. package/src/tools/browser/auth-detector.ts +43 -12
  403. package/src/tools/browser/browser-execution.ts +645 -340
  404. package/src/tools/browser/browser-manager.ts +36 -12
  405. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  406. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  407. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  408. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  409. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  410. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  411. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  412. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  413. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  414. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  415. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  416. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  417. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  418. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  419. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  420. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  421. package/src/tools/browser/cdp-client/errors.ts +34 -0
  422. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  423. package/src/tools/browser/cdp-client/factory.ts +204 -0
  424. package/src/tools/browser/cdp-client/index.ts +14 -0
  425. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  426. package/src/tools/browser/cdp-client/types.ts +52 -0
  427. package/src/tools/filesystem/edit.ts +1 -1
  428. package/src/tools/filesystem/list.ts +1 -1
  429. package/src/tools/filesystem/read.ts +1 -1
  430. package/src/tools/filesystem/write.ts +2 -1
  431. package/src/tools/host-filesystem/edit.ts +1 -1
  432. package/src/tools/host-filesystem/read.ts +12 -15
  433. package/src/tools/host-filesystem/write.ts +1 -1
  434. package/src/tools/host-terminal/host-shell.ts +21 -16
  435. package/src/tools/permission-checker.ts +77 -100
  436. package/src/tools/registry.ts +0 -2
  437. package/src/tools/secret-detection-handler.ts +34 -1
  438. package/src/tools/shared/filesystem/image-read.ts +61 -40
  439. package/src/tools/skills/sandbox-runner.ts +3 -6
  440. package/src/tools/subagent/spawn.ts +47 -3
  441. package/src/tools/subagent/status.ts +2 -0
  442. package/src/tools/system/register.ts +2 -16
  443. package/src/tools/terminal/safe-env.ts +7 -0
  444. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  445. package/src/tools/terminal/sandbox.ts +4 -1
  446. package/src/tools/terminal/shell.ts +24 -21
  447. package/src/tools/tool-approval-handler.ts +48 -2
  448. package/src/tools/types.ts +2 -3
  449. package/src/util/platform.ts +14 -19
  450. package/src/watcher/provider-types.ts +1 -1
  451. package/src/workspace/migrations/029-seed-pkb.ts +1 -0
  452. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  453. package/src/workspace/migrations/registry.ts +2 -0
  454. package/src/workspace/top-level-renderer.ts +19 -1
  455. package/src/__tests__/chrome-cdp.test.ts +0 -419
  456. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  457. package/src/__tests__/permission-mode-store.test.ts +0 -277
  458. package/src/browser-extension-relay/protocol.ts +0 -63
  459. package/src/browser-extension-relay/server.ts +0 -203
  460. package/src/config/schemas/sandbox.ts +0 -14
  461. package/src/permissions/permission-mode-store.ts +0 -180
  462. package/src/tools/browser/chrome-cdp.ts +0 -239
  463. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -212,7 +212,7 @@ mock.module("../daemon/conversation-runtime-assembly.js", () => ({
212
212
  }));
213
213
 
214
214
  mock.module("../daemon/date-context.js", () => ({
215
- formatTurnTimestamp: () => "2026-01-01 (Thu) 00:00:00 +00:00 (UTC)",
215
+ formatTurnTimestamp: () => "2026-01-01 (Thursday) 00:00:00 +00:00 (UTC)",
216
216
  }));
217
217
 
218
218
  mock.module("../daemon/history-repair.js", () => ({
@@ -201,7 +201,7 @@ mock.module("../daemon/conversation-runtime-assembly.js", () => ({
201
201
  }));
202
202
 
203
203
  mock.module("../daemon/date-context.js", () => ({
204
- formatTurnTimestamp: () => "2026-01-01 (Thu) 00:00:00 +00:00 (UTC)",
204
+ formatTurnTimestamp: () => "2026-01-01 (Thursday) 00:00:00 +00:00 (UTC)",
205
205
  }));
206
206
 
207
207
  mock.module("../daemon/history-repair.js", () => ({
@@ -0,0 +1,169 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ mock.module("../util/logger.js", () => ({
4
+ getLogger: () =>
5
+ new Proxy({} as Record<string, unknown>, {
6
+ get: () => () => {},
7
+ }),
8
+ }));
9
+
10
+ const mockResolveConversationId = mock((id: string) => id);
11
+ const mockGetConversation = mock(() => ({
12
+ id: "conv-1",
13
+ title: "Source",
14
+ conversationType: "normal",
15
+ }));
16
+ const mockGetMessages = mock(() => [{ id: "m-source" }]);
17
+ const mockCreateConversation = mock(() => ({ id: "analysis-1" }));
18
+ const mockAddMessage = mock(async () => ({ id: "msg-1" }));
19
+
20
+ mock.module("../memory/conversation-key-store.js", () => ({
21
+ resolveConversationId: mockResolveConversationId,
22
+ }));
23
+
24
+ mock.module("../memory/conversation-crud.js", () => ({
25
+ getConversation: mockGetConversation,
26
+ getMessages: mockGetMessages,
27
+ createConversation: mockCreateConversation,
28
+ addMessage: mockAddMessage,
29
+ }));
30
+
31
+ mock.module("../export/transcript-formatter.js", () => ({
32
+ buildAnalysisTranscript: () => "user: hi",
33
+ }));
34
+
35
+ import { AssistantEventHub } from "../runtime/assistant-event-hub.js";
36
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
37
+ import type { SendMessageDeps } from "../runtime/http-types.js";
38
+ import { conversationAnalysisRouteDefinitions } from "../runtime/routes/conversation-analysis-routes.js";
39
+
40
+ beforeEach(() => {
41
+ mockResolveConversationId.mockClear();
42
+ mockGetConversation.mockClear();
43
+ mockGetMessages.mockClear();
44
+ mockCreateConversation.mockClear();
45
+ mockAddMessage.mockClear();
46
+ });
47
+
48
+ function makeConversation() {
49
+ return {
50
+ setTrustContext: mock(() => {}),
51
+ ensureActorScopedHistory: mock(() => Promise.resolve()),
52
+ setSubagentAllowedTools: mock(() => {}),
53
+ updateClient: mock(() => {}),
54
+ processing: false,
55
+ abortController: null as AbortController | null,
56
+ currentRequestId: null as string | null,
57
+ runAgentLoop: mock(() => Promise.resolve()),
58
+ };
59
+ }
60
+
61
+ describe("POST /v1/conversations/:id/analyze", () => {
62
+ test("runs headless analysis with unknown trust and no tools when no subscriber is present", async () => {
63
+ const conversation = makeConversation();
64
+ const assistantEventHub = new AssistantEventHub();
65
+ const sendMessageDeps = {
66
+ getOrCreateConversation: mock(async () => conversation),
67
+ assistantEventHub,
68
+ resolveAttachments: () => [],
69
+ } as unknown as SendMessageDeps;
70
+
71
+ const routes = conversationAnalysisRouteDefinitions({
72
+ sendMessageDeps,
73
+ buildConversationDetailResponse: () => ({ id: "analysis-1" }),
74
+ });
75
+ const route = routes.find(
76
+ (r) => r.method === "POST" && r.endpoint === "conversations/:id/analyze",
77
+ );
78
+ if (!route) throw new Error("analyze route missing");
79
+
80
+ const req = new Request("http://localhost/v1/conversations/conv-1/analyze", {
81
+ method: "POST",
82
+ });
83
+
84
+ const res = await route.handler({
85
+ req,
86
+ url: new URL(req.url),
87
+ server: null as never,
88
+ authContext: {} as never,
89
+ params: { id: "conv-1" },
90
+ });
91
+
92
+ expect(res.status).toBe(200);
93
+ expect(mockAddMessage).toHaveBeenCalledWith(
94
+ "analysis-1",
95
+ "user",
96
+ expect.any(String),
97
+ { provenanceTrustClass: "unknown" },
98
+ );
99
+ expect(conversation.setTrustContext).toHaveBeenCalledWith({
100
+ trustClass: "unknown",
101
+ sourceChannel: "vellum",
102
+ });
103
+ expect(conversation.ensureActorScopedHistory).toHaveBeenCalledTimes(1);
104
+ expect(conversation.setSubagentAllowedTools).toHaveBeenCalledTimes(1);
105
+ const allowedTools = (
106
+ conversation.setSubagentAllowedTools.mock.calls as unknown as Array<
107
+ [Set<string> | undefined]
108
+ >
109
+ )[0]?.[0];
110
+ expect(allowedTools).toBeInstanceOf(Set);
111
+ expect(allowedTools?.size).toBe(0);
112
+ expect(conversation.updateClient).toHaveBeenCalledWith(
113
+ expect.any(Function),
114
+ true,
115
+ );
116
+ expect(conversation.runAgentLoop).toHaveBeenCalledWith(
117
+ expect.any(String),
118
+ "msg-1",
119
+ expect.any(Function),
120
+ expect.objectContaining({ isInteractive: false, isUserMessage: true }),
121
+ );
122
+ });
123
+
124
+ test("keeps analysis non-interactive even when a matching subscriber is connected", async () => {
125
+ const conversation = makeConversation();
126
+ const assistantEventHub = new AssistantEventHub();
127
+ assistantEventHub.subscribe(
128
+ { assistantId: DAEMON_INTERNAL_ASSISTANT_ID },
129
+ () => {},
130
+ );
131
+ const sendMessageDeps = {
132
+ getOrCreateConversation: mock(async () => conversation),
133
+ assistantEventHub,
134
+ resolveAttachments: () => [],
135
+ } as unknown as SendMessageDeps;
136
+
137
+ const routes = conversationAnalysisRouteDefinitions({
138
+ sendMessageDeps,
139
+ buildConversationDetailResponse: () => ({ id: "analysis-1" }),
140
+ });
141
+ const route = routes.find(
142
+ (r) => r.method === "POST" && r.endpoint === "conversations/:id/analyze",
143
+ );
144
+ if (!route) throw new Error("analyze route missing");
145
+
146
+ const req = new Request("http://localhost/v1/conversations/conv-1/analyze", {
147
+ method: "POST",
148
+ });
149
+
150
+ await route.handler({
151
+ req,
152
+ url: new URL(req.url),
153
+ server: null as never,
154
+ authContext: {} as never,
155
+ params: { id: "conv-1" },
156
+ });
157
+
158
+ expect(conversation.updateClient).toHaveBeenCalledWith(
159
+ expect.any(Function),
160
+ false,
161
+ );
162
+ expect(conversation.runAgentLoop).toHaveBeenCalledWith(
163
+ expect.any(String),
164
+ "msg-1",
165
+ expect.any(Function),
166
+ expect.objectContaining({ isInteractive: false, isUserMessage: true }),
167
+ );
168
+ });
169
+ });
@@ -1,6 +1,8 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { beforeEach, describe, expect, mock, test } from "bun:test";
3
3
 
4
+ import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
5
+
4
6
  mock.module("../util/logger.js", () => ({
5
7
  getLogger: () =>
6
8
  new Proxy({} as Record<string, unknown>, {
@@ -25,15 +27,18 @@ mock.module("../daemon/video-thumbnail.js", () => ({
25
27
  }));
26
28
 
27
29
  // Stub out permission checker / trust store
30
+ const checkSpy = mock(() => Promise.resolve({ decision: "allow" }));
31
+ const addRuleSpy = mock(() => {});
32
+
28
33
  mock.module("../permissions/checker.js", () => ({
29
- check: () => Promise.resolve({ decision: "allow" }),
34
+ check: checkSpy,
30
35
  classifyRisk: () => Promise.resolve("low"),
31
36
  generateAllowlistOptions: () => Promise.resolve([]),
32
37
  generateScopeOptions: () => [],
33
38
  }));
34
39
 
35
40
  mock.module("../permissions/trust-store.js", () => ({
36
- addRule: () => {},
41
+ addRule: addRuleSpy,
37
42
  }));
38
43
 
39
44
  mock.module("../permissions/types.js", () => ({
@@ -47,7 +52,11 @@ mock.module("../permissions/types.js", () => ({
47
52
 
48
53
  import type { AssistantAttachmentDraft } from "../daemon/assistant-attachments.js";
49
54
  import { getFilePathForAttachment } from "../memory/attachments-store.js";
50
- import { addMessage, createConversation } from "../memory/conversation-crud.js";
55
+ import {
56
+ addMessage,
57
+ createConversation,
58
+ updateConversationHostAccess,
59
+ } from "../memory/conversation-crud.js";
51
60
  import { getDb, initializeDb } from "../memory/db.js";
52
61
 
53
62
  initializeDb();
@@ -73,7 +82,12 @@ function makeBase64(bytes: number): string {
73
82
  // ---------------------------------------------------------------------------
74
83
 
75
84
  describe("resolveAssistantAttachments", () => {
76
- beforeEach(resetTables);
85
+ beforeEach(() => {
86
+ resetTables();
87
+ _setOverridesForTesting({});
88
+ checkSpy.mockClear();
89
+ addRuleSpy.mockClear();
90
+ });
77
91
 
78
92
  test("small attachments are stored on disk via uploadAttachment", async () => {
79
93
  const conv = createConversation("test-conv");
@@ -196,3 +210,65 @@ describe("resolveAssistantAttachments", () => {
196
210
  expect(emitted.fileBacked).toBe(true);
197
211
  });
198
212
  });
213
+
214
+ describe("approveHostAttachmentRead", () => {
215
+ beforeEach(() => {
216
+ resetTables();
217
+ _setOverridesForTesting({});
218
+ checkSpy.mockClear();
219
+ addRuleSpy.mockClear();
220
+ });
221
+
222
+ test("uses the conversation-scoped host-access prompt under v2", async () => {
223
+ _setOverridesForTesting({ "permission-controls-v2": true });
224
+ const conversation = createConversation("attachment-host-gate");
225
+ const promptSpy = mock(() =>
226
+ Promise.resolve({ decision: "allow" as const }),
227
+ );
228
+
229
+ const { approveHostAttachmentRead } =
230
+ await import("../daemon/conversation-attachments.js");
231
+
232
+ const allowed = await approveHostAttachmentRead(
233
+ "/tmp/example.txt",
234
+ "/tmp",
235
+ { prompt: promptSpy } as never,
236
+ conversation.id,
237
+ false,
238
+ );
239
+
240
+ expect(allowed).toBe(true);
241
+ expect(checkSpy).not.toHaveBeenCalled();
242
+ expect(addRuleSpy).not.toHaveBeenCalled();
243
+ const call = promptSpy.mock.calls[0] as unknown as unknown[];
244
+ expect(call[3]).toEqual([]);
245
+ expect(call[4]).toEqual([]);
246
+ expect(call[8]).toBe(false);
247
+ expect(call[10]).toBeUndefined();
248
+ expect(call[12]).toBe(true);
249
+ });
250
+
251
+ test("auto-allows host attachment reads when the conversation already has host access", async () => {
252
+ _setOverridesForTesting({ "permission-controls-v2": true });
253
+ const conversation = createConversation("attachment-host-allowed");
254
+ updateConversationHostAccess(conversation.id, true);
255
+ const promptSpy = mock(() =>
256
+ Promise.resolve({ decision: "deny" as const }),
257
+ );
258
+
259
+ const { approveHostAttachmentRead } =
260
+ await import("../daemon/conversation-attachments.js");
261
+
262
+ const allowed = await approveHostAttachmentRead(
263
+ "/tmp/example.txt",
264
+ "/tmp",
265
+ { prompt: promptSpy } as never,
266
+ conversation.id,
267
+ false,
268
+ );
269
+
270
+ expect(allowed).toBe(true);
271
+ expect(promptSpy).not.toHaveBeenCalled();
272
+ expect(checkSpy).not.toHaveBeenCalled();
273
+ });
274
+ });
@@ -214,6 +214,8 @@ mock.module("../memory/canonical-guardian-store.js", () => ({
214
214
  // ---------------------------------------------------------------------------
215
215
 
216
216
  import { Conversation } from "../daemon/conversation.js";
217
+ import { HostBashProxy } from "../daemon/host-bash-proxy.js";
218
+ import { HostBrowserProxy } from "../daemon/host-browser-proxy.js";
217
219
 
218
220
  // ---------------------------------------------------------------------------
219
221
  // Helpers
@@ -558,3 +560,156 @@ describe("sendToClient receives state signals", () => {
558
560
  });
559
561
  });
560
562
  });
563
+
564
+ describe("restoreBrowserProxyAvailability", () => {
565
+ test("re-enables only the host browser proxy after clearProxyAvailability", () => {
566
+ const conversation = makeConversation();
567
+ const browserProxy = new HostBrowserProxy(() => {});
568
+ const bashProxy = new HostBashProxy(() => {});
569
+ conversation.setHostBrowserProxy(browserProxy);
570
+ conversation.setHostBashProxy(bashProxy);
571
+
572
+ // Mark as having a connected client (interactive desktop path).
573
+ conversation.updateClient(() => {}, false);
574
+ expect(browserProxy.isAvailable()).toBe(true);
575
+ expect(bashProxy.isAvailable()).toBe(true);
576
+
577
+ // The drain queue clears all proxies for non-interactive turns.
578
+ conversation.clearProxyAvailability();
579
+ expect(browserProxy.isAvailable()).toBe(false);
580
+ expect(bashProxy.isAvailable()).toBe(false);
581
+
582
+ // restoreBrowserProxyAvailability should bring back ONLY the browser proxy.
583
+ conversation.restoreBrowserProxyAvailability();
584
+ expect(browserProxy.isAvailable()).toBe(true);
585
+ expect(bashProxy.isAvailable()).toBe(false);
586
+ });
587
+
588
+ test("re-enables the browser proxy even when hasNoClient is true (chrome-extension)", () => {
589
+ // Regression: chrome-extension is non-interactive (hasNoClient stays
590
+ // true so host_bash/host_file tools remain gated), but we still need
591
+ // to provision the hostBrowserProxy so it can service CDP commands.
592
+ // The helper must NOT gate on hasNoClient.
593
+ const conversation = makeConversation();
594
+ const browserProxy = new HostBrowserProxy(() => {});
595
+ conversation.setHostBrowserProxy(browserProxy);
596
+
597
+ // updateClient with hasNoClient=true emulates the non-interactive
598
+ // chrome-extension turn. Host proxies start disabled because
599
+ // updateClient propagates hasNoClient through to updateSender.
600
+ conversation.updateClient(() => {}, true);
601
+ expect(browserProxy.isAvailable()).toBe(false);
602
+ expect(conversation["hasNoClient"]).toBe(true);
603
+
604
+ // The targeted helper bypasses the hasNoClient gate so the
605
+ // single-capability chrome-extension turn can drive the browser
606
+ // via CDP without flipping hasNoClient (which would also enable
607
+ // host_bash/host_file gating downstream).
608
+ conversation.restoreBrowserProxyAvailability();
609
+ expect(browserProxy.isAvailable()).toBe(true);
610
+ // hasNoClient itself MUST remain true so that
611
+ // isToolActiveForContext keeps host_bash/host_file/host_cu gated.
612
+ expect(conversation["hasNoClient"]).toBe(true);
613
+ });
614
+
615
+ test("leaves bash/file/cu proxies disabled when called for chrome-extension", () => {
616
+ // Regression: the targeted helper must not accidentally re-enable
617
+ // proxies other than host_browser, even when called from a path that
618
+ // owns multiple proxies (e.g. macOS holdover state with hasNoClient
619
+ // forced true for an explicit non-interactive run).
620
+ const conversation = makeConversation();
621
+ const browserProxy = new HostBrowserProxy(() => {});
622
+ const bashProxy = new HostBashProxy(() => {});
623
+ conversation.setHostBrowserProxy(browserProxy);
624
+ conversation.setHostBashProxy(bashProxy);
625
+
626
+ conversation.updateClient(() => {}, true);
627
+ expect(browserProxy.isAvailable()).toBe(false);
628
+ expect(bashProxy.isAvailable()).toBe(false);
629
+
630
+ conversation.restoreBrowserProxyAvailability();
631
+ expect(browserProxy.isAvailable()).toBe(true);
632
+ // Crucial: bash proxy stays disabled. The helper must touch ONLY the
633
+ // browser proxy.
634
+ expect(bashProxy.isAvailable()).toBe(false);
635
+ });
636
+
637
+ test("uses hostBrowserSenderOverride when set so drain-queue restores preserve the registry-routed sender", () => {
638
+ // Regression (PR #24129 cycle 2): the queue-drain path calls
639
+ // `restoreBrowserProxyAvailability()` on dequeue, which used to pass
640
+ // `this.sendToClient` (the SSE hub emitter) to the proxy, clobbering the
641
+ // chrome-extension registry-routed sender established by the POST
642
+ // /messages handler. The override field lets the HTTP handler pin the
643
+ // registry-routed sender so the drain path preserves it.
644
+ const sseHub: ServerMessage[] = [];
645
+ const registry: ServerMessage[] = [];
646
+ const conversation = makeConversation((msg) => sseHub.push(msg));
647
+ const browserProxy = new HostBrowserProxy(() => {});
648
+ conversation.setHostBrowserProxy(browserProxy);
649
+
650
+ // Simulate updateClient setting sendToClient to the SSE hub and
651
+ // marking the conversation as client-less (chrome-extension is
652
+ // non-interactive).
653
+ conversation.updateClient((msg) => sseHub.push(msg), true);
654
+ expect(browserProxy.isAvailable()).toBe(false);
655
+
656
+ // The HTTP handler stashes the registry-routed sender as the override.
657
+ const registrySender = (msg: ServerMessage) => registry.push(msg);
658
+ conversation.hostBrowserSenderOverride = registrySender;
659
+
660
+ // Drain-queue path calls restoreBrowserProxyAvailability — it must now
661
+ // prefer the override over sendToClient.
662
+ conversation.restoreBrowserProxyAvailability();
663
+ expect(browserProxy.isAvailable()).toBe(true);
664
+
665
+ // Send a frame through the proxy and verify it flows through the
666
+ // registry sender, not the SSE hub.
667
+ const internalSend = (
668
+ browserProxy as unknown as {
669
+ sendToClient: (msg: ServerMessage) => void;
670
+ }
671
+ ).sendToClient;
672
+ const probe: ServerMessage = {
673
+ type: "host_browser_cancel",
674
+ requestId: "probe-1",
675
+ } as ServerMessage;
676
+ internalSend(probe);
677
+ expect(registry).toHaveLength(1);
678
+ expect(sseHub.some((m) => m === probe)).toBe(false);
679
+ });
680
+
681
+ test("falls back to sendToClient when hostBrowserSenderOverride is cleared", () => {
682
+ // When a non-chrome-extension turn takes over, the HTTP handler clears
683
+ // the override and restoreBrowserProxyAvailability must fall back to
684
+ // sendToClient (the SSE hub), otherwise macOS turns would route their
685
+ // host_browser frames through the stale chrome-extension registry.
686
+ const sseHub: ServerMessage[] = [];
687
+ const conversation = makeConversation((msg) => sseHub.push(msg));
688
+ const browserProxy = new HostBrowserProxy(() => {});
689
+ conversation.setHostBrowserProxy(browserProxy);
690
+
691
+ // First the chrome-extension path pins the override.
692
+ const registry: ServerMessage[] = [];
693
+ conversation.hostBrowserSenderOverride = (msg) => registry.push(msg);
694
+ conversation.updateClient((msg) => sseHub.push(msg), true);
695
+ conversation.restoreBrowserProxyAvailability();
696
+
697
+ // Then a macOS handoff clears the override.
698
+ conversation.hostBrowserSenderOverride = undefined;
699
+ conversation.updateClient((msg) => sseHub.push(msg), false);
700
+ conversation.restoreBrowserProxyAvailability();
701
+
702
+ const internalSend = (
703
+ browserProxy as unknown as {
704
+ sendToClient: (msg: ServerMessage) => void;
705
+ }
706
+ ).sendToClient;
707
+ const probe: ServerMessage = {
708
+ type: "host_browser_cancel",
709
+ requestId: "probe-2",
710
+ } as ServerMessage;
711
+ internalSend(probe);
712
+ expect(sseHub).toContain(probe);
713
+ expect(registry).not.toContain(probe);
714
+ });
715
+ });
@@ -0,0 +1,105 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ getConversationDirName,
5
+ parseConversationDirName,
6
+ } from "../memory/conversation-directories.js";
7
+
8
+ describe("parseConversationDirName", () => {
9
+ describe("round-trip with getConversationDirName", () => {
10
+ test("round-trips a UUID-shaped id", () => {
11
+ const id = "4ae7ea90-86e4-446a-8673-7bba94ecfea1";
12
+ const createdAtMs = Date.parse("2026-04-07T10:47:23.075Z");
13
+ const name = getConversationDirName(id, createdAtMs);
14
+ const parsed = parseConversationDirName(name);
15
+ expect(parsed).toEqual({ conversationId: id, createdAtMs });
16
+ });
17
+
18
+ test("round-trips an id with embedded hyphens", () => {
19
+ const id = "conv-with-hyphens-123";
20
+ const createdAtMs = Date.parse("2024-01-15T00:00:00.000Z");
21
+ const name = getConversationDirName(id, createdAtMs);
22
+ const parsed = parseConversationDirName(name);
23
+ expect(parsed).toEqual({ conversationId: id, createdAtMs });
24
+ });
25
+
26
+ test("round-trips an id with embedded underscores", () => {
27
+ const id = "foo_bar_baz";
28
+ const createdAtMs = Date.parse("2025-12-31T23:59:59.999Z");
29
+ const name = getConversationDirName(id, createdAtMs);
30
+ const parsed = parseConversationDirName(name);
31
+ expect(parsed).toEqual({ conversationId: id, createdAtMs });
32
+ });
33
+ });
34
+
35
+ describe("exact parsing against literal example", () => {
36
+ test("parses the canonical example from the spec", () => {
37
+ const name =
38
+ "2026-04-07T10-47-23.075Z_4ae7ea90-86e4-446a-8673-7bba94ecfea1";
39
+ const parsed = parseConversationDirName(name);
40
+ expect(parsed).not.toBeNull();
41
+ expect(parsed?.conversationId).toBe(
42
+ "4ae7ea90-86e4-446a-8673-7bba94ecfea1",
43
+ );
44
+ expect(parsed?.createdAtMs).toBe(Date.parse("2026-04-07T10:47:23.075Z"));
45
+ });
46
+ });
47
+
48
+ describe("returns null for malformed input", () => {
49
+ test("returns null for empty string", () => {
50
+ expect(parseConversationDirName("")).toBeNull();
51
+ });
52
+
53
+ test("returns null for missing underscore", () => {
54
+ expect(parseConversationDirName("2026-04-07T10-47-23.075Z")).toBeNull();
55
+ });
56
+
57
+ test("returns null for legacy format (id first, timestamp second)", () => {
58
+ expect(
59
+ parseConversationDirName(
60
+ "4ae7ea90-86e4-446a-8673-7bba94ecfea1_2026-04-07T10-47-23.075Z",
61
+ ),
62
+ ).toBeNull();
63
+ });
64
+
65
+ test("returns null for non-ISO prefix", () => {
66
+ expect(parseConversationDirName("hello_world")).toBeNull();
67
+ });
68
+
69
+ test("returns null for random garbage", () => {
70
+ expect(parseConversationDirName("drafts/foo")).toBeNull();
71
+ });
72
+
73
+ test("returns null when the conversation id is '.'", () => {
74
+ expect(parseConversationDirName("2025-01-15T00-00-00.000Z_.")).toBeNull();
75
+ });
76
+
77
+ test("returns null when the conversation id is '..'", () => {
78
+ expect(
79
+ parseConversationDirName("2025-01-15T00-00-00.000Z_.."),
80
+ ).toBeNull();
81
+ });
82
+
83
+ test("returns null when the conversation id contains a forward slash", () => {
84
+ expect(
85
+ parseConversationDirName("2025-01-15T00-00-00.000Z_foo/bar"),
86
+ ).toBeNull();
87
+ });
88
+
89
+ test("returns null when the conversation id contains a backslash", () => {
90
+ expect(
91
+ parseConversationDirName("2025-01-15T00-00-00.000Z_foo\\bar"),
92
+ ).toBeNull();
93
+ });
94
+ });
95
+
96
+ describe("ids containing underscores", () => {
97
+ test("captures everything after the timestamp as the conversationId", () => {
98
+ const name = "2026-04-07T10-47-23.075Z_my_test_id";
99
+ const parsed = parseConversationDirName(name);
100
+ expect(parsed).not.toBeNull();
101
+ expect(parsed?.conversationId).toBe("my_test_id");
102
+ expect(parsed?.createdAtMs).toBe(Date.parse("2026-04-07T10:47:23.075Z"));
103
+ });
104
+ });
105
+ });
@@ -35,6 +35,7 @@ import {
35
35
  addMessage,
36
36
  createConversation,
37
37
  forkConversation,
38
+ getConversationHostAccess,
38
39
  getMessages,
39
40
  PRIVATE_CONVERSATION_FORK_ERROR,
40
41
  } from "../memory/conversation-crud.js";
@@ -138,6 +139,22 @@ describe("forkConversation", () => {
138
139
  ).toBe(true);
139
140
  });
140
141
 
142
+ test("forked conversations start with host access disabled", async () => {
143
+ const source = createConversation({
144
+ title: "Computer-enabled thread",
145
+ hostAccess: true,
146
+ });
147
+ await addMessage(source.id, "user", "Use the computer", undefined, {
148
+ skipIndexing: true,
149
+ });
150
+
151
+ const fork = forkConversation({ conversationId: source.id });
152
+
153
+ expect(getConversationHostAccess(source.id)).toBe(true);
154
+ expect(getConversationHostAccess(fork.id)).toBe(false);
155
+ expect(fork.hostAccess).toBe(0);
156
+ });
157
+
141
158
  test("preserves source order when source messages share a timestamp", () => {
142
159
  const source = createConversation("Equal timestamp thread");
143
160
  const db = getDb();
@@ -747,6 +747,7 @@ describe("web_search_tool_result structural guard", () => {
747
747
  // web_search_tool_result has a structurally different content format
748
748
  // (array of web_search_result objects) and is not truncated this way.
749
749
  "context/tool-result-truncation.ts",
750
+ "context/post-turn-tool-result-truncation.ts",
750
751
 
751
752
  // Anthropic provider type guards define API-specific discriminants.
752
753
  // It has a separate isWebSearchToolResultBlock for the other type.