@vellumai/assistant 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docs/architecture/memory.md +1 -1
  4. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  5. package/openapi.yaml +184 -69
  6. package/package.json +41 -41
  7. package/scripts/generate-openapi.ts +1 -2
  8. package/src/__tests__/acp-session.test.ts +43 -0
  9. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  10. package/src/__tests__/app-executors.test.ts +1 -0
  11. package/src/__tests__/app-source-watcher.test.ts +37 -11
  12. package/src/__tests__/approval-routes-http.test.ts +178 -1
  13. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  14. package/src/__tests__/browser-manager.test.ts +40 -27
  15. package/src/__tests__/catalog-files.test.ts +862 -0
  16. package/src/__tests__/channel-approvals.test.ts +53 -0
  17. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +125 -48
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  21. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  22. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  24. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  25. package/src/__tests__/conversation-attachments.test.ts +80 -4
  26. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  27. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  28. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  29. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  30. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  31. package/src/__tests__/conversation-queue.test.ts +45 -2
  32. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  33. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  34. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  35. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  36. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  37. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  38. package/src/__tests__/conversation-store.test.ts +195 -0
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  40. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  41. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  42. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  43. package/src/__tests__/credential-vault.test.ts +152 -13
  44. package/src/__tests__/credentials-cli.test.ts +2 -2
  45. package/src/__tests__/date-context.test.ts +4 -4
  46. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  47. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  48. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  49. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  50. package/src/__tests__/gemini-provider.test.ts +2 -2
  51. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  52. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  53. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  54. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  55. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  56. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  57. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  58. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  59. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  60. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  61. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  62. package/src/__tests__/host-browser-routes.test.ts +198 -0
  63. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  64. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  65. package/src/__tests__/host-file-proxy.test.ts +185 -1
  66. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  67. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  68. package/src/__tests__/host-shell-tool.test.ts +1 -11
  69. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  70. package/src/__tests__/integration-status.test.ts +6 -7
  71. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  72. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  73. package/src/__tests__/mcp-health-check.test.ts +10 -3
  74. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  75. package/src/__tests__/migration-export-http.test.ts +61 -2
  76. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  77. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  78. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  80. package/src/__tests__/oauth-cli.test.ts +707 -60
  81. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  82. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  83. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  84. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  85. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  86. package/src/__tests__/oauth-store.test.ts +1386 -182
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  88. package/src/__tests__/onboarding-template-contract.test.ts +75 -57
  89. package/src/__tests__/openai-provider.test.ts +2 -2
  90. package/src/__tests__/outlook-categories.test.ts +1 -1
  91. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  92. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  93. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  94. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  95. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  96. package/src/__tests__/outlook-trash.test.ts +1 -1
  97. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  98. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  99. package/src/__tests__/permission-mode.test.ts +28 -56
  100. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  101. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  102. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  103. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  104. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  105. package/src/__tests__/schedule-routes.test.ts +162 -0
  106. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  107. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  108. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  109. package/src/__tests__/set-permission-mode.test.ts +13 -250
  110. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  111. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  112. package/src/__tests__/slack-channel-config.test.ts +12 -15
  113. package/src/__tests__/subagent-detail.test.ts +44 -2
  114. package/src/__tests__/subagent-disposal.test.ts +1 -0
  115. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  116. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  117. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  118. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  119. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  120. package/src/__tests__/subagent-tools.test.ts +1 -0
  121. package/src/__tests__/subagent-types.test.ts +1 -0
  122. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  123. package/src/__tests__/system-prompt.test.ts +72 -1
  124. package/src/__tests__/task-scheduler.test.ts +32 -6
  125. package/src/__tests__/telegram-config.test.ts +10 -13
  126. package/src/__tests__/terminal-tools.test.ts +9 -0
  127. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  128. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  129. package/src/__tests__/top-level-renderer.test.ts +73 -1
  130. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  131. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  132. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  133. package/src/acp/client-handler.ts +30 -4
  134. package/src/agent/loop.ts +12 -6
  135. package/src/approvals/guardian-request-resolvers.ts +21 -15
  136. package/src/browser-session/__tests__/manager.test.ts +297 -0
  137. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  138. package/src/browser-session/backends/extension.ts +26 -0
  139. package/src/browser-session/backends/local.ts +24 -0
  140. package/src/browser-session/events.ts +164 -0
  141. package/src/browser-session/index.ts +27 -0
  142. package/src/browser-session/manager.ts +159 -0
  143. package/src/browser-session/types.ts +28 -0
  144. package/src/channels/__tests__/types.test.ts +134 -0
  145. package/src/channels/types.ts +53 -3
  146. package/src/cli/commands/browser-relay.ts +339 -409
  147. package/src/cli/commands/credentials.ts +3 -3
  148. package/src/cli/commands/email.ts +18 -13
  149. package/src/cli/commands/mcp.ts +16 -4
  150. package/src/cli/commands/oauth/__tests__/connect.test.ts +44 -44
  151. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  152. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  153. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  154. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  155. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  156. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  157. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  158. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  159. package/src/cli/commands/oauth/apps.ts +7 -4
  160. package/src/cli/commands/oauth/connect.ts +6 -3
  161. package/src/cli/commands/oauth/disconnect.ts +1 -1
  162. package/src/cli/commands/oauth/providers.ts +200 -36
  163. package/src/cli/commands/oauth/shared.ts +5 -5
  164. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  165. package/src/cli/commands/platform/index.ts +107 -10
  166. package/src/cli/commands/usage.ts +10 -9
  167. package/src/cli/lib/daemon-credential-client.ts +4 -0
  168. package/src/cli/program.ts +1 -1
  169. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  170. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  171. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  172. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  173. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  174. package/src/config/bundled-skills/document/SKILL.md +4 -0
  175. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  176. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  177. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  178. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  179. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  180. package/src/config/env-registry.ts +14 -0
  181. package/src/config/env.ts +21 -0
  182. package/src/config/feature-flag-registry.json +44 -5
  183. package/src/config/loader.ts +56 -1
  184. package/src/config/sanitize-for-transfer.ts +47 -0
  185. package/src/config/schema.ts +46 -5
  186. package/src/config/schemas/host-browser.ts +66 -0
  187. package/src/config/schemas/memory-lifecycle.ts +1 -1
  188. package/src/config/schemas/memory-retrieval.ts +103 -0
  189. package/src/config/schemas/security.ts +0 -6
  190. package/src/config/schemas/services.ts +8 -0
  191. package/src/config/types.ts +0 -1
  192. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  193. package/src/context/window-manager.ts +19 -1
  194. package/src/credential-execution/approval-bridge.ts +49 -15
  195. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  196. package/src/daemon/app-source-watcher.ts +35 -0
  197. package/src/daemon/context-overflow-approval.ts +5 -0
  198. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  199. package/src/daemon/conversation-agent-loop.ts +58 -24
  200. package/src/daemon/conversation-attachments.ts +40 -0
  201. package/src/daemon/conversation-process.ts +48 -1
  202. package/src/daemon/conversation-runtime-assembly.ts +118 -36
  203. package/src/daemon/conversation-surfaces.ts +37 -36
  204. package/src/daemon/conversation-tool-setup.ts +74 -8
  205. package/src/daemon/conversation-workspace.ts +12 -0
  206. package/src/daemon/conversation.ts +226 -8
  207. package/src/daemon/date-context.ts +10 -10
  208. package/src/daemon/first-greeting.ts +3 -2
  209. package/src/daemon/handlers/conversations.ts +9 -140
  210. package/src/daemon/handlers/shared.ts +58 -0
  211. package/src/daemon/handlers/skills.ts +232 -37
  212. package/src/daemon/host-bash-proxy.ts +48 -13
  213. package/src/daemon/host-browser-proxy.ts +191 -0
  214. package/src/daemon/host-cu-proxy.ts +36 -11
  215. package/src/daemon/host-file-proxy.ts +57 -9
  216. package/src/daemon/lifecycle.ts +65 -11
  217. package/src/daemon/message-protocol.ts +7 -0
  218. package/src/daemon/message-types/conversations.ts +55 -13
  219. package/src/daemon/message-types/host-browser.ts +100 -0
  220. package/src/daemon/message-types/messages.ts +5 -5
  221. package/src/daemon/message-types/skills.ts +10 -0
  222. package/src/daemon/message-types/subagents.ts +2 -0
  223. package/src/daemon/server.ts +92 -12
  224. package/src/daemon/tool-side-effects.ts +6 -0
  225. package/src/daemon/transport-hints.ts +5 -24
  226. package/src/inbound/platform-callback-registration.ts +18 -17
  227. package/src/mcp/client.ts +59 -24
  228. package/src/memory/app-store.ts +31 -1
  229. package/src/memory/conversation-crud.ts +23 -0
  230. package/src/memory/conversation-starters-cadence.ts +76 -0
  231. package/src/memory/conversation-title-service.ts +5 -2
  232. package/src/memory/db-init.ts +12 -0
  233. package/src/memory/embedding-backend.test.ts +75 -0
  234. package/src/memory/embedding-backend.ts +131 -5
  235. package/src/memory/embedding-gemini.test.ts +54 -0
  236. package/src/memory/embedding-gemini.ts +20 -9
  237. package/src/memory/embedding-local.ts +176 -17
  238. package/src/memory/graph/consolidation.ts +10 -23
  239. package/src/memory/graph/extraction-job.ts +15 -0
  240. package/src/memory/graph/retriever.ts +40 -22
  241. package/src/memory/graph/store.test.ts +7 -3
  242. package/src/memory/graph/store.ts +47 -12
  243. package/src/memory/llm-usage-store.ts +45 -4
  244. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  245. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  246. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  247. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  248. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  249. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  250. package/src/memory/migrations/index.ts +6 -0
  251. package/src/memory/migrations/registry.ts +8 -0
  252. package/src/memory/schema/conversations.ts +1 -0
  253. package/src/memory/schema/oauth.ts +18 -13
  254. package/src/oauth/AGENTS.md +76 -0
  255. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  256. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  257. package/src/oauth/byo-connection.test.ts +8 -8
  258. package/src/oauth/byo-connection.ts +7 -7
  259. package/src/oauth/connect-orchestrator.ts +23 -21
  260. package/src/oauth/connect-types.ts +3 -3
  261. package/src/oauth/connection-resolver.test.ts +17 -4
  262. package/src/oauth/connection-resolver.ts +16 -16
  263. package/src/oauth/connection.ts +1 -1
  264. package/src/oauth/manual-token-connection.ts +13 -13
  265. package/src/oauth/oauth-store.ts +214 -100
  266. package/src/oauth/platform-connection.test.ts +3 -3
  267. package/src/oauth/platform-connection.ts +4 -4
  268. package/src/oauth/provider-serializer.ts +31 -5
  269. package/src/oauth/revoke.ts +76 -0
  270. package/src/oauth/seed-providers.ts +126 -87
  271. package/src/oauth/token-persistence.ts +1 -1
  272. package/src/permissions/permission-mode.ts +4 -11
  273. package/src/permissions/prompter.ts +13 -1
  274. package/src/permissions/v2-consent-policy.ts +87 -0
  275. package/src/prompts/system-prompt.ts +18 -21
  276. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  277. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  278. package/src/providers/anthropic/client.ts +1 -0
  279. package/src/providers/types.ts +1 -1
  280. package/src/runtime/AGENTS.md +23 -0
  281. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  282. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  283. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  284. package/src/runtime/assistant-event-hub.ts +2 -2
  285. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  286. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  287. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  288. package/src/runtime/auth/middleware.ts +98 -0
  289. package/src/runtime/auth/route-policy.ts +6 -7
  290. package/src/runtime/capability-tokens.ts +414 -0
  291. package/src/runtime/channel-approvals.ts +18 -5
  292. package/src/runtime/chrome-extension-registry.ts +332 -0
  293. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  294. package/src/runtime/guardian-decision-types.ts +7 -0
  295. package/src/runtime/http-server.ts +425 -70
  296. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  297. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  298. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  299. package/src/runtime/migrations/migration-transport.ts +6 -0
  300. package/src/runtime/migrations/migration-wizard.ts +22 -2
  301. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  302. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  303. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  304. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  305. package/src/runtime/pending-interactions.ts +29 -13
  306. package/src/runtime/routes/approval-routes.ts +90 -16
  307. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  308. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  309. package/src/runtime/routes/conversation-analysis-routes.ts +2 -1
  310. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  311. package/src/runtime/routes/conversation-routes.ts +301 -27
  312. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  313. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  314. package/src/runtime/routes/host-browser-routes.ts +279 -0
  315. package/src/runtime/routes/host-file-routes.ts +9 -1
  316. package/src/runtime/routes/identity-routes.ts +259 -16
  317. package/src/runtime/routes/log-export-routes.ts +42 -22
  318. package/src/runtime/routes/memory-item-routes.ts +1 -7
  319. package/src/runtime/routes/migration-routes.ts +87 -2
  320. package/src/runtime/routes/oauth-apps.ts +15 -17
  321. package/src/runtime/routes/oauth-providers.ts +4 -0
  322. package/src/runtime/routes/schedule-routes.ts +24 -11
  323. package/src/runtime/routes/settings-routes.ts +9 -97
  324. package/src/runtime/routes/skills-routes.ts +52 -2
  325. package/src/runtime/routes/subagents-routes.ts +14 -10
  326. package/src/runtime/routes/usage-routes.ts +8 -7
  327. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  328. package/src/runtime/routes/workspace-routes.ts +8 -1
  329. package/src/runtime/routes/workspace-utils.ts +2 -0
  330. package/src/schedule/scheduler.ts +7 -5
  331. package/src/security/ces-credential-client.ts +20 -0
  332. package/src/security/ces-rpc-credential-backend.ts +17 -0
  333. package/src/security/credential-backend.ts +5 -0
  334. package/src/security/oauth2.ts +42 -25
  335. package/src/security/secure-keys.ts +118 -25
  336. package/src/security/token-manager.ts +23 -10
  337. package/src/skills/catalog-files.ts +492 -0
  338. package/src/subagent/manager.ts +131 -26
  339. package/src/subagent/types.ts +19 -0
  340. package/src/tools/apps/executors.ts +11 -2
  341. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  342. package/src/tools/browser/auth-detector.ts +43 -12
  343. package/src/tools/browser/browser-execution.ts +645 -340
  344. package/src/tools/browser/browser-manager.ts +36 -12
  345. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  346. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  347. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  348. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  349. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  350. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  351. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  352. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  353. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  354. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  355. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  356. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  357. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  358. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  359. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  360. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  361. package/src/tools/browser/cdp-client/errors.ts +34 -0
  362. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  363. package/src/tools/browser/cdp-client/factory.ts +204 -0
  364. package/src/tools/browser/cdp-client/index.ts +14 -0
  365. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  366. package/src/tools/browser/cdp-client/types.ts +52 -0
  367. package/src/tools/filesystem/edit.ts +1 -1
  368. package/src/tools/filesystem/list.ts +1 -1
  369. package/src/tools/filesystem/read.ts +1 -1
  370. package/src/tools/filesystem/write.ts +2 -1
  371. package/src/tools/host-filesystem/edit.ts +1 -1
  372. package/src/tools/host-filesystem/read.ts +12 -15
  373. package/src/tools/host-filesystem/write.ts +1 -1
  374. package/src/tools/host-terminal/host-shell.ts +21 -16
  375. package/src/tools/permission-checker.ts +77 -82
  376. package/src/tools/registry.ts +0 -2
  377. package/src/tools/secret-detection-handler.ts +34 -0
  378. package/src/tools/shared/filesystem/image-read.ts +61 -40
  379. package/src/tools/subagent/spawn.ts +47 -3
  380. package/src/tools/subagent/status.ts +2 -0
  381. package/src/tools/system/register.ts +2 -16
  382. package/src/tools/terminal/safe-env.ts +7 -0
  383. package/src/tools/terminal/shell.ts +21 -16
  384. package/src/tools/tool-approval-handler.ts +48 -2
  385. package/src/tools/types.ts +2 -0
  386. package/src/util/platform.ts +14 -19
  387. package/src/workspace/top-level-renderer.ts +19 -1
  388. package/src/__tests__/chrome-cdp.test.ts +0 -419
  389. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  390. package/src/__tests__/permission-mode-store.test.ts +0 -277
  391. package/src/browser-extension-relay/protocol.ts +0 -63
  392. package/src/browser-extension-relay/server.ts +0 -203
  393. package/src/config/schemas/sandbox.ts +0 -14
  394. package/src/permissions/permission-mode-store.ts +0 -180
  395. package/src/tools/browser/chrome-cdp.ts +0 -239
  396. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Route handler for `POST /v1/browser-extension-pair`.
