@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
@@ -8,7 +8,7 @@ import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
8
8
  class HostFileWriteTool implements Tool {
9
9
  name = "host_file_write";
10
10
  description =
11
- "Write content to a file on the host filesystem, creating it if it does not exist. Not for workspace files under .vellum (use file_write instead).";
11
+ "Write content to a file on your guardian's device, creating it if it does not exist. For files on your own machine, use file_write instead.";
12
12
  category = "host-filesystem";
13
13
  defaultRiskLevel = RiskLevel.Medium;
14
14
 
@@ -207,30 +207,35 @@ class HostShellTool implements Tool {
207
207
  detached: true,
208
208
  });
209
209
 
210
- const timer = setTimeout(() => {
211
- timedOut = true;
210
+ // Kill the entire process tree. Tries the process group first
211
+ // (negative PID), then falls back to killing the direct child if the
212
+ // PID is unavailable or the group kill fails.
213
+ const killTree = () => {
214
+ if (child.pid != null) {
215
+ try {
216
+ process.kill(-child.pid, "SIGKILL");
217
+ return;
218
+ } catch {
219
+ // Process group may have already exited — fall through.
220
+ }
221
+ }
212
222
  try {
213
- process.kill(-child.pid!, "SIGKILL");
223
+ child.kill("SIGKILL");
214
224
  } catch {
215
- // Process group may have already exited.
225
+ // Child may have already exited.
216
226
  }
227
+ };
228
+
229
+ const timer = setTimeout(() => {
230
+ timedOut = true;
231
+ killTree();
217
232
  }, timeoutMs);
218
233
 
219
234
  // Cooperative cancellation via AbortSignal
220
- const onAbort = () => {
221
- try {
222
- process.kill(-child.pid!, "SIGKILL");
223
- } catch {
224
- // Process group may have already exited.
225
- }
226
- };
235
+ const onAbort = () => killTree();
227
236
  if (context.signal) {
228
237
  if (context.signal.aborted) {
229
- try {
230
- process.kill(-child.pid!, "SIGKILL");
231
- } catch {
232
- // Process group may have already exited.
233
- }
238
+ killTree();
234
239
  } else {
235
240
  context.signal.addEventListener("abort", onAbort, { once: true });
236
241
  }
@@ -7,11 +7,14 @@ import {
7
7
  generateAllowlistOptions,
8
8
  generateScopeOptions,
9
9
  } from "../permissions/checker.js";
10
- import { getMode } from "../permissions/permission-mode-store.js";
11
10
  import type { PermissionPrompter } from "../permissions/prompter.js";
12
11
  import { addRule } from "../permissions/trust-store.js";
13
12
  import { RiskLevel } from "../permissions/types.js";
14
- import { isHostTool } from "../permissions/workspace-policy.js";
13
+ import {
14
+ CONVERSATION_HOST_ACCESS_PROMPT,
15
+ evaluateV2ConsentDisposition,
16
+ isConversationHostAccessDecision,
17
+ } from "../permissions/v2-consent-policy.js";
15
18
  import {
16
19
  getEffectiveMode,
17
20
  setConversationMode,
@@ -67,50 +70,29 @@ export class PermissionChecker {
67
70
  }
68
71
  | undefined,
69
72
  ): Promise<PermissionDecision> {
70
- // ── permission-controls-v2 early gate ──────────────────────────────
71
- // When the v2 flag is enabled, replace the entire risk-classification
72
- // path with a simple binary check: is it a host tool + is host access
73
- // enabled? Certain security gates (requireFreshApproval,
74
- // forcePromptSideEffects, hostAccess=false) fall through to the v1
75
- // prompt flow so the interactive prompter is engaged.
76
- const cfg = getConfig();
77
73
  let v2ForcePrompt = false;
78
- if (isAssistantFeatureFlagEnabled("permission-controls-v2", cfg)) {
79
- // requireFreshApproval demands an interactive prompt every time —
80
- // fall through to v1 so the prompter is engaged.
81
- const needsFreshApproval = !!context.requireFreshApproval;
82
-
83
- // forcePromptSideEffects (private conversations, untrusted actors)
84
- // requires explicit approval for side-effect tools.
85
- const needsSideEffectPrompt =
86
- !!context.forcePromptSideEffects && isSideEffectTool(name, input);
87
-
88
- if (!needsFreshApproval && !needsSideEffectPrompt) {
89
- if (isHostTool(name)) {
90
- const mode = getMode();
91
- if (mode.hostAccess) {
92
- return {
93
- allowed: true,
94
- decision: "allow",
95
- riskLevel: RiskLevel.Low,
96
- };
97
- }
98
- // Host tool with hostAccess disabled — fall through to v1 so the
99
- // interactive prompter is engaged (returning allowed:false here
100
- // would surface an error string instead of a permission dialog).
101
- // The v2ForcePrompt flag ensures check()'s allow decision is
102
- // promoted to prompt so the user sees a permission dialog.
103
- v2ForcePrompt = true;
104
- } else {
105
- // Non-host tools are auto-allowed when v2 is on
106
- return {
107
- allowed: true,
108
- decision: "allow",
109
- riskLevel: RiskLevel.Low,
110
- };
111
- }
74
+ const cfg = getConfig();
75
+ const v2Enabled = isAssistantFeatureFlagEnabled(
76
+ "permission-controls-v2",
77
+ cfg,
78
+ );
79
+ if (v2Enabled) {
80
+ const v2Disposition = evaluateV2ConsentDisposition(name, input, context);
81
+ if (v2Disposition === "auto_allow") {
82
+ return {
83
+ allowed: true,
84
+ decision: "allow",
85
+ riskLevel: RiskLevel.Low,
86
+ };
87
+ }
88
+ if (v2Disposition === "prompt_host_access") {
89
+ // Host tool with hostAccess disabled — fall through to v1 so the
90
+ // interactive prompter is engaged (returning allowed:false here
91
+ // would surface an error string instead of a permission dialog).
92
+ // The v2ForcePrompt flag ensures check()'s allow decision is
93
+ // promoted to prompt so the user sees a permission dialog.
94
+ v2ForcePrompt = true;
112
95
  }
113
- // Falls through to the v1 risk-classification + prompter path
114
96
  }
115
97
 
116
98
  const risk = await classifyRisk(
@@ -301,25 +283,34 @@ export class PermissionChecker {
301
283
  return { allowed: true, decision: "temporary_override", riskLevel };
302
284
  }
303
285
 
304
- const allowlistOptions = await generateAllowlistOptions(
305
- name,
306
- input,
307
- context.signal,
308
- );
309
- const scopeOptions = generateScopeOptions(context.workingDir, name);
310
286
  const previewDiff = computePreviewDiff(name, input, context.workingDir);
311
-
312
- const persistentDecisionsAllowed = !context.requireFreshApproval;
313
-
314
- // Offer temporary approval options to guardians. Suppressed when
315
- // requireFreshApproval is true - temporary overrides would be
316
- // misleading since future invocations still require fresh approval.
317
- const temporaryOptionsAvailable:
318
- | Array<"allow_10m" | "allow_conversation">
319
- | undefined =
320
- context.trustClass === "guardian" && !context.requireFreshApproval
321
- ? ["allow_10m", "allow_conversation"]
322
- : undefined;
287
+ const promptOptions = v2ForcePrompt
288
+ ? CONVERSATION_HOST_ACCESS_PROMPT
289
+ : v2Enabled
290
+ ? {
291
+ allowlistOptions: [] as Awaited<
292
+ ReturnType<typeof generateAllowlistOptions>
293
+ >,
294
+ scopeOptions: [] as ReturnType<typeof generateScopeOptions>,
295
+ persistentDecisionsAllowed: false,
296
+ temporaryOptionsAvailable: undefined,
297
+ }
298
+ : {
299
+ allowlistOptions: await generateAllowlistOptions(
300
+ name,
301
+ input,
302
+ context.signal,
303
+ ),
304
+ scopeOptions: generateScopeOptions(context.workingDir, name),
305
+ persistentDecisionsAllowed: !context.requireFreshApproval,
306
+ temporaryOptionsAvailable:
307
+ context.trustClass === "guardian" &&
308
+ !context.requireFreshApproval
309
+ ? (["allow_10m", "allow_conversation"] as Array<
310
+ "allow_10m" | "allow_conversation"
311
+ >)
312
+ : undefined,
313
+ };
323
314
 
324
315
  emitLifecycleEvent({
325
316
  type: "permission_prompt",
@@ -331,10 +322,10 @@ export class PermissionChecker {
331
322
  requestId: context.requestId,
332
323
  riskLevel,
333
324
  reason: result.reason,
334
- allowlistOptions,
335
- scopeOptions,
325
+ allowlistOptions: promptOptions.allowlistOptions,
326
+ scopeOptions: promptOptions.scopeOptions,
336
327
  diff: previewDiff,
337
- persistentDecisionsAllowed,
328
+ persistentDecisionsAllowed: promptOptions.persistentDecisionsAllowed,
338
329
  });
339
330
 
340
331
  await getHookManager().trigger("permission-request", {
@@ -348,27 +339,31 @@ export class PermissionChecker {
348
339
  name,
349
340
  input,
350
341
  riskLevel,
351
- allowlistOptions,
352
- scopeOptions,
342
+ promptOptions.allowlistOptions,
343
+ promptOptions.scopeOptions,
353
344
  previewDiff,
354
345
  context.conversationId,
355
346
  executionTarget,
356
- persistentDecisionsAllowed,
347
+ promptOptions.persistentDecisionsAllowed,
357
348
  context.signal,
358
- temporaryOptionsAvailable,
349
+ promptOptions.temporaryOptionsAvailable,
359
350
  context.toolUseId,
351
+ v2ForcePrompt,
360
352
  );
361
353
 
362
- const decision = response.decision;
354
+ const decision =
355
+ v2ForcePrompt && !isConversationHostAccessDecision(response.decision)
356
+ ? "deny"
357
+ : response.decision;
363
358
 
364
359
  await getHookManager().trigger("permission-resolve", {
365
360
  toolName: name,
366
- decision: response.decision,
361
+ decision,
367
362
  riskLevel,
368
363
  conversationId: context.conversationId,
369
364
  });
370
365
 
371
- if (response.decision === "deny") {
366
+ if (decision === "deny") {
372
367
  const contextualDenial =
373
368
  typeof response.decisionContext === "string"
374
369
  ? response.decisionContext.trim()
@@ -403,15 +398,15 @@ export class PermissionChecker {
403
398
  };
404
399
  }
405
400
 
406
- if (response.decision === "always_deny") {
401
+ if (decision === "always_deny") {
407
402
  // For non-scoped tools (empty scopeOptions), default to 'everywhere' since
408
403
  // the client has no scope picker and will send undefined.
409
404
  const effectiveDenyScope =
410
- scopeOptions.length === 0
405
+ promptOptions.scopeOptions.length === 0
411
406
  ? (response.selectedScope ?? "everywhere")
412
407
  : response.selectedScope;
413
408
  const ruleSaved = !!(
414
- persistentDecisionsAllowed &&
409
+ promptOptions.persistentDecisionsAllowed &&
415
410
  response.selectedPattern &&
416
411
  effectiveDenyScope
417
412
  );
@@ -452,9 +447,9 @@ export class PermissionChecker {
452
447
  }
453
448
 
454
449
  if (
455
- persistentDecisionsAllowed &&
456
- (response.decision === "always_allow" ||
457
- response.decision === "always_allow_high_risk") &&
450
+ promptOptions.persistentDecisionsAllowed &&
451
+ (decision === "always_allow" ||
452
+ decision === "always_allow_high_risk") &&
458
453
  response.selectedPattern
459
454
  ) {
460
455
  const ruleOptions: {
@@ -462,7 +457,7 @@ export class PermissionChecker {
462
457
  executionTarget?: string;
463
458
  } = {};
464
459
 
465
- if (response.decision === "always_allow_high_risk") {
460
+ if (decision === "always_allow_high_risk") {
466
461
  ruleOptions.allowHighRisk = true;
467
462
  }
468
463
 
@@ -474,7 +469,7 @@ export class PermissionChecker {
474
469
  // Only default to 'everywhere' for non-scoped tools (empty scopeOptions).
475
470
  // For scoped tools, require an explicit scope to prevent silent permission widening.
476
471
  const effectiveScope =
477
- scopeOptions.length === 0
472
+ promptOptions.scopeOptions.length === 0
478
473
  ? (response.selectedScope ?? "everywhere")
479
474
  : response.selectedScope;
480
475
  if (effectiveScope) {
@@ -493,13 +488,13 @@ export class PermissionChecker {
493
488
  // time-limited or conversation-scoped override. Subsequent tool
494
489
  // invocations in this conversation will auto-approve without
495
490
  // prompting (checked above in the temporary override block).
496
- if (response.decision === "allow_10m") {
491
+ if (decision === "allow_10m") {
497
492
  setTimedMode(context.conversationId);
498
493
  log.info(
499
494
  { toolName: name, conversationId: context.conversationId },
500
495
  "Activated timed (10m) temporary approval mode",
501
496
  );
502
- } else if (response.decision === "allow_conversation") {
497
+ } else if (decision === "allow_conversation") {
503
498
  setConversationMode(context.conversationId);
504
499
  log.info(
505
500
  { toolName: name, conversationId: context.conversationId },
@@ -8,7 +8,6 @@ import { hostFileReadTool } from "./host-filesystem/read.js";
8
8
  import { hostFileWriteTool } from "./host-filesystem/write.js";
9
9
  import { hostShellTool } from "./host-terminal/host-shell.js";
10
10
  import { registerSystemTools } from "./system/register.js";
11
- import { setPermissionModeTool } from "./system/set-permission-mode.js";
12
11
  import type { Tool } from "./types.js";
13
12
  import { allUiSurfaceTools } from "./ui-surface/definitions.js";
14
13
  import { registerUiSurfaceTools } from "./ui-surface/registry.js";
@@ -285,7 +284,6 @@ export async function initializeTools(): Promise<void> {
285
284
  ...allComputerUseTools.map((t: Tool) => t.name),
286
285
  ...allUiSurfaceTools.map((t: Tool) => t.name),
287
286
  ...coreAppProxyTools.map((t: Tool) => t.name),
288
- setPermissionModeTool.name,
289
287
  ]);
290
288
 
291
289
  coreToolsSnapshot = new Map<string, Tool>();
@@ -2,6 +2,7 @@ import { getConfig } from "../config/loader.js";
2
2
  import { getHookManager } from "../hooks/manager.js";
3
3
  import { PermissionPrompter } from "../permissions/prompter.js";
4
4
  import { RiskLevel } from "../permissions/types.js";
5
+ import { isPermissionControlsV2Enabled } from "../permissions/v2-consent-policy.js";
5
6
  import type { SecretPattern } from "../security/secret-scanner.js";
6
7
  import {
7
8
  compileCustomPatterns,
@@ -269,6 +270,39 @@ export class SecretDetectionHandler {
269
270
  ): Promise<{ result: ToolExecutionResult; earlyReturn: boolean }> {
270
271
  const types = [...new Set(allMatches.map((m) => m.type))].join(", ");
271
272
 
273
+ if (isPermissionControlsV2Enabled()) {
274
+ const blockedContent = `Tool output blocked: detected ${allMatches.length} potential secret(s) (${types}). Secret-output approval cards are disabled under v2. Ask the user for confirmation conversationally before retrying.`;
275
+ const durationMs = Date.now() - startTime;
276
+
277
+ emitLifecycleEvent(context, {
278
+ type: "permission_denied",
279
+ toolName: name,
280
+ executionTarget,
281
+ input,
282
+ workingDir: context.workingDir,
283
+ conversationId: context.conversationId,
284
+ requestId: context.requestId,
285
+ riskLevel: RiskLevel.High,
286
+ decision: "deny",
287
+ reason: "Secret output blocked without deterministic prompt under v2",
288
+ durationMs,
289
+ });
290
+
291
+ void getHookManager().trigger("post-tool-execute", {
292
+ toolName: name,
293
+ input: sanitizeToolInput(name, input),
294
+ riskLevel,
295
+ isError: true,
296
+ durationMs,
297
+ conversationId: context.conversationId,
298
+ });
299
+
300
+ return {
301
+ result: { content: blockedContent, isError: true },
302
+ earlyReturn: true,
303
+ };
304
+ }
305
+
272
306
  // Non-interactive sessions: auto-block secret output instead of waiting for prompt
273
307
  if (context.isInteractive === false) {
274
308
  const blockedContent = `Tool output blocked: detected ${allMatches.length} potential secret(s) (${types}). No interactive client available to approve.`;
@@ -61,63 +61,37 @@ function detectMediaType(buf: Buffer): string | null {
61
61
  return null;
62
62
  }
63
63
 
64
- /**
65
- * Read an image file from disk, optionally optimize it, and return a
66
- * ToolExecutionResult with base64-encoded image content blocks.
67
- *
68
- * The caller is responsible for path resolution and sandbox enforcement -
69
- * `resolvedPath` must be an already-validated absolute path.
70
- */
71
- export function readImageFile(resolvedPath: string): ToolExecutionResult {
72
- let stat;
73
- try {
74
- stat = statSync(resolvedPath);
75
- } catch {
76
- return {
77
- content: `Error: file not found: ${resolvedPath}`,
78
- isError: true,
79
- };
80
- }
81
-
82
- if (!stat.isFile()) {
83
- return { content: `Error: ${resolvedPath} is not a file`, isError: true };
84
- }
85
-
86
- if (stat.size > MAX_SOURCE_SIZE_BYTES) {
87
- const sizeMB = (stat.size / (1024 * 1024)).toFixed(1);
64
+ function buildImageToolResult(
65
+ buffer: Buffer,
66
+ sourceLabel: string,
67
+ ): ToolExecutionResult {
68
+ if (buffer.length > MAX_SOURCE_SIZE_BYTES) {
69
+ const sizeMB = (buffer.length / (1024 * 1024)).toFixed(1);
88
70
  return {
89
71
  content: `Error: image too large (${sizeMB} MB). Maximum source file size is 100 MB.`,
90
72
  isError: true,
91
73
  };
92
74
  }
93
75
 
94
- let buffer: Buffer;
95
- try {
96
- buffer = readFileSync(resolvedPath) as Buffer;
97
- } catch (err) {
98
- const msg = err instanceof Error ? err.message : String(err);
99
- return { content: `Error reading file: ${msg}`, isError: true };
100
- }
101
-
102
76
  // Detect actual format from magic bytes - never trust the file extension
103
77
  // alone, since sips converts to JPEG and files can be misnamed.
104
78
  const detectedType = detectMediaType(buffer);
105
79
  if (!detectedType) {
106
80
  return {
107
- content: `Error: could not detect image format for ${resolvedPath}. The file may be corrupt.`,
81
+ content: `Error: could not detect image format for ${sourceLabel}. The file may be corrupt.`,
108
82
  isError: true,
109
83
  };
110
84
  }
111
85
 
112
86
  // Optimize before size-checking — oversized images may compress under the limit.
113
87
  const rawBase64 = buffer.toString("base64");
114
- const { data: base64Data, mediaType: finalType } =
115
- optimizeImageForTransport(rawBase64, detectedType);
88
+ const { data: base64Data, mediaType: finalType } = optimizeImageForTransport(
89
+ rawBase64,
90
+ detectedType,
91
+ );
116
92
  const optimized = base64Data !== rawBase64;
117
93
 
118
- const optimizedBytes = optimized
119
- ? Math.ceil((base64Data.length * 3) / 4)
120
- : buffer.length;
94
+ const optimizedBytes = Buffer.from(base64Data, "base64").length;
121
95
  if (optimizedBytes > MAX_SIZE_BYTES) {
122
96
  const sizeMB = (optimizedBytes / (1024 * 1024)).toFixed(1);
123
97
  return {
@@ -136,14 +110,61 @@ export function readImageFile(resolvedPath: string): ToolExecutionResult {
136
110
  };
137
111
 
138
112
  const sizeSuffix = optimized
139
- ? ` (optimized from ${(stat.size / 1024).toFixed(0)} KB to ${(
113
+ ? ` (optimized from ${(buffer.length / 1024).toFixed(0)} KB to ${(
140
114
  optimizedBytes / 1024
141
115
  ).toFixed(0)} KB)`
142
116
  : "";
143
117
 
144
118
  return {
145
- content: `Image loaded: ${resolvedPath} (${optimizedBytes} bytes, ${finalType})${sizeSuffix}`,
119
+ content: `Image loaded: ${sourceLabel} (${optimizedBytes} bytes, ${finalType})${sizeSuffix}`,
146
120
  isError: false,
147
121
  contentBlocks: [imageBlock],
148
122
  };
149
123
  }
124
+
125
+ export function readImageBase64(
126
+ base64Data: string,
127
+ sourceLabel: string,
128
+ ): ToolExecutionResult {
129
+ return buildImageToolResult(Buffer.from(base64Data, "base64"), sourceLabel);
130
+ }
131
+
132
+ /**
133
+ * Read an image file from disk, optionally optimize it, and return a
134
+ * ToolExecutionResult with base64-encoded image content blocks.
135
+ *
136
+ * The caller is responsible for path resolution and sandbox enforcement -
137
+ * `resolvedPath` must be an already-validated absolute path.
138
+ */
139
+ export function readImageFile(resolvedPath: string): ToolExecutionResult {
140
+ let stat;
141
+ try {
142
+ stat = statSync(resolvedPath);
143
+ } catch {
144
+ return {
145
+ content: `Error: file not found: ${resolvedPath}`,
146
+ isError: true,
147
+ };
148
+ }
149
+
150
+ if (!stat.isFile()) {
151
+ return { content: `Error: ${resolvedPath} is not a file`, isError: true };
152
+ }
153
+
154
+ if (stat.size > MAX_SOURCE_SIZE_BYTES) {
155
+ const sizeMB = (stat.size / (1024 * 1024)).toFixed(1);
156
+ return {
157
+ content: `Error: image too large (${sizeMB} MB). Maximum source file size is 100 MB.`,
158
+ isError: true,
159
+ };
160
+ }
161
+
162
+ let buffer: Buffer;
163
+ try {
164
+ buffer = readFileSync(resolvedPath) as Buffer;
165
+ } catch (err) {
166
+ const msg = err instanceof Error ? err.message : String(err);
167
+ return { content: `Error reading file: ${msg}`, isError: true };
168
+ }
169
+ return buildImageToolResult(buffer, resolvedPath);
170
+ }
@@ -8,9 +8,15 @@ export async function executeSubagentSpawn(
8
8
  const label = input.label as string;
9
9
  const objective = input.objective as string;
10
10
  const extraContext = input.context as string | undefined;
11
- const sendResultToUser = input.send_result_to_user !== false;
11
+ const fork = input.fork === true;
12
12
  const role = (input.role as string | undefined) ?? undefined;
13
13
 
14
+ // For fork mode, sendResultToUser defaults to false unless explicitly set to true.
15
+ // For regular mode, sendResultToUser defaults to true (existing behavior).
16
+ const sendResultToUser = fork
17
+ ? input.send_result_to_user === true
18
+ : input.send_result_to_user !== false;
19
+
14
20
  if (!label || !objective) {
15
21
  return {
16
22
  content: 'Both "label" and "objective" are required.',
@@ -29,6 +35,36 @@ export async function executeSubagentSpawn(
29
35
  };
30
36
  }
31
37
 
38
+ // ── Fork mode: resolve parent context ────────────────────────────
39
+ let forkFields: {
40
+ fork: true;
41
+ parentMessages: import("../../providers/types.js").Message[];
42
+ parentSystemPrompt: string;
43
+ } | undefined;
44
+
45
+ if (fork) {
46
+ const parentConversation = manager.resolveParentConversation?.(
47
+ context.conversationId,
48
+ );
49
+ if (!parentConversation) {
50
+ return {
51
+ content:
52
+ "Cannot fork: parent conversation could not be resolved. " +
53
+ "This may happen if the conversation was evicted or the resolveParentConversation callback is not wired.",
54
+ isError: true,
55
+ };
56
+ }
57
+
58
+ const parentMessages = [...parentConversation.messages];
59
+ const parentSystemPrompt = parentConversation.getCurrentSystemPrompt();
60
+
61
+ forkFields = {
62
+ fork: true,
63
+ parentMessages,
64
+ parentSystemPrompt,
65
+ };
66
+ }
67
+
32
68
  try {
33
69
  const subagentId = await manager.spawn(
34
70
  {
@@ -37,7 +73,12 @@ export async function executeSubagentSpawn(
37
73
  objective,
38
74
  context: extraContext,
39
75
  sendResultToUser,
40
- ...(role ? { role: role as import("../../subagent/types.js").SubagentRole } : {}),
76
+ // For fork mode, role is ignored by the manager (forced to general),
77
+ // but we still omit it from the config to signal intent.
78
+ ...(!fork && role
79
+ ? { role: role as import("../../subagent/types.js").SubagentRole }
80
+ : {}),
81
+ ...forkFields,
41
82
  },
42
83
  sendToClient as (msg: unknown) => void,
43
84
  );
@@ -47,7 +88,10 @@ export async function executeSubagentSpawn(
47
88
  subagentId,
48
89
  label,
49
90
  status: "pending",
50
- message: `Subagent "${label}" spawned. You will be notified automatically when it completes or fails - do NOT poll subagent_status. Continue the conversation normally.`,
91
+ ...(fork ? { isFork: true } : {}),
92
+ message: fork
93
+ ? `Forked subagent "${label}" spawned with full parent context. You will be notified automatically when it completes or fails - do NOT poll subagent_status. Continue the conversation normally.`
94
+ : `Subagent "${label}" spawned. You will be notified automatically when it completes or fails - do NOT poll subagent_status. Continue the conversation normally.`,
51
95
  }),
52
96
  isError: false,
53
97
  };
@@ -34,6 +34,7 @@ export async function executeSubagentStatus(
34
34
  subagentId: state.config.id,
35
35
  label: state.config.label,
36
36
  status: state.status,
37
+ isFork: state.isFork,
37
38
  error: state.error,
38
39
  createdAt: state.createdAt,
39
40
  startedAt: state.startedAt,
@@ -57,6 +58,7 @@ export async function executeSubagentStatus(
57
58
  subagentId: s.config.id,
58
59
  label: s.config.label,
59
60
  status: s.status,
61
+ isFork: s.isFork,
60
62
  error: s.error,
61
63
  }));
62
64
 
@@ -1,23 +1,9 @@
1
1
  /**
2
2
  * Registers feature-flag-gated system tools with the daemon's tool registry.
3
3
  *
4
- * Called once at daemon startup via initializeTools(). Tools that are always
5
- * registered (e.g. request_system_permission) are handled via the tool
6
- * manifest's explicit tools list; this module handles conditional registration.
4
+ * No conditional tools are currently registered.
7
5
  */
8
6
 
9
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
10
- import { getConfig } from "../../config/loader.js";
11
- import { registerTool } from "../registry.js";
12
- import { setPermissionModeTool } from "./set-permission-mode.js";
13
-
14
7
  export function registerSystemTools(): void {
15
- try {
16
- const config = getConfig();
17
- if (isAssistantFeatureFlagEnabled("permission-controls-v2", config)) {
18
- registerTool(setPermissionModeTool);
19
- }
20
- } catch {
21
- // Config not yet loaded (e.g. during test setup) — permission mode tool stays off.
22
- }
8
+ // No-op.
23
9
  }