@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
@@ -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
+ }
@@ -19,7 +19,6 @@ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
19
19
  import { httpError } from "../http-errors.js";
20
20
  import type { RouteDefinition } from "../http-router.js";
21
21
  import type { SendMessageDeps } from "../http-types.js";
22
- import { resolveLocalTrustContext } from "../local-actor-identity.js";
23
22
 
24
23
  const log = getLogger("conversation-analysis-routes");
25
24
 
@@ -112,25 +111,38 @@ Analyze the conversation above. Provide a structured self-assessment:
112
111
  3. **What went wrong**: Errors, unnecessary tool calls, incorrect assumptions, wasted turns, misunderstandings.
113
112
  4. **Root causes**: Why did failures happen? Missing context? Wrong approach? Tool limitations?
114
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.
115
115
 
116
116
  Be honest and specific. Reference particular moments in the transcript. Focus on patterns that generalize beyond this specific conversation.
117
117
 
118
- If you identify insights worth remembering for future conversations, use your memory tools to save them.`;
118
+ Do not use tools during analysis. If you identify insights worth remembering for future conversations, include them in the response as explicit memory candidates instead of saving them directly.`;
119
119
 
120
120
  // h. Persist the user message
121
121
  const message = await addMessage(
122
122
  newConv.id,
123
123
  "user",
124
124
  JSON.stringify([{ type: "text", text: prompt }]),
125
- { provenanceTrustClass: "guardian" as const },
125
+ { provenanceTrustClass: "unknown" as const },
126
126
  );
127
127
  const messageId = message.id;
128
128
 
129
- // i. Load the conversation into memory and set guardian trust context
129
+ // i. Load the conversation into memory with untrusted analysis context
130
130
  const analysisConversation =
131
131
  await deps.sendMessageDeps.getOrCreateConversation(newConv.id);
132
- analysisConversation.setTrustContext(resolveLocalTrustContext("vellum"));
132
+ analysisConversation.setTrustContext({
133
+ trustClass: "unknown",
134
+ sourceChannel: "vellum",
135
+ });
133
136
  await analysisConversation.ensureActorScopedHistory();
137
+ // Analysis runs over attacker-influenced transcript content, so do not
138
+ // expose any tools, even when a live client is available.
139
+ analysisConversation.setSubagentAllowedTools(new Set<string>());
140
+
141
+ const hasLiveSubscriber =
142
+ deps.sendMessageDeps.assistantEventHub.hasSubscribersForEvent({
143
+ assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
144
+ conversationId: newConv.id,
145
+ });
134
146
 
135
147
  // j. Build onEvent using inline hub publisher
136
148
  const onEvent = (msg: ServerMessage) => {
@@ -138,6 +150,7 @@ If you identify insights worth remembering for future conversations, use your me
138
150
  buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg, newConv.id),
139
151
  );
140
152
  };
153
+ analysisConversation.updateClient(onEvent, !hasLiveSubscriber);
141
154
 
142
155
  // k. Set up processing state (required by runAgentLoop guard)
143
156
  analysisConversation.processing = true;