3
+ *
4
+ * Mints a short-lived, scoped `host_browser_command` capability token for a
5
+ * chrome extension that has proved (via the native messaging helper) it is
6
+ * running locally with an allowlisted extension id.
7
+ *
8
+ * Security properties:
9
+ * - **Localhost-only**: enforced by both the TCP peer IP (via
10
+ * `server.requestIP`) and the `Host` header. Non-localhost callers
11
+ * receive a 403.
12
+ * - **Native-host marker header**: the request must carry the
13
+ * `x-vellum-native-host: 1` marker. Only the native messaging helper
14
+ * sets this header; browsers cannot attach custom request headers to
15
+ * fetches from web pages (custom headers trip CORS preflight, which
16
+ * this endpoint does not accept). Missing marker header is rejected
17
+ * with 403.
18
+ * - **Browser-origin rejection**: if an `Origin` header is present it
19
+ * must be either empty or explicitly on the
20
+ * `ALLOWED_EXTENSION_ORIGINS` allowlist. This defends against a
21
+ * malicious web page in another tab issuing a cross-origin POST from
22
+ * the user's browser — such a request would carry the page's origin
23
+ * and would be rejected here even if it somehow reached loopback.
24
+ * - **Strict rate limiting**: a dedicated per-peer sliding-window
25
+ * limiter caps pair requests at 10/minute per peer IP. This is
26
+ * separate from the global API limiter because the pair endpoint
27
+ * is pre-auth and extra abuse-sensitive.
28
+ * - **Audit logs on denial**: every rejected request emits a structured
29
+ * warn log including peer IP, Host header, Origin header, native-host
30
+ * marker presence, and a reason code so operators can triage denied
31
+ * attempts.
32
+ * - **Origin allowlist**: the body must include `extensionOrigin`
33
+ * matching a hard-coded allowlist of known Vellum chrome extension
34
+ * ids. This is treated as a secondary defense — the primary gate is
35
+ * the native-host marker header plus localhost peer check.
36
+ *
37
+ * Request body: `{ extensionOrigin: string }` (also accepts the legacy
38
+ * `{ origin: string }` for backwards compatibility).
39
+ * Response body: `{ token, expiresAt, guardianId }` — `expiresAt` is an
40
+ * ISO 8601 timestamp string matching what the native
41
+ * messaging helper validates.
42
+ */
43
+
44
+ import { readFileSync } from "node:fs";
45
+ import { resolve } from "node:path";
46
+
47
+ import { findGuardianForChannel } from "../../contacts/contact-store.js";
48
+ import { getLogger } from "../../util/logger.js";
49
+ import { mintHostBrowserCapability } from "../capability-tokens.js";
50
+ import { httpError } from "../http-errors.js";
51
+ import { isLoopbackAddress } from "../middleware/auth.js";
52
+ import { TokenRateLimiter } from "../middleware/rate-limiter.js";
53
+
54
+ const log = getLogger("browser-extension-pair");
55
+
56
+ /**
57
+ * Header name the native messaging helper MUST set on pair requests.
58
+ * Exported for tests and for the helper to keep in sync. Browsers cannot
59
+ * attach custom headers to fetches from web pages without tripping CORS
60
+ * preflight, which this endpoint does not handle — so a request carrying
61
+ * this header is (by construction) not a drive-by browser call.
62
+ */
63
+ export const NATIVE_HOST_MARKER_HEADER = "x-vellum-native-host";
64
+
65
+ /** Expected value for the native-host marker header. */
66
+ export const NATIVE_HOST_MARKER_VALUE = "1";
67
+
68
+ /**
69
+ * Strict per-peer rate limit for pair requests: 10 requests/minute per
70
+ * loopback peer IP. The native messaging flow only issues one pair
71
+ * request per extension spawn, so this budget is generous for normal
72
+ * use (account for retries on transient failures) while still clamping
73
+ * any abuse surface if a local attacker somehow invokes the endpoint
74
+ * in a tight loop. Exported for tests that need to reset state.
75
+ */
76
+ const PAIR_RATE_LIMIT_MAX_REQUESTS = 10;
77
+ const PAIR_RATE_LIMIT_WINDOW_MS = 60_000;
78
+
79
+ /**
80
+ * Dedicated rate limiter instance for the pair endpoint. Keyed on the
81
+ * TCP peer IP (always loopback here, so the key space is tiny and a
82
+ * handful of tracked keys is plenty).
83
+ */
84
+ const pairRateLimiter = new TokenRateLimiter(
85
+ PAIR_RATE_LIMIT_MAX_REQUESTS,
86
+ PAIR_RATE_LIMIT_WINDOW_MS,
87
+ 64,
88
+ );
89
+
90
+ /** Bun server shape needed for requestIP. */
91
+ export type PairServerContext = {
92
+ requestIP(
93
+ req: Request,
94
+ ): { address: string; family: string; port: number } | null;
95
+ };
96
+
97
+ const EXTENSION_ID_REGEX = /^[a-p]{32}$/;
98
+ const ALLOWLIST_CONFIG_PATH_CANDIDATES = [
99
+ // Source-checkout / test path (works when running from repo).
100
+ resolve(
101
+ import.meta.dir,
102
+ "..",
103
+ "..",
104
+ "..",
105
+ "..",
106
+ "meta",
107
+ "browser-extension",
108
+ "chrome-extension-allowlist.json",
109
+ ),
110
+ // Repo-root current-working-directory fallback.
111
+ resolve(
112
+ process.cwd(),
113
+ "meta",
114
+ "browser-extension",
115
+ "chrome-extension-allowlist.json",
116
+ ),
117
+ ];
118
+
119
+ type ChromeExtensionAllowlistConfig = {
120
+ version: number;
121
+ allowedExtensionIds: string[];
122
+ };
123
+
124
+ function parseAllowedExtensionIds(value: unknown): string[] {
125
+ if (!Array.isArray(value)) {
126
+ throw new Error("allowedExtensionIds is not an array");
127
+ }
128
+ const ids = value
129
+ .filter((id): id is string => typeof id === "string")
130
+ .filter((id) => EXTENSION_ID_REGEX.test(id));
131
+ if (ids.length === 0) {
132
+ throw new Error("allowedExtensionIds has no valid extension ids");
133
+ }
134
+ return ids;
135
+ }
136
+
137
+ function loadAllowedExtensionIdsFromEnv(): string[] {
138
+ const raw =
139
+ process.env.VELLUM_CHROME_EXTENSION_IDS ??
140
+ process.env.VELLUM_CHROME_EXTENSION_ID;
141
+ if (!raw) return [];
142
+ const ids = raw
143
+ .split(/[,\s]+/)
144
+ .map((id) => id.trim())
145
+ .filter((id) => id.length > 0)
146
+ .filter((id) => EXTENSION_ID_REGEX.test(id));
147
+ return Array.from(new Set(ids));
148
+ }
149
+
150
+ function loadAllowedExtensionOrigins(): ReadonlySet<string> {
151
+ const loadErrors: string[] = [];
152
+ for (const configPath of ALLOWLIST_CONFIG_PATH_CANDIDATES) {
153
+ try {
154
+ const raw = readFileSync(configPath, "utf8");
155
+ const parsed = JSON.parse(raw) as Partial<ChromeExtensionAllowlistConfig>;
156
+ const ids = parseAllowedExtensionIds(parsed.allowedExtensionIds);
157
+ return new Set<string>(ids.map((id) => `chrome-extension://${id}/`));
158
+ } catch (err) {
159
+ const detail = err instanceof Error ? err.message : String(err);
160
+ loadErrors.push(`${configPath}: ${detail}`);
161
+ }
162
+ }
163
+
164
+ // Compiled Bun binaries run from a virtual FS root (import.meta.dir is
165
+ // usually `/$bunfs/root`), so repo-relative config paths can disappear in
166
+ // packaged builds. In that case, allow a build-time injected env fallback.
167
+ const envIds = loadAllowedExtensionIdsFromEnv();
168
+ if (envIds.length > 0) {
169
+ return new Set<string>(envIds.map((id) => `chrome-extension://${id}/`));
170
+ }
171
+
172
+ log.error(
173
+ {
174
+ allowlistConfigPathCandidates: ALLOWLIST_CONFIG_PATH_CANDIDATES,
175
+ loadErrors,
176
+ },
177
+ "Failed to load Chrome extension allowlist config; pairing will reject all origins",
178
+ );
179
+ return new Set<string>();
180
+ }
181
+
182
+ /**
183
+ * Allowlist of chrome extension origins permitted to request a capability
184
+ * token. Loaded from the canonical config at
185
+ * `meta/browser-extension/chrome-extension-allowlist.json`.
186
+ */
187
+ export const ALLOWED_EXTENSION_ORIGINS = loadAllowedExtensionOrigins();
188
+
189
+ /**
190
+ * Reset the dedicated pair-endpoint rate limiter. Exported for tests
191
+ * so one test's burst can't bleed into another. Production code never
192
+ * calls this.
193
+ *
194
+ * We reach into the private `requests` map via a typed cast rather
195
+ * than adding a `reset()` method to the shared `TokenRateLimiter` —
196
+ * the limiter is a general-purpose utility that other routes also
197
+ * use, and we don't want to pollute its public API with a test-only
198
+ * escape hatch.
199
+ */
200
+ export function resetPairRateLimiterForTests(): void {
201
+ const limiter = pairRateLimiter as unknown as {
202
+ requests: Map<string, unknown>;
203
+ };
204
+ limiter.requests.clear();
205
+ }
206
+
207
+ /**
208
+ * Parse an HTTP `Host` header value and extract the hostname portion.
209
+ *
210
+ * Handles IPv6 bracket notation (`[::1]:8765`), unbracketed IPv6
211
+ * (`::1`), hostname with port (`localhost:8765`), and bare hostnames
212
+ * (`localhost`). Returns `null` when the header is malformed (e.g.
213
+ * missing closing bracket, or content after the closing bracket that
214
+ * isn't an optional `:port`).
215
+ *
216
+ * Exported for testing.
217
+ */
218
+ export function parseHostHeader(raw: string): string | null {
219
+ if (raw.length === 0) return null;
220
+ // IPv6 literal in brackets, e.g. `[::1]` or `[::1]:8765`.
221
+ if (raw.startsWith("[")) {
222
+ const end = raw.indexOf("]");
223
+ if (end < 0) return null;
224
+ // After the closing bracket only an optional ":port" is valid. Anything
225
+ // else (e.g. `[::1]attacker.com`) is a malformed Host header that an
226
+ // attacker could craft to slip a non-loopback hostname past the parser.
227
+ const after = raw.substring(end + 1);
228
+ if (after.length > 0 && !after.startsWith(":")) return null;
229
+ return raw.substring(1, end);
230
+ }
231
+ // Bare IPv6 (no brackets) contains multiple colons and should be
232
+ // treated as a whole. Anything with a single colon is `host:port`.
233
+ const firstColon = raw.indexOf(":");
234
+ if (firstColon < 0) return raw;
235
+ const secondColon = raw.indexOf(":", firstColon + 1);
236
+ if (secondColon >= 0) {
237
+ // Multiple colons and no brackets — assume unbracketed IPv6.
238
+ return raw;
239
+ }
240
+ return raw.substring(0, firstColon);
241
+ }
242
+
243
+ /**
244
+ * Returns true if the Host header (if present) points at a loopback
245
+ * address. We accept a missing Host header because some HTTP clients
246
+ * (notably node test harnesses) omit it.
247
+ */
248
+ function isLoopbackHostHeader(host: string | null): boolean {
249
+ if (!host) return true;
250
+ const parsed = parseHostHeader(host);
251
+ if (parsed === null) return false;
252
+ const hostname = parsed.toLowerCase();
253
+ if (hostname === "localhost") return true;
254
+ if (hostname === "127.0.0.1") return true;
255
+ if (hostname === "::1") return true;
256
+ if (hostname.startsWith("127.")) {
257
+ // Matches the 127.0.0.0/8 loopback range (e.g. 127.0.0.1, 127.1.2.3).
258
+ const parts = hostname.split(".");
259
+ if (parts.length !== 4) return false;
260
+ return parts.every((p) => /^\d+$/.test(p) && Number(p) <= 255);
261
+ }
262
+ return false;
263
+ }
264
+
265
+ /**
266
+ * Resolve the guardian id to bind the capability token to. Phase 2 uses
267
+ * the local vellum guardian principal when one exists, falling back to
268
+ * the string `"local"` for fresh installs that haven't bootstrapped a
269
+ * guardian yet.
270
+ */
271
+ function resolveLocalGuardianId(): string {
272
+ try {
273
+ const result = findGuardianForChannel("vellum");
274
+ if (result?.contact.principalId) {
275
+ return result.contact.principalId;
276
+ }
277
+ } catch (err) {
278
+ log.warn(
279
+ { err },
280
+ "Failed to look up local vellum guardian; falling back to 'local'",
281
+ );
282
+ }
283
+ return "local";
284
+ }
285
+
286
+ /**
287
+ * Emit an audit log for a denied pair attempt. Centralizes the field
288
+ * shape (peer IP, host header, origin header, native-host marker
289
+ * presence, reason) so operators can grep for a single log signature
290
+ * when triaging abuse.
291
+ */
292
+ function auditDeny(
293
+ req: Request,
294
+ peerIp: string,
295
+ reason: string,
296
+ extra?: Record<string, unknown>,
297
+ ): void {
298
+ const host = req.headers.get("host");
299
+ const origin = req.headers.get("origin");
300
+ const nativeHostMarker = req.headers.get(NATIVE_HOST_MARKER_HEADER);
301
+ log.warn(
302
+ {
303
+ audit: "browser-extension-pair-denied",
304
+ peerIp,
305
+ host,
306
+ origin,
307
+ nativeHostMarkerPresent: nativeHostMarker !== null,
308
+ reason,
309
+ ...extra,
310
+ },
311
+ `pair_denied: ${reason}`,
312
+ );
313
+ }
314
+
315
+ /**
316
+ * Handle POST /v1/browser-extension-pair.
317
+ *
318
+ * Body: `{ extensionOrigin: string }` (also accepts legacy
319
+ * `{ origin: string }` for backwards compatibility).
320
+ * Returns: `{ token, expiresAt, guardianId }` where `expiresAt` is an
321
+ * ISO 8601 timestamp string that the native messaging helper
322
+ * validates as a string.
323
+ */
324
+ export async function handleBrowserExtensionPair(
325
+ req: Request,
326
+ server: PairServerContext,
327
+ ): Promise<Response> {
328
+ if (req.method !== "POST") {
329
+ return new Response("method not allowed", {
330
+ status: 405,
331
+ headers: { Allow: "POST" },
332
+ });
333
+ }
334
+
335
+ // Enforce localhost-only via peer IP.
336
+ const peer = server.requestIP(req);
337
+ const peerIp = peer?.address ?? "";
338
+ if (!peerIp || !isLoopbackAddress(peerIp)) {
339
+ auditDeny(req, peerIp, "non_loopback_peer");
340
+ return httpError("FORBIDDEN", "endpoint is local-only", 403);
341
+ }
342
+
343
+ // Secondary check: Host header. Rejects requests that slip past the
344
+ // TCP-level check via proxies that rewrite the peer address.
345
+ const host = req.headers.get("host");
346
+ if (!isLoopbackHostHeader(host)) {
347
+ auditDeny(req, peerIp, "non_loopback_host_header");
348
+ return httpError("FORBIDDEN", "endpoint is local-only", 403);
349
+ }
350
+
351
+ // Any `x-forwarded-for` header indicates the request was proxied from a
352
+ // non-local client. Reject — the pair endpoint is strictly machine-local.
353
+ if (req.headers.get("x-forwarded-for")) {
354
+ auditDeny(req, peerIp, "x_forwarded_for_present");
355
+ return httpError("FORBIDDEN", "endpoint is local-only", 403);
356
+ }
357
+
358
+ // Primary marker-header gate. The native messaging helper sets this
359
+ // header on every pair request; browsers cannot (without CORS
360
+ // preflight, which this endpoint does not serve). Reject when the
361
+ // header is absent or set to an unexpected value.
362
+ //
363
+ // IMPORTANT: this check runs BEFORE the rate limiter so that
364
+ // unmarked drive-by POSTs from a malicious webpage cannot burn the
365
+ // legitimate 10/min budget. If the rate limiter ran first, a
366
+ // cross-origin page could issue 10 unmarked requests per minute and
367
+ // starve the native messaging helper's real pair attempts with 429s
368
+ // until the window reset. Unmarked requests therefore return 403
369
+ // without touching the limiter at all.
370
+ const marker = req.headers.get(NATIVE_HOST_MARKER_HEADER);
371
+ if (marker !== NATIVE_HOST_MARKER_VALUE) {
372
+ auditDeny(req, peerIp, "missing_native_host_marker");
373
+ return httpError("FORBIDDEN", "native host marker required", 403);
374
+ }
375
+
376
+ // Strict rate limit by peer IP. The limiter is keyed on the loopback
377
+ // peer address; browsers (even local ones) all appear as 127.0.0.1
378
+ // here, which is intentional — a single compromised local process
379
+ // should not be able to hammer the mint endpoint. We evaluate this
380
+ // AFTER the native-host marker check so that unauthenticated
381
+ // drive-by POSTs can't consume the legitimate 10/min quota (see
382
+ // comment above the marker check for the DoS rationale).
383
+ const rateResult = pairRateLimiter.check(
384
+ peerIp,
385
+ "/v1/browser-extension-pair",
386
+ );
387
+ if (!rateResult.allowed) {
388
+ auditDeny(req, peerIp, "rate_limited", {
389
+ limit: rateResult.limit,
390
+ resetAt: rateResult.resetAt,
391
+ });
392
+ const retryAfter = Math.max(
393
+ 1,
394
+ rateResult.resetAt - Math.ceil(Date.now() / 1000),
395
+ );
396
+ // Return the same error envelope shape as `httpError` but with
397
+ // Retry-After + X-RateLimit-* headers attached so the native
398
+ // host can back off sensibly. We construct the body inline to
399
+ // avoid cloning / re-consuming a Response returned by
400
+ // `httpError` (Response bodies are one-shot streams).
401
+ return Response.json(
402
+ {
403
+ error: {
404
+ code: "RATE_LIMITED",
405
+ message: "too many pair requests",
406
+ },
407
+ },
408
+ {
409
+ status: 429,
410
+ headers: {
411
+ "Retry-After": String(retryAfter),
412
+ "X-RateLimit-Limit": String(rateResult.limit),
413
+ "X-RateLimit-Remaining": "0",
414
+ "X-RateLimit-Reset": String(rateResult.resetAt),
415
+ },
416
+ },
417
+ );
418
+ }
419
+
420
+ // Browser-origin rejection. Any non-empty `Origin` header that isn't
421
+ // on the extension origin allowlist is a cross-origin browser fetch
422
+ // and must be rejected. The native messaging helper sends no Origin
423
+ // header at all (it's a plain node fetch, not a browser fetch), so
424
+ // the common case is `origin === null`.
425
+ const originHeader = req.headers.get("origin");
426
+ if (originHeader !== null && originHeader.length > 0) {
427
+ // Normalize by stripping any trailing slash mismatch: the
428
+ // allowlist entries end with `/` but browsers' Origin headers
429
+ // never include a trailing slash (per RFC 6454 an origin is
430
+ // scheme+host+port with no path). Compare both the bare origin
431
+ // and the `/`-suffixed form against the allowlist.
432
+ const withSlash = `${originHeader}/`;
433
+ if (
434
+ !ALLOWED_EXTENSION_ORIGINS.has(originHeader) &&
435
+ !ALLOWED_EXTENSION_ORIGINS.has(withSlash)
436
+ ) {
437
+ auditDeny(req, peerIp, "browser_origin_not_allowlisted", {
438
+ originHeader,
439
+ });
440
+ return httpError("FORBIDDEN", "origin not allowed", 403);
441
+ }
442
+ }
443
+
444
+ let body: unknown;
445
+ try {
446
+ body = await req.json();
447
+ } catch {
448
+ auditDeny(req, peerIp, "invalid_json_body");
449
+ return httpError("BAD_REQUEST", "invalid JSON body", 400);
450
+ }
451
+
452
+ if (!body || typeof body !== "object") {
453
+ auditDeny(req, peerIp, "body_not_object");
454
+ return httpError("BAD_REQUEST", "body must be an object", 400);
455
+ }
456
+
457
+ // Accept `extensionOrigin` (preferred, matches the native messaging
458
+ // helper) and fall back to `origin` (legacy, for any callers that
459
+ // haven't migrated yet).
460
+ const raw = body as {
461
+ extensionOrigin?: unknown;
462
+ origin?: unknown;
463
+ };
464
+ const extensionOrigin =
465
+ typeof raw.extensionOrigin === "string" && raw.extensionOrigin.length > 0
466
+ ? raw.extensionOrigin
467
+ : typeof raw.origin === "string" && raw.origin.length > 0
468
+ ? raw.origin
469
+ : null;
470
+ if (extensionOrigin === null) {
471
+ auditDeny(req, peerIp, "missing_extension_origin");
472
+ return httpError("BAD_REQUEST", "extensionOrigin is required", 400);
473
+ }
474
+
475
+ // Secondary defense: body-level extension origin allowlist. The
476
+ // primary gate is the native-host marker + loopback peer check; this
477
+ // check catches the failure mode where a compromised extension id
478
+ // that doesn't match a known Vellum build still manages to reach
479
+ // the endpoint.
480
+ if (!ALLOWED_EXTENSION_ORIGINS.has(extensionOrigin)) {
481
+ auditDeny(req, peerIp, "extension_origin_not_allowlisted", {
482
+ extensionOrigin,
483
+ });
484
+ return httpError("UNAUTHORIZED", "unauthorized origin", 401);
485
+ }
486
+
487
+ const guardianId = resolveLocalGuardianId();
488
+ const { token, expiresAt } = mintHostBrowserCapability(guardianId);
489
+ const expiresAtIso = new Date(expiresAt).toISOString();
490
+
491
+ log.info(
492
+ { extensionOrigin, guardianId, expiresAt: expiresAtIso },
493
+ "Issued chrome extension capability token",
494
+ );
495
+
496
+ return Response.json({ token, expiresAt: expiresAtIso, guardianId });
497
+ }
@@ -111,6 +111,7 @@ Analyze the conversation above. Provide a structured self-assessment:
111
111
  3. **What went wrong**: Errors, unnecessary tool calls, incorrect assumptions, wasted turns, misunderstandings.
