@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
@@ -25,6 +25,7 @@
25
25
  import { isHttpAuthDisabled } from "../../config/env.js";
26
26
  import { getLogger } from "../../util/logger.js";
27
27
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
28
+ import { verifyHostBrowserCapability } from "../capability-tokens.js";
28
29
  import { extractBearerToken } from "../middleware/auth.js";
29
30
  import { buildAuthContext } from "./context.js";
30
31
  import { resolveScopeProfile } from "./scopes.js";
@@ -186,3 +187,100 @@ export function authenticateRequest(req: Request): AuthenticateResult {
186
187
 
187
188
  return { ok: true, context: contextResult.context };
188
189
  }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Capability-token-aware auth for /v1/host-browser-result
193
+ // ---------------------------------------------------------------------------
194
+
195
+ /**
196
+ * Build a synthetic AuthContext from a verified host_browser capability
197
+ * claim. The resulting context is shaped to look like an
198
+ * `actor_client_v1` actor so downstream route policy (which requires
199
+ * `approval.write`) and `requireBoundGuardian` (which compares
200
+ * `actorPrincipalId` against the bound guardian) both accept it.
201
+ *
202
+ * The capability token already carries its own HMAC-checked expiry, so
203
+ * there is no policy-epoch gate to apply here — we pin `policyEpoch` to
204
+ * `Number.MAX_SAFE_INTEGER` the same way the dev-bypass context does.
205
+ */
206
+ function buildCapabilityAuthContext(guardianId: string): AuthContext {
207
+ return {
208
+ subject: `actor:${DAEMON_INTERNAL_ASSISTANT_ID}:${guardianId}`,
209
+ principalType: "actor",
210
+ assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
211
+ actorPrincipalId: guardianId,
212
+ scopeProfile: "actor_client_v1",
213
+ scopes: resolveScopeProfile("actor_client_v1"),
214
+ policyEpoch: Number.MAX_SAFE_INTEGER,
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Authenticate a request that is allowed to present either a JWT or a
220
+ * host_browser capability token. This is the auth entry point for
221
+ * `/v1/host-browser-result` POST specifically — the chrome extension
222
+ * stores a capability token (minted by the
223
+ * `/v1/browser-extension-pair` flow) rather than a daemon JWT, so the
224
+ * POST fallback used when the `/v1/browser-relay` WebSocket is
225
+ * unavailable would otherwise 401 through the JWT-only
226
+ * `authenticateRequest` path.
227
+ *
228
+ * Order of operations (mirrors `handleBrowserRelayUpgrade`):
229
+ * 1. Extract the bearer token. Missing header → 401.
230
+ * 2. Try `verifyHostBrowserCapability(token)` first. If it succeeds,
231
+ * derive `guardianId` from the capability claims and synthesize an
232
+ * AuthContext.
233
+ * 3. Otherwise fall through to the standard JWT path so daemon-minted
234
+ * JWTs (gateway-proxied or direct) continue to work as a
235
+ * regression-safe compatibility path.
236
+ *
237
+ * Dev bypass (`isHttpAuthDisabled()`) is honored the same way as
238
+ * `authenticateRequest` — we delegate to it directly to pick up the
239
+ * shared synthetic dev-bypass context.
240
+ */
241
+ export function authenticateHostBrowserResultRequest(
242
+ req: Request,
243
+ ): AuthenticateResult {
244
+ if (isHttpAuthDisabled()) {
245
+ return { ok: true, context: buildDevBypassContext() };
246
+ }
247
+
248
+ const rawToken = extractBearerToken(req);
249
+ if (!rawToken) {
250
+ log.warn(
251
+ { reason: "missing_token", path: "/v1/host-browser-result" },
252
+ "Host browser result auth denied: missing Authorization header",
253
+ );
254
+ return {
255
+ ok: false,
256
+ response: Response.json(
257
+ {
258
+ error: {
259
+ code: "UNAUTHORIZED",
260
+ message: "Missing Authorization header",
261
+ },
262
+ },
263
+ { status: 401 },
264
+ ),
265
+ };
266
+ }
267
+
268
+ // 1) Capability-token path (self-hosted default). The chrome
269
+ // extension presents the token it received from the native
270
+ // messaging pair flow. We derive `actorPrincipalId` from the
271
+ // capability claims directly — the claims are HMAC-signed by the
272
+ // same daemon so there is no cross-tenant risk.
273
+ const capabilityClaims = verifyHostBrowserCapability(rawToken);
274
+ if (capabilityClaims) {
275
+ return {
276
+ ok: true,
277
+ context: buildCapabilityAuthContext(capabilityClaims.guardianId),
278
+ };
279
+ }
280
+
281
+ // 2) JWT compatibility path. Fall back to the existing daemon/gateway
282
+ // JWT verification so cloud callers and any legacy self-hosted
283
+ // clients still holding a daemon JWT continue to work. Any 401
284
+ // emitted here already includes the JWT-specific reason.
285
+ return authenticateRequest(req);
286
+ }
@@ -133,6 +133,8 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
133
133
  { endpoint: "conversations/analyze", scopes: ["chat.write"] },
134
134
  { endpoint: "conversations/switch", scopes: ["chat.write"] },
135
135
  { endpoint: "conversations/name", scopes: ["chat.write"] },
136
+ { endpoint: "conversations/host-access:GET", scopes: ["chat.read"] },
137
+ { endpoint: "conversations/host-access", scopes: ["approval.write"] },
136
138
  { endpoint: "conversations/cancel", scopes: ["chat.write"] },
137
139
  { endpoint: "conversations/undo", scopes: ["chat.write"] },
138
140
  { endpoint: "conversations/regenerate", scopes: ["chat.write"] },
@@ -148,6 +150,7 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
148
150
  { endpoint: "secret", scopes: ["approval.write"] },
149
151
  { endpoint: "trust-rules", scopes: ["approval.write"] },
150
152
  { endpoint: "host-bash-result", scopes: ["approval.write"] },
153
+ { endpoint: "host-browser-result", scopes: ["approval.write"] },
151
154
  { endpoint: "host-cu-result", scopes: ["approval.write"] },
152
155
  { endpoint: "host-file-result", scopes: ["approval.write"] },
153
156
  { endpoint: "pending-interactions", scopes: ["approval.read"] },
@@ -381,10 +384,6 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
381
384
  // Queued message deletion
382
385
  { endpoint: "messages/queued", scopes: ["chat.write"] },
383
386
 
384
- // Browser relay
385
- { endpoint: "browser-relay/status", scopes: ["settings.read"] },
386
- { endpoint: "browser-relay/command", scopes: ["settings.write"] },
387
-
388
387
  // Interfaces
389
388
  { endpoint: "interfaces", scopes: ["settings.read"] },
390
389
 
@@ -482,9 +481,9 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
482
481
  { endpoint: "tools", scopes: ["settings.read"] },
483
482
  { endpoint: "tools/simulate-permission", scopes: ["settings.read"] },
484
483
 
485
- // Permission mode
486
- { endpoint: "permission-mode:GET", scopes: ["settings.read"] },
487
- { endpoint: "permission-mode", scopes: ["settings.write"] },
484
+ // Browser CDP shim — backs the `assistant browser chrome relay` CLI used
485
+ // by the in-tree Amazon and Influencer skills.
486
+ { endpoint: "browser-cdp", scopes: ["settings.write"] },
488
487
  ];
489
488
 
490
489
  for (const { endpoint, scopes } of ACTOR_ENDPOINTS) {
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Capability token minting and verification for scoped, short-lived tokens
3
+ * issued to the chrome extension (and other thin clients) so they can submit
4
+ * results back to the runtime without a full guardian-bound JWT.
5
+ *
6
+ * Design:
7
+ * - Tokens are HMAC-SHA256 signed over a JSON claims payload.
8
+ * - Claims include a bound capability, guardian id, nonce, and expiry.
9
+ * - Signing uses a long-lived random secret persisted to
10
+ * `~/.vellum/protected/` with 0600 permissions. The protected
11
+ * directory sits outside the workspace per AGENTS.md: workspace
12
+ * directories must not hold security-sensitive material.
13
+ * - The secret is generated once on first launch and reused across
14
+ * subsequent daemon restarts so previously-minted tokens still verify.
15
+ * - Tests inject their own secret via `setCapabilityTokenSecretForTests`.
16
+ *
17
+ * The encoded token format is `<base64url(payload)>.<base64url(sig)>`.
18
+ */
19
+
20
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
21
+ import {
22
+ chmodSync,
23
+ existsSync,
24
+ mkdirSync,
25
+ readFileSync,
26
+ renameSync,
27
+ unlinkSync,
28
+ writeFileSync,
29
+ } from "node:fs";
30
+ import { homedir } from "node:os";
31
+ import { dirname, join } from "node:path";
32
+
33
+ import { getLogger } from "../util/logger.js";
34
+ import { getDataDir, getProtectedDir } from "../util/platform.js";
35
+
36
+ const log = getLogger("capability-tokens");
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Types
40
+ // ---------------------------------------------------------------------------
41
+
42
+ /** Capability identifiers that can be bound to a capability token. */
43
+ export type Capability = "host_browser_command";
44
+
45
+ /** Claims encoded in the signed payload. */
46
+ export interface CapabilityClaims {
47
+ capability: Capability;
48
+ guardianId: string;
49
+ /** 16-byte random nonce, hex-encoded. Prevents replay across fresh mints. */
50
+ nonce: string;
51
+ /** ms-since-epoch expiry. */
52
+ expiresAt: number;
53
+ }
54
+
55
+ /** A freshly-minted capability token and its absolute expiry. */
56
+ export interface CapabilityToken {
57
+ token: string;
58
+ expiresAt: number;
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Secret lifecycle
63
+ // ---------------------------------------------------------------------------
64
+
65
+ let _secret: Buffer | undefined;
66
+
67
+ /**
68
+ * Returns the canonical path where the capability-token secret is
69
+ * persisted: `~/.vellum/protected/capability-token-secret`. The protected
70
+ * directory is the canonical location for security-sensitive material
71
+ * and sits outside the workspace (which AGENTS.md forbids for secrets).
72
+ */
73
+ function getSecretPath(): string {
74
+ return join(getProtectedDir(), "capability-token-secret");
75
+ }
76
+
77
+ /**
78
+ * Legacy path under `workspace/data/` where earlier builds persisted the
79
+ * capability-token secret. We keep this as a read-only migration source
80
+ * so existing deployments don't regenerate their secret (and invalidate
81
+ * every outstanding token) on upgrade — the first launch after the
82
+ * upgrade copies the legacy file into `getProtectedDir()` and removes it
83
+ * from the workspace.
84
+ */
85
+ function getLegacySecretPath(): string {
86
+ return join(getDataDir(), "capability-token-secret");
87
+ }
88
+
89
+ /**
90
+ * Path overrides used by unit tests to drive the secret lifecycle
91
+ * without touching the real `~/.vellum/` tree. Production callers must
92
+ * omit this argument so the canonical paths (`getProtectedDir()` +
93
+ * `getDataDir()`) are used.
94
+ */
95
+ export interface CapabilityTokenSecretPaths {
96
+ /** Protected-directory secret path (authoritative). */
97
+ secretPath: string;
98
+ /** Legacy workspace-directory secret path (migration source). */
99
+ legacySecretPath: string;
100
+ }
101
+
102
+ /**
103
+ * Load the capability-token secret from disk or generate and persist a new
104
+ * one. Atomically writes with mode 0o600 so the secret is not readable by
105
+ * other users on the same host.
106
+ *
107
+ * Migration: if the secret exists only at the legacy workspace path, copy
108
+ * it into the protected directory and delete the workspace copy so we do
109
+ * not leave security-sensitive material inside `workspace/`.
110
+ *
111
+ * The optional `paths` argument is for unit tests only — production
112
+ * callers must omit it and use the canonical `~/.vellum/protected/` /
113
+ * `~/.vellum/workspace/data/` paths.
114
+ */
115
+ export function loadOrCreateCapabilityTokenSecret(
116
+ paths?: CapabilityTokenSecretPaths,
117
+ ): Buffer {
118
+ const keyPath = paths?.secretPath ?? getSecretPath();
119
+ const legacyPath = paths?.legacySecretPath ?? getLegacySecretPath();
120
+ if (existsSync(keyPath)) {
121
+ try {
122
+ const raw = readFileSync(keyPath);
123
+ if (raw.length === 32) {
124
+ return raw;
125
+ }
126
+ log.warn(
127
+ { keyPath, length: raw.length },
128
+ "capability token secret has unexpected length — regenerating",
129
+ );
130
+ } catch (err) {
131
+ log.warn(
132
+ { err, keyPath },
133
+ "Failed to read capability token secret — regenerating",
134
+ );
135
+ }
136
+ }
137
+
138
+ // Attempt to migrate a legacy workspace-directory secret before we
139
+ // generate a fresh one. If this succeeds we end up with the legacy
140
+ // secret persisted at the protected path and the workspace copy
141
+ // removed, preserving every outstanding token across the upgrade.
142
+ const migrated = migrateLegacyCapabilityTokenSecret(keyPath, legacyPath);
143
+ if (migrated) {
144
+ return migrated;
145
+ }
146
+
147
+ const fresh = randomBytes(32);
148
+ writeSecretAtomic(keyPath, fresh);
149
+ log.info("Capability token secret generated and persisted");
150
+ return fresh;
151
+ }
152
+
153
+ /**
154
+ * Write `secret` to `keyPath` atomically with mode 0o600. Ensures the
155
+ * parent directory exists.
156
+ */
157
+ function writeSecretAtomic(keyPath: string, secret: Buffer): void {
158
+ const dir = dirname(keyPath);
159
+ if (!existsSync(dir)) {
160
+ mkdirSync(dir, { recursive: true });
161
+ }
162
+ const tmpPath = `${keyPath}.tmp.${process.pid}`;
163
+ writeFileSync(tmpPath, secret, { mode: 0o600 });
164
+ renameSync(tmpPath, keyPath);
165
+ try {
166
+ chmodSync(keyPath, 0o600);
167
+ } catch (err) {
168
+ log.warn(
169
+ { err, keyPath },
170
+ "Failed to chmod capability token secret after write",
171
+ );
172
+ }
173
+ }
174
+
175
+ /**
176
+ * If a pre-migration capability token secret exists under the workspace
177
+ * data directory, copy it into the protected directory and remove the
178
+ * workspace copy. Returns the migrated secret if migration ran
179
+ * successfully, or `undefined` if there was nothing to migrate or the
180
+ * migration failed.
181
+ */
182
+ function migrateLegacyCapabilityTokenSecret(
183
+ secretPath: string,
184
+ legacyPath: string,
185
+ ): Buffer | undefined {
186
+ if (!existsSync(legacyPath)) {
187
+ return undefined;
188
+ }
189
+ try {
190
+ const raw = readFileSync(legacyPath);
191
+ if (raw.length !== 32) {
192
+ log.warn(
193
+ { legacyPath, length: raw.length },
194
+ "legacy capability token secret has unexpected length — ignoring",
195
+ );
196
+ return undefined;
197
+ }
198
+ writeSecretAtomic(secretPath, raw);
199
+ try {
200
+ unlinkSync(legacyPath);
201
+ } catch (err) {
202
+ log.warn(
203
+ { err, legacyPath },
204
+ "Failed to remove legacy workspace capability token secret after migration",
205
+ );
206
+ }
207
+ log.info(
208
+ { from: legacyPath, to: secretPath },
209
+ "Migrated capability token secret out of workspace into protected directory",
210
+ );
211
+ return raw;
212
+ } catch (err) {
213
+ log.warn(
214
+ { err, legacyPath },
215
+ "Failed to migrate legacy capability token secret — regenerating",
216
+ );
217
+ return undefined;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Initialize the module-level secret. Called once at daemon startup. Safe
223
+ * to call multiple times — subsequent calls overwrite the cached value
224
+ * (useful in tests that reset state).
225
+ */
226
+ export function initCapabilityTokenSecret(secret: Buffer): void {
227
+ if (secret.length !== 32) {
228
+ throw new Error(
229
+ `capability token secret must be 32 bytes, got ${secret.length}`,
230
+ );
231
+ }
232
+ _secret = secret;
233
+ }
234
+
235
+ /**
236
+ * Test-only helper to inject a deterministic secret.
237
+ */
238
+ export function setCapabilityTokenSecretForTests(secret: Buffer): void {
239
+ _secret = secret;
240
+ }
241
+
242
+ /**
243
+ * Reset the cached secret. Test-only — exposed so test isolation can
244
+ * force a reload from disk.
245
+ */
246
+ export function resetCapabilityTokenSecretForTests(): void {
247
+ _secret = undefined;
248
+ }
249
+
250
+ function getSecret(): Buffer {
251
+ if (_secret) return _secret;
252
+ if (process.env.NODE_ENV === "test") {
253
+ _secret = randomBytes(32);
254
+ return _secret;
255
+ }
256
+ // Lazy load — daemon startup is expected to call
257
+ // `initCapabilityTokenSecret(loadOrCreateCapabilityTokenSecret())` but
258
+ // we fall back to a disk load here so unit tests and early call sites
259
+ // don't have to depend on startup ordering.
260
+ _secret = loadOrCreateCapabilityTokenSecret();
261
+ return _secret;
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Mint / verify
266
+ // ---------------------------------------------------------------------------
267
+
268
+ const DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 minutes
269
+
270
+ function base64urlEncode(buf: Buffer): string {
271
+ return buf
272
+ .toString("base64")
273
+ .replace(/\+/g, "-")
274
+ .replace(/\//g, "_")
275
+ .replace(/=+$/, "");
276
+ }
277
+
278
+ function base64urlDecode(s: string): Buffer {
279
+ const pad = s.length % 4 === 0 ? 0 : 4 - (s.length % 4);
280
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(pad);
281
+ return Buffer.from(b64, "base64");
282
+ }
283
+
284
+ function sign(payload: string, secret: Buffer): string {
285
+ return base64urlEncode(createHmac("sha256", secret).update(payload).digest());
286
+ }
287
+
288
+ /**
289
+ * Mint a capability token bound to the `host_browser_command` capability
290
+ * for the given guardian id. Default TTL is 30 minutes.
291
+ */
292
+ export function mintHostBrowserCapability(
293
+ guardianId: string,
294
+ ttlMs: number = DEFAULT_TTL_MS,
295
+ ): CapabilityToken {
296
+ const expiresAt = Date.now() + ttlMs;
297
+ const nonce = randomBytes(16).toString("hex");
298
+ const claims: CapabilityClaims = {
299
+ capability: "host_browser_command",
300
+ guardianId,
301
+ nonce,
302
+ expiresAt,
303
+ };
304
+ const payload = base64urlEncode(Buffer.from(JSON.stringify(claims), "utf8"));
305
+ const sig = sign(payload, getSecret());
306
+ return { token: `${payload}.${sig}`, expiresAt };
307
+ }
308
+
309
+ /**
310
+ * Verify a capability token minted by `mintHostBrowserCapability`.
311
+ *
312
+ * Returns the decoded claims on success or null if the signature is
313
+ * invalid, the payload is malformed, the token has expired, or the bound
314
+ * capability is not `host_browser_command`.
315
+ *
316
+ * Signature comparison uses `timingSafeEqual` to avoid leaking the secret
317
+ * through timing side channels.
318
+ *
319
+ * The `/v1/browser-relay` WebSocket upgrade handler in `http-server.ts`
320
+ * (`handleBrowserRelayUpgrade`) calls this to authenticate self-hosted
321
+ * chrome extensions on the capability-token branch before falling
322
+ * through to the JWT compatibility path. The `/v1/host-browser-result`
323
+ * POST route may also call it (see that route's auth handling) when a
324
+ * result is posted back with a capability-token bearer instead of a
325
+ * guardian-bound JWT.
326
+ */
327
+ export function verifyHostBrowserCapability(
328
+ token: string,
329
+ ): CapabilityClaims | null {
330
+ if (typeof token !== "string") return null;
331
+ const dot = token.indexOf(".");
332
+ if (dot < 0) return null;
333
+ const payload = token.slice(0, dot);
334
+ const sig = token.slice(dot + 1);
335
+ if (!payload || !sig) return null;
336
+
337
+ const expected = sign(payload, getSecret());
338
+ const a = Buffer.from(sig, "utf8");
339
+ const b = Buffer.from(expected, "utf8");
340
+ if (a.length !== b.length) return null;
341
+ if (!timingSafeEqual(a, b)) return null;
342
+
343
+ let claims: CapabilityClaims;
344
+ try {
345
+ claims = JSON.parse(
346
+ base64urlDecode(payload).toString("utf8"),
347
+ ) as CapabilityClaims;
348
+ } catch {
349
+ return null;
350
+ }
351
+
352
+ if (!claims || typeof claims !== "object") return null;
353
+ if (claims.capability !== "host_browser_command") return null;
354
+ if (typeof claims.guardianId !== "string" || claims.guardianId.length === 0) {
355
+ return null;
356
+ }
357
+ if (typeof claims.nonce !== "string" || claims.nonce.length === 0) {
358
+ return null;
359
+ }
360
+ if (typeof claims.expiresAt !== "number" || claims.expiresAt <= Date.now()) {
361
+ return null;
362
+ }
363
+ return claims;
364
+ }
365
+
366
+ // ---------------------------------------------------------------------------
367
+ // Dev-only fallback token file
368
+ // ---------------------------------------------------------------------------
369
+
370
+ /**
371
+ * Path to the dev-pairing fallback token file. The runtime writes a freshly
372
+ * minted capability token to this location on daemon startup so developers
373
+ * can manually pair the chrome extension without wiring the native
374
+ * messaging helper. Production users should pair via the native helper
375
+ * (PRs 7/12/13).
376
+ */
377
+ export function getDaemonTokenFilePath(): string {
378
+ // Always under `~/.vellum/` (not the configurable workspace dir) so the
379
+ // native messaging helper can find it at a fixed path regardless of
380
+ // workspace overrides. This is a dev-only convenience path — production
381
+ // pairing goes through the native messaging flow.
382
+ return join(homedir(), ".vellum", "daemon-token");
383
+ }
384
+
385
+ /**
386
+ * Write a freshly-minted capability token to `~/.vellum/daemon-token` with
387
+ * 0600 permissions. Swallows errors so a failure here never blocks daemon
388
+ * startup — this is a dev-convenience path, not a production auth
389
+ * requirement.
390
+ */
391
+ export function writeDaemonTokenFallback(guardianId: string): void {
392
+ try {
393
+ const { token } = mintHostBrowserCapability(guardianId);
394
+ const filePath = getDaemonTokenFilePath();
395
+ const dir = dirname(filePath);
396
+ if (!existsSync(dir)) {
397
+ mkdirSync(dir, { recursive: true });
398
+ }
399
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
400
+ writeFileSync(tmpPath, token, { mode: 0o600 });
401
+ renameSync(tmpPath, filePath);
402
+ try {
403
+ chmodSync(filePath, 0o600);
404
+ } catch {
405
+ // best-effort
406
+ }
407
+ log.info({ filePath }, "Dev capability token written to daemon-token file");
408
+ } catch (err) {
409
+ log.warn(
410
+ { err },
411
+ "Failed to write dev capability token file; manual pairing still available via /v1/browser-extension-pair",
412
+ );
413
+ }
414
+ }
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { addRule } from "../permissions/trust-store.js";
13
13
  import type { UserDecision } from "../permissions/types.js";
14
+ import { isPermissionControlsV2Enabled } from "../permissions/v2-consent-policy.js";
14
15
  import { getTool } from "../tools/registry.js";
15
16
  import { composeApprovalMessage } from "./approval-message-composer.js";
16
17
  import type {
@@ -22,6 +23,7 @@ import type {
22
23
  import { toApprovalActionOptions } from "./channel-approval-types.js";
23
24
  import {
24
25
  buildDecisionActions,
26
+ buildOneTimeDecisionActions,
25
27
  buildPlainTextFallback,
26
28
  } from "./guardian-decision-types.js";
27
29
  import * as pendingInteractions from "./pending-interactions.js";
@@ -92,9 +94,11 @@ function buildPromptFromApprovalInfo(
92
94
  toolName: info.toolName,
93
95
  });
94
96
 
95
- const decisionActions = buildDecisionActions({
96
- persistentDecisionsAllowed: info.persistentDecisionsAllowed,
97
- });
97
+ const decisionActions = isPermissionControlsV2Enabled()
98
+ ? buildOneTimeDecisionActions()
99
+ : buildDecisionActions({
100
+ persistentDecisionsAllowed: info.persistentDecisionsAllowed,
101
+ });
98
102
  const actions = toApprovalActionOptions(decisionActions);
99
103
  const plainTextFallback = buildPlainTextFallback(promptText, decisionActions);
100
104
 
@@ -138,6 +142,10 @@ export function buildApprovalUIMetadata(
138
142
  * the permission pipeline can activate the appropriate override.
139
143
  */
140
144
  function mapApprovalActionToUserDecision(action: ApprovalAction): UserDecision {
145
+ if (isPermissionControlsV2Enabled()) {
146
+ return action === "reject" ? "deny" : "allow";
147
+ }
148
+
141
149
  switch (action) {
142
150
  case "reject":
143
151
  return "deny";
@@ -182,7 +190,10 @@ export function handleChannelDecision(
182
190
  : pending[0];
183
191
  if (!info) return { applied: false };
184
192
 
185
- if (decision.action === "approve_always") {
193
+ if (
194
+ !isPermissionControlsV2Enabled() &&
195
+ decision.action === "approve_always"
196
+ ) {
186
197
  // Only persist a trust rule when the confirmation explicitly allows persistence
187
198
  // AND provides explicit allowlist/scope options. Without explicit options we
188
199
  // would create a blanket "**"/"everywhere" rule, which is a security risk.
@@ -264,7 +275,9 @@ export function buildGuardianApprovalPrompt(
264
275
  requesterIdentifier,
265
276
  });
266
277
 
267
- const decisionActions = buildDecisionActions({ forGuardianOnBehalf: true });
278
+ const decisionActions = isPermissionControlsV2Enabled()
279
+ ? buildOneTimeDecisionActions()
280
+ : buildDecisionActions({ forGuardianOnBehalf: true });
268
281
  const actions = toApprovalActionOptions(decisionActions);
269
282
  const plainTextFallback = buildPlainTextFallback(promptText, decisionActions);
270
283