112
112
  4. **Root causes**: Why did failures happen? Missing context? Wrong approach? Tool limitations?
113
113
  5. **Recommendations**: Specific, actionable improvements for similar conversations next time.
114
+ 6. **Code & tooling changes**: Are there any changes to files you should make based on these learnings? Are there any skills or scripts that are worth creating or modifying? Don't make these changes yet — just provide your analysis.
114
115
 
115
116
  Be honest and specific. Reference particular moments in the transcript. Focus on patterns that generalize beyond this specific conversation.
116
117
 
@@ -159,7 +160,7 @@ Do not use tools during analysis. If you identify insights worth remembering for
159
160
  // l. Fire-and-forget the agent loop
160
161
  analysisConversation
161
162
  .runAgentLoop(prompt, messageId, onEvent, {
162
- isInteractive: hasLiveSubscriber,
163
+ isInteractive: false,
163
164
  isUserMessage: true,
164
165
  })
165
166
  .catch((err) => {
@@ -4,6 +4,8 @@
4
4
  * POST /v1/conversations — create a new conversation
5
5
  * POST /v1/conversations/switch — switch to an existing conversation
6
6
  * POST /v1/conversations/fork — fork an existing conversation
7
+ * GET /v1/conversations/:id/host-access — read host access for one conversation
8
+ * PATCH /v1/conversations/:id/host-access — update host access for one conversation
7
9
  * PATCH /v1/conversations/:id/name — rename a conversation
8
10
  * DELETE /v1/conversations — clear all conversations
9
11
  * POST /v1/conversations/:id/wipe — wipe conversation and revert memory
@@ -21,7 +23,9 @@ import {
21
23
  countConversationsByScheduleJobId,
22
24
  deleteConversation,
23
25
  getConversation,
26
+ getConversationHostAccess,
24
27
  PRIVATE_CONVERSATION_FORK_ERROR,
28
+ updateConversationHostAccess,
25
29
  wipeConversation,
26
30
  } from "../../memory/conversation-crud.js";
27
31
  import { updateConversationTitle } from "../../memory/conversation-crud.js";
@@ -34,6 +38,10 @@ import { enqueueMemoryJob } from "../../memory/jobs-store.js";
34
38
  import { deleteSchedule } from "../../schedule/schedule-store.js";
35
39
  import { UserError } from "../../util/errors.js";
36
40
  import { getLogger } from "../../util/logger.js";
41
+ import { buildAssistantEvent } from "../assistant-event.js";
42
+ import { assistantEventHub } from "../assistant-event-hub.js";
43
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
44
+ import { requireBoundGuardian } from "../auth/require-bound-guardian.js";
37
45
  import { httpError } from "../http-errors.js";
38
46
  import type { RouteDefinition } from "../http-router.js";
39
47
 
@@ -52,6 +60,7 @@ export interface ConversationManagementDeps {
52
60
  conversationId: string;
53
61
  title: string;
54
62
  conversationType: string;
63
+ hostAccess: boolean;
55
64
  } | null>;
56
65
  renameConversation: (conversationId: string, name: string) => boolean;
57
66
  clearAllConversations: () => number;
@@ -218,6 +227,7 @@ export function conversationManagementRouteDefinitions(
218
227
  conversationId: z.string(),
219
228
  title: z.string(),
220
229
  conversationType: z.string(),
230
+ hostAccess: z.boolean(),
221
231
  }),
222
232
  handler: async ({ req }) => {
223
233
  const body = (await req.json()) as {
@@ -246,6 +256,104 @@ export function conversationManagementRouteDefinitions(
246
256
  title: result.title,
247
257
  conversationType:
248
258
  result.conversationType === "private" ? "private" : "standard",
259
+ hostAccess: result.hostAccess,
260
+ });
261
+ },
262
+ },
263
+ {
264
+ endpoint: "conversations/:id/host-access",
265
+ method: "GET",
266
+ policyKey: "conversations/host-access:GET",
267
+ summary: "Get conversation host access",
268
+ description: "Return whether the conversation can use host tools.",
269
+ tags: ["conversations"],
270
+ responseBody: z.object({
271
+ conversationId: z.string(),
272
+ hostAccess: z.boolean(),
273
+ }),
274
+ handler: ({ params }) => {
275
+ const resolvedId = resolveConversationId(params.id) ?? params.id;
276
+ const conversation = getConversation(resolvedId);
277
+ if (!conversation) {
278
+ return httpError(
279
+ "NOT_FOUND",
280
+ `Conversation ${params.id} not found`,
281
+ 404,
282
+ );
283
+ }
284
+ return Response.json({
285
+ conversationId: conversation.id,
286
+ hostAccess: getConversationHostAccess(conversation.id),
287
+ });
288
+ },
289
+ },
290
+ {
291
+ endpoint: "conversations/:id/host-access",
292
+ method: "PATCH",
293
+ policyKey: "conversations/host-access",
294
+ summary: "Update conversation host access",
295
+ description: "Enable or disable host access for a conversation.",
296
+ tags: ["conversations"],
297
+ requestBody: z.object({
298
+ hostAccess: z.boolean(),
299
+ }),
300
+ responseBody: z.object({
301
+ conversationId: z.string(),
302
+ hostAccess: z.boolean(),
303
+ }),
304
+ handler: async ({ req, params, authContext }) => {
305
+ const guardianError = requireBoundGuardian(authContext);
306
+ if (guardianError) return guardianError;
307
+
308
+ const rawBody = (await req.json()) as unknown;
309
+ if (
310
+ rawBody == null ||
311
+ typeof rawBody !== "object" ||
312
+ Array.isArray(rawBody)
313
+ ) {
314
+ return httpError("BAD_REQUEST", "Invalid request body", 400);
315
+ }
316
+ const body = rawBody as { hostAccess?: unknown };
317
+ if (typeof body.hostAccess !== "boolean") {
318
+ return httpError("BAD_REQUEST", "Missing hostAccess boolean", 400);
319
+ }
320
+
321
+ const resolvedId = resolveConversationId(params.id) ?? params.id;
322
+ const conversation = getConversation(resolvedId);
323
+ if (!conversation) {
324
+ return httpError(
325
+ "NOT_FOUND",
326
+ `Conversation ${params.id} not found`,
327
+ 404,
328
+ );
329
+ }
330
+
331
+ const nextHostAccess = body.hostAccess;
332
+ if (conversation.hostAccess !== (nextHostAccess ? 1 : 0)) {
333
+ updateConversationHostAccess(resolvedId, nextHostAccess);
334
+ assistantEventHub
335
+ .publish(
336
+ buildAssistantEvent(
337
+ DAEMON_INTERNAL_ASSISTANT_ID,
338
+ {
339
+ type: "conversation_host_access_updated",
340
+ conversationId: resolvedId,
341
+ hostAccess: nextHostAccess,
342
+ },
343
+ resolvedId,
344
+ ),
345
+ )
346
+ .catch((err) => {
347
+ log.warn(
348
+ { err, conversationId: resolvedId },
349
+ "Failed to publish conversation_host_access_updated event",
350
+ );
351
+ });
352
+ }
353
+
354
+ return Response.json({
355
+ conversationId: resolvedId,
356
+ hostAccess: nextHostAccess,
249
357
  });
250
358
  },
251
359
  },