@vellumai/assistant 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (463) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docker-entrypoint.sh +12 -2
  4. package/docs/architecture/memory.md +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  7. package/openapi.yaml +184 -69
  8. package/package.json +41 -41
  9. package/scripts/generate-openapi.ts +1 -2
  10. package/src/__tests__/acp-session.test.ts +43 -0
  11. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +1 -0
  13. package/src/__tests__/app-source-watcher.test.ts +37 -11
  14. package/src/__tests__/approval-routes-http.test.ts +178 -1
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  17. package/src/__tests__/browser-manager.test.ts +40 -27
  18. package/src/__tests__/catalog-files.test.ts +862 -0
  19. package/src/__tests__/channel-approvals.test.ts +53 -0
  20. package/src/__tests__/checker.test.ts +104 -170
  21. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  22. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  23. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  24. package/src/__tests__/config-schema.test.ts +125 -48
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  26. package/src/__tests__/context-overflow-approval.test.ts +21 -6
  27. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  28. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  29. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  30. package/src/__tests__/conversation-attachments.test.ts +80 -4
  31. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  32. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  33. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  34. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  35. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  36. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  37. package/src/__tests__/conversation-queue.test.ts +45 -2
  38. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  39. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  40. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  42. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  43. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  44. package/src/__tests__/conversation-store.test.ts +195 -0
  45. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  46. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -3
  47. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  48. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  49. package/src/__tests__/credential-vault.test.ts +152 -13
  50. package/src/__tests__/credentials-cli.test.ts +2 -2
  51. package/src/__tests__/date-context.test.ts +4 -4
  52. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  53. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  54. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  55. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  56. package/src/__tests__/gemini-provider.test.ts +2 -2
  57. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  58. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  59. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  60. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  61. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  62. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  63. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  64. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  65. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  66. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  67. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  68. package/src/__tests__/host-browser-routes.test.ts +198 -0
  69. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  70. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  71. package/src/__tests__/host-file-proxy.test.ts +185 -1
  72. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  73. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  74. package/src/__tests__/host-shell-tool.test.ts +1 -11
  75. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  76. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  77. package/src/__tests__/inline-command-runner.test.ts +7 -5
  78. package/src/__tests__/integration-status.test.ts +6 -7
  79. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  80. package/src/__tests__/log-export-workspace.test.ts +190 -0
  81. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  82. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  83. package/src/__tests__/mcp-health-check.test.ts +10 -3
  84. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  85. package/src/__tests__/migration-export-http.test.ts +61 -2
  86. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  87. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  88. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  89. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  90. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  91. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  92. package/src/__tests__/oauth-cli.test.ts +707 -60
  93. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  94. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  95. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  96. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  97. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  98. package/src/__tests__/oauth-store.test.ts +1386 -182
  99. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  100. package/src/__tests__/onboarding-template-contract.test.ts +74 -55
  101. package/src/__tests__/openai-provider.test.ts +2 -2
  102. package/src/__tests__/outlook-categories.test.ts +1 -1
  103. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  104. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  105. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  106. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  107. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  108. package/src/__tests__/outlook-trash.test.ts +1 -1
  109. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  110. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  111. package/src/__tests__/permission-mode.test.ts +28 -56
  112. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  113. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  114. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  115. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  116. package/src/__tests__/require-fresh-approval.test.ts +40 -3
  117. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  118. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  119. package/src/__tests__/schedule-routes.test.ts +162 -0
  120. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  121. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  122. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  123. package/src/__tests__/set-permission-mode.test.ts +13 -250
  124. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  125. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  126. package/src/__tests__/slack-channel-config.test.ts +12 -15
  127. package/src/__tests__/subagent-detail.test.ts +44 -2
  128. package/src/__tests__/subagent-disposal.test.ts +1 -0
  129. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  130. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  131. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  132. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  133. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  134. package/src/__tests__/subagent-tools.test.ts +1 -0
  135. package/src/__tests__/subagent-types.test.ts +1 -0
  136. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  137. package/src/__tests__/system-prompt.test.ts +72 -1
  138. package/src/__tests__/task-scheduler.test.ts +32 -6
  139. package/src/__tests__/telegram-config.test.ts +10 -13
  140. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  141. package/src/__tests__/terminal-tools.test.ts +11 -5
  142. package/src/__tests__/test-preload.ts +14 -0
  143. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  144. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  145. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  146. package/src/__tests__/tool-executor.test.ts +0 -1
  147. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  148. package/src/__tests__/top-level-renderer.test.ts +73 -1
  149. package/src/__tests__/transport-hints-queue.test.ts +62 -0
  150. package/src/__tests__/trust-store.test.ts +4 -4
  151. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  152. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  153. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  154. package/src/__tests__/workspace-policy.test.ts +2 -7
  155. package/src/acp/client-handler.ts +30 -4
  156. package/src/agent/loop.ts +12 -35
  157. package/src/approvals/guardian-request-resolvers.ts +21 -15
  158. package/src/browser-session/__tests__/manager.test.ts +297 -0
  159. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  160. package/src/browser-session/backends/extension.ts +26 -0
  161. package/src/browser-session/backends/local.ts +24 -0
  162. package/src/browser-session/events.ts +164 -0
  163. package/src/browser-session/index.ts +27 -0
  164. package/src/browser-session/manager.ts +159 -0
  165. package/src/browser-session/types.ts +28 -0
  166. package/src/channels/__tests__/types.test.ts +134 -0
  167. package/src/channels/types.ts +55 -0
  168. package/src/cli/__tests__/run-assistant-command.ts +34 -7
  169. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  170. package/src/cli/commands/browser-relay.ts +339 -409
  171. package/src/cli/commands/credentials.ts +3 -3
  172. package/src/cli/commands/default-action.ts +68 -1
  173. package/src/cli/commands/email.ts +18 -13
  174. package/src/cli/commands/mcp.ts +16 -4
  175. package/src/cli/commands/oauth/__tests__/connect.test.ts +68 -41
  176. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  177. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  178. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  179. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  180. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  181. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  182. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  183. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  184. package/src/cli/commands/oauth/apps.ts +7 -4
  185. package/src/cli/commands/oauth/connect.ts +16 -2
  186. package/src/cli/commands/oauth/disconnect.ts +1 -1
  187. package/src/cli/commands/oauth/providers.ts +200 -36
  188. package/src/cli/commands/oauth/shared.ts +5 -5
  189. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  190. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  191. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  192. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  193. package/src/cli/commands/platform/index.ts +107 -10
  194. package/src/cli/commands/usage.ts +10 -9
  195. package/src/cli/lib/daemon-credential-client.ts +4 -0
  196. package/src/cli/program.ts +10 -3
  197. package/src/config/assistant-feature-flags.ts +59 -55
  198. package/src/config/bundled-skills/app-builder/SKILL.md +33 -173
  199. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  200. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  201. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  202. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  203. package/src/config/bundled-skills/document/SKILL.md +4 -0
  204. package/src/config/bundled-skills/gmail/SKILL.md +12 -7
  205. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  206. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  207. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  208. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  209. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  210. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  211. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  212. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  213. package/src/config/env-registry.ts +14 -0
  214. package/src/config/env.ts +21 -0
  215. package/src/config/feature-flag-registry.json +46 -7
  216. package/src/config/loader.ts +56 -1
  217. package/src/config/sanitize-for-transfer.ts +47 -0
  218. package/src/config/schema.ts +46 -5
  219. package/src/config/schemas/host-browser.ts +66 -0
  220. package/src/config/schemas/memory-lifecycle.ts +1 -1
  221. package/src/config/schemas/memory-retrieval.ts +103 -0
  222. package/src/config/schemas/security.ts +0 -6
  223. package/src/config/schemas/services.ts +16 -0
  224. package/src/config/types.ts +0 -1
  225. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  226. package/src/context/window-manager.ts +19 -1
  227. package/src/credential-execution/approval-bridge.ts +49 -16
  228. package/src/credential-execution/managed-catalog.ts +3 -7
  229. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  230. package/src/daemon/app-source-watcher.ts +35 -0
  231. package/src/daemon/config-watcher.ts +6 -2
  232. package/src/daemon/context-overflow-approval.ts +5 -1
  233. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  234. package/src/daemon/conversation-agent-loop.ts +74 -19
  235. package/src/daemon/conversation-attachments.ts +40 -1
  236. package/src/daemon/conversation-messaging.ts +3 -0
  237. package/src/daemon/conversation-process.ts +66 -3
  238. package/src/daemon/conversation-queue-manager.ts +8 -0
  239. package/src/daemon/conversation-runtime-assembly.ts +159 -20
  240. package/src/daemon/conversation-surfaces.ts +78 -12
  241. package/src/daemon/conversation-tool-setup.ts +74 -11
  242. package/src/daemon/conversation-workspace.ts +12 -0
  243. package/src/daemon/conversation.ts +227 -11
  244. package/src/daemon/date-context.ts +10 -10
  245. package/src/daemon/first-greeting.ts +3 -2
  246. package/src/daemon/handlers/conversations.ts +9 -139
  247. package/src/daemon/handlers/shared.ts +65 -0
  248. package/src/daemon/handlers/skills.ts +232 -37
  249. package/src/daemon/host-bash-proxy.ts +48 -13
  250. package/src/daemon/host-browser-proxy.ts +191 -0
  251. package/src/daemon/host-cu-proxy.ts +36 -11
  252. package/src/daemon/host-file-proxy.ts +57 -9
  253. package/src/daemon/lifecycle.ts +86 -12
  254. package/src/daemon/message-protocol.ts +7 -0
  255. package/src/daemon/message-types/conversations.ts +59 -13
  256. package/src/daemon/message-types/host-browser.ts +100 -0
  257. package/src/daemon/message-types/messages.ts +5 -6
  258. package/src/daemon/message-types/notifications.ts +12 -0
  259. package/src/daemon/message-types/settings.ts +12 -0
  260. package/src/daemon/message-types/skills.ts +10 -0
  261. package/src/daemon/message-types/subagents.ts +2 -0
  262. package/src/daemon/server.ts +112 -35
  263. package/src/daemon/tool-side-effects.ts +6 -0
  264. package/src/daemon/transport-hints.ts +14 -0
  265. package/src/inbound/platform-callback-registration.ts +18 -17
  266. package/src/index.ts +1 -1
  267. package/src/mcp/client.ts +59 -24
  268. package/src/memory/app-store.ts +31 -1
  269. package/src/memory/conversation-crud.ts +38 -10
  270. package/src/memory/conversation-directories.ts +39 -0
  271. package/src/memory/conversation-group-migration.ts +65 -5
  272. package/src/memory/conversation-starters-cadence.ts +76 -0
  273. package/src/memory/conversation-title-service.ts +5 -2
  274. package/src/memory/db-init.ts +12 -0
  275. package/src/memory/embedding-backend.test.ts +75 -0
  276. package/src/memory/embedding-backend.ts +131 -5
  277. package/src/memory/embedding-gemini.test.ts +54 -0
  278. package/src/memory/embedding-gemini.ts +20 -9
  279. package/src/memory/embedding-local.ts +177 -18
  280. package/src/memory/graph/capability-seed.ts +3 -5
  281. package/src/memory/graph/consolidation.ts +10 -23
  282. package/src/memory/graph/extraction-job.ts +15 -0
  283. package/src/memory/graph/retriever.ts +40 -22
  284. package/src/memory/graph/store.test.ts +7 -3
  285. package/src/memory/graph/store.ts +47 -12
  286. package/src/memory/group-crud.ts +25 -9
  287. package/src/memory/llm-usage-store.ts +45 -4
  288. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  289. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  290. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  291. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  292. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  293. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  294. package/src/memory/migrations/index.ts +6 -0
  295. package/src/memory/migrations/registry.ts +8 -0
  296. package/src/memory/schema/conversations.ts +1 -0
  297. package/src/memory/schema/oauth.ts +18 -13
  298. package/src/messaging/provider.ts +1 -1
  299. package/src/notifications/broadcaster.ts +6 -0
  300. package/src/notifications/conversation-pairing.ts +12 -4
  301. package/src/notifications/emit-signal.ts +14 -0
  302. package/src/notifications/signal.ts +11 -0
  303. package/src/oauth/AGENTS.md +76 -0
  304. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  305. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  306. package/src/oauth/byo-connection.test.ts +8 -8
  307. package/src/oauth/byo-connection.ts +7 -7
  308. package/src/oauth/connect-orchestrator.ts +23 -21
  309. package/src/oauth/connect-types.ts +3 -3
  310. package/src/oauth/connection-resolver.test.ts +17 -4
  311. package/src/oauth/connection-resolver.ts +16 -16
  312. package/src/oauth/connection.ts +1 -1
  313. package/src/oauth/manual-token-connection.ts +13 -13
  314. package/src/oauth/oauth-store.ts +214 -100
  315. package/src/oauth/platform-connection.test.ts +5 -5
  316. package/src/oauth/platform-connection.ts +4 -4
  317. package/src/oauth/provider-serializer.ts +31 -5
  318. package/src/oauth/revoke.ts +76 -0
  319. package/src/oauth/seed-providers.ts +127 -87
  320. package/src/oauth/token-persistence.ts +1 -1
  321. package/src/permissions/checker.ts +3 -3
  322. package/src/permissions/defaults.ts +7 -8
  323. package/src/permissions/permission-mode.ts +4 -11
  324. package/src/permissions/prompter.ts +13 -3
  325. package/src/permissions/v2-consent-policy.ts +87 -0
  326. package/src/platform/client.ts +1 -1
  327. package/src/prompts/system-prompt.ts +18 -21
  328. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  329. package/src/prompts/templates/BOOTSTRAP.md +59 -96
  330. package/src/prompts/templates/SOUL.md +11 -11
  331. package/src/providers/anthropic/client.ts +1 -0
  332. package/src/providers/types.ts +1 -1
  333. package/src/runtime/AGENTS.md +23 -0
  334. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  335. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  336. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  337. package/src/runtime/assistant-event-hub.ts +24 -2
  338. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  339. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  340. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  341. package/src/runtime/auth/middleware.ts +98 -0
  342. package/src/runtime/auth/route-policy.ts +6 -7
  343. package/src/runtime/auth/token-service.ts +8 -0
  344. package/src/runtime/capability-tokens.ts +414 -0
  345. package/src/runtime/channel-approvals.ts +18 -5
  346. package/src/runtime/chrome-extension-registry.ts +332 -0
  347. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  348. package/src/runtime/guardian-decision-types.ts +7 -0
  349. package/src/runtime/http-server.ts +425 -70
  350. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  351. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  352. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  353. package/src/runtime/migrations/migration-transport.ts +6 -0
  354. package/src/runtime/migrations/migration-wizard.ts +22 -2
  355. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  356. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  357. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  358. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  359. package/src/runtime/pending-interactions.ts +29 -13
  360. package/src/runtime/routes/approval-routes.ts +90 -16
  361. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  362. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  363. package/src/runtime/routes/conversation-analysis-routes.ts +18 -5
  364. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  365. package/src/runtime/routes/conversation-routes.ts +308 -28
  366. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  367. package/src/runtime/routes/group-routes.ts +22 -8
  368. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  369. package/src/runtime/routes/host-browser-routes.ts +279 -0
  370. package/src/runtime/routes/host-file-routes.ts +9 -1
  371. package/src/runtime/routes/identity-routes.ts +259 -16
  372. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  373. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  374. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  375. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  376. package/src/runtime/routes/log-export-routes.ts +60 -25
  377. package/src/runtime/routes/memory-item-routes.ts +1 -7
  378. package/src/runtime/routes/migration-routes.ts +87 -2
  379. package/src/runtime/routes/oauth-apps.ts +15 -17
  380. package/src/runtime/routes/oauth-providers.ts +4 -0
  381. package/src/runtime/routes/schedule-routes.ts +24 -11
  382. package/src/runtime/routes/settings-routes.ts +9 -97
  383. package/src/runtime/routes/skills-routes.ts +52 -2
  384. package/src/runtime/routes/subagents-routes.ts +14 -10
  385. package/src/runtime/routes/usage-routes.ts +8 -7
  386. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  387. package/src/runtime/routes/workspace-routes.ts +8 -1
  388. package/src/runtime/routes/workspace-utils.ts +2 -0
  389. package/src/schedule/scheduler.ts +7 -5
  390. package/src/security/ces-credential-client.ts +20 -0
  391. package/src/security/ces-rpc-credential-backend.ts +17 -0
  392. package/src/security/credential-backend.ts +5 -0
  393. package/src/security/oauth2.ts +42 -25
  394. package/src/security/secure-keys.ts +118 -25
  395. package/src/security/token-manager.ts +23 -10
  396. package/src/skills/catalog-files.ts +492 -0
  397. package/src/skills/inline-command-runner.ts +12 -14
  398. package/src/subagent/manager.ts +131 -26
  399. package/src/subagent/types.ts +19 -0
  400. package/src/tools/apps/executors.ts +11 -2
  401. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  402. package/src/tools/browser/auth-detector.ts +43 -12
  403. package/src/tools/browser/browser-execution.ts +645 -340
  404. package/src/tools/browser/browser-manager.ts +36 -12
  405. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  406. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  407. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  408. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  409. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  410. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  411. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  412. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  413. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  414. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  415. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  416. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  417. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  418. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  419. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  420. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  421. package/src/tools/browser/cdp-client/errors.ts +34 -0
  422. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  423. package/src/tools/browser/cdp-client/factory.ts +204 -0
  424. package/src/tools/browser/cdp-client/index.ts +14 -0
  425. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  426. package/src/tools/browser/cdp-client/types.ts +52 -0
  427. package/src/tools/filesystem/edit.ts +1 -1
  428. package/src/tools/filesystem/list.ts +1 -1
  429. package/src/tools/filesystem/read.ts +1 -1
  430. package/src/tools/filesystem/write.ts +2 -1
  431. package/src/tools/host-filesystem/edit.ts +1 -1
  432. package/src/tools/host-filesystem/read.ts +12 -15
  433. package/src/tools/host-filesystem/write.ts +1 -1
  434. package/src/tools/host-terminal/host-shell.ts +21 -16
  435. package/src/tools/permission-checker.ts +77 -100
  436. package/src/tools/registry.ts +0 -2
  437. package/src/tools/secret-detection-handler.ts +34 -1
  438. package/src/tools/shared/filesystem/image-read.ts +61 -40
  439. package/src/tools/skills/sandbox-runner.ts +3 -6
  440. package/src/tools/subagent/spawn.ts +47 -3
  441. package/src/tools/subagent/status.ts +2 -0
  442. package/src/tools/system/register.ts +2 -16
  443. package/src/tools/terminal/safe-env.ts +7 -0
  444. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  445. package/src/tools/terminal/sandbox.ts +4 -1
  446. package/src/tools/terminal/shell.ts +24 -21
  447. package/src/tools/tool-approval-handler.ts +48 -2
  448. package/src/tools/types.ts +2 -3
  449. package/src/util/platform.ts +14 -19
  450. package/src/watcher/provider-types.ts +1 -1
  451. package/src/workspace/migrations/029-seed-pkb.ts +1 -0
  452. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  453. package/src/workspace/migrations/registry.ts +2 -0
  454. package/src/workspace/top-level-renderer.ts +19 -1
  455. package/src/__tests__/chrome-cdp.test.ts +0 -419
  456. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  457. package/src/__tests__/permission-mode-store.test.ts +0 -277
  458. package/src/browser-extension-relay/protocol.ts +0 -63
  459. package/src/browser-extension-relay/server.ts +0 -203
  460. package/src/config/schemas/sandbox.ts +0 -14
  461. package/src/permissions/permission-mode-store.ts +0 -180
  462. package/src/tools/browser/chrome-cdp.ts +0 -239
  463. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -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,
@@ -20,7 +23,6 @@ import {
20
23
  import { getLogger } from "../util/logger.js";
21
24
  import { buildPolicyContext } from "./policy-context.js";
22
25
  import { isSideEffectTool } from "./side-effects.js";
23
- import { wrapCommand } from "./terminal/sandbox.js";
24
26
  import type { ExecutionTarget } from "./types.js";
25
27
  import type { Tool, ToolContext, ToolLifecycleEvent } from "./types.js";
26
28
 
@@ -68,50 +70,29 @@ export class PermissionChecker {
68
70
  }
69
71
  | undefined,
70
72
  ): Promise<PermissionDecision> {
71
- // ── permission-controls-v2 early gate ──────────────────────────────
72
- // When the v2 flag is enabled, replace the entire risk-classification
73
- // path with a simple binary check: is it a host tool + is host access
74
- // enabled? Certain security gates (requireFreshApproval,
75
- // forcePromptSideEffects, hostAccess=false) fall through to the v1
76
- // prompt flow so the interactive prompter is engaged.
77
- const cfg = getConfig();
78
73
  let v2ForcePrompt = false;
79
- if (isAssistantFeatureFlagEnabled("permission-controls-v2", cfg)) {
80
- // requireFreshApproval demands an interactive prompt every time —
81
- // fall through to v1 so the prompter is engaged.
82
- const needsFreshApproval = !!context.requireFreshApproval;
83
-
84
- // forcePromptSideEffects (private conversations, untrusted actors)
85
- // requires explicit approval for side-effect tools.
86
- const needsSideEffectPrompt =
87
- !!context.forcePromptSideEffects && isSideEffectTool(name, input);
88
-
89
- if (!needsFreshApproval && !needsSideEffectPrompt) {
90
- if (isHostTool(name)) {
91
- const mode = getMode();
92
- if (mode.hostAccess) {
93
- return {
94
- allowed: true,
95
- decision: "allow",
96
- riskLevel: RiskLevel.Low,
97
- };
98
- }
99
- // Host tool with hostAccess disabled — fall through to v1 so the
100
- // interactive prompter is engaged (returning allowed:false here
101
- // would surface an error string instead of a permission dialog).
102
- // The v2ForcePrompt flag ensures check()'s allow decision is
103
- // promoted to prompt so the user sees a permission dialog.
104
- v2ForcePrompt = true;
105
- } else {
106
- // Non-host tools are auto-allowed when v2 is on
107
- return {
108
- allowed: true,
109
- decision: "allow",
110
- riskLevel: RiskLevel.Low,
111
- };
112
- }
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;
113
95
  }
114
- // Falls through to the v1 risk-classification + prompter path
115
96
  }
116
97
 
117
98
  const risk = await classifyRisk(
@@ -302,40 +283,34 @@ export class PermissionChecker {
302
283
  return { allowed: true, decision: "temporary_override", riskLevel };
303
284
  }
304
285
 
305
- const allowlistOptions = await generateAllowlistOptions(
306
- name,
307
- input,
308
- context.signal,
309
- );
310
- const scopeOptions = generateScopeOptions(context.workingDir, name);
311
286
  const previewDiff = computePreviewDiff(name, input, context.workingDir);
312
-
313
- let sandboxed: boolean | undefined;
314
- if (name === "bash" && typeof input.command === "string") {
315
- const cfg = getConfig();
316
- const sandboxConfig =
317
- context.sandboxOverride != null
318
- ? { ...cfg.sandbox, enabled: context.sandboxOverride }
319
- : cfg.sandbox;
320
- const wrapped = wrapCommand(
321
- input.command,
322
- context.workingDir,
323
- sandboxConfig,
324
- );
325
- sandboxed = wrapped.sandboxed;
326
- }
327
-
328
- const persistentDecisionsAllowed = !context.requireFreshApproval;
329
-
330
- // Offer temporary approval options to guardians. Suppressed when
331
- // requireFreshApproval is true - temporary overrides would be
332
- // misleading since future invocations still require fresh approval.
333
- const temporaryOptionsAvailable:
334
- | Array<"allow_10m" | "allow_conversation">
335
- | undefined =
336
- context.trustClass === "guardian" && !context.requireFreshApproval
337
- ? ["allow_10m", "allow_conversation"]
338
- : 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
+ };
339
314
 
340
315
  emitLifecycleEvent({
341
316
  type: "permission_prompt",
@@ -347,11 +322,10 @@ export class PermissionChecker {
347
322
  requestId: context.requestId,
348
323
  riskLevel,
349
324
  reason: result.reason,
350
- allowlistOptions,
351
- scopeOptions,
325
+ allowlistOptions: promptOptions.allowlistOptions,
326
+ scopeOptions: promptOptions.scopeOptions,
352
327
  diff: previewDiff,
353
- sandboxed,
354
- persistentDecisionsAllowed,
328
+ persistentDecisionsAllowed: promptOptions.persistentDecisionsAllowed,
355
329
  });
356
330
 
357
331
  await getHookManager().trigger("permission-request", {
@@ -365,28 +339,31 @@ export class PermissionChecker {
365
339
  name,
366
340
  input,
367
341
  riskLevel,
368
- allowlistOptions,
369
- scopeOptions,
342
+ promptOptions.allowlistOptions,
343
+ promptOptions.scopeOptions,
370
344
  previewDiff,
371
- sandboxed,
372
345
  context.conversationId,
373
346
  executionTarget,
374
- persistentDecisionsAllowed,
347
+ promptOptions.persistentDecisionsAllowed,
375
348
  context.signal,
376
- temporaryOptionsAvailable,
349
+ promptOptions.temporaryOptionsAvailable,
377
350
  context.toolUseId,
351
+ v2ForcePrompt,
378
352
  );
379
353
 
380
- const decision = response.decision;
354
+ const decision =
355
+ v2ForcePrompt && !isConversationHostAccessDecision(response.decision)
356
+ ? "deny"
357
+ : response.decision;
381
358
 
382
359
  await getHookManager().trigger("permission-resolve", {
383
360
  toolName: name,
384
- decision: response.decision,
361
+ decision,
385
362
  riskLevel,
386
363
  conversationId: context.conversationId,
387
364
  });
388
365
 
389
- if (response.decision === "deny") {
366
+ if (decision === "deny") {
390
367
  const contextualDenial =
391
368
  typeof response.decisionContext === "string"
392
369
  ? response.decisionContext.trim()
@@ -421,15 +398,15 @@ export class PermissionChecker {
421
398
  };
422
399
  }
423
400
 
424
- if (response.decision === "always_deny") {
401
+ if (decision === "always_deny") {
425
402
  // For non-scoped tools (empty scopeOptions), default to 'everywhere' since
426
403
  // the client has no scope picker and will send undefined.
427
404
  const effectiveDenyScope =
428
- scopeOptions.length === 0
405
+ promptOptions.scopeOptions.length === 0
429
406
  ? (response.selectedScope ?? "everywhere")
430
407
  : response.selectedScope;
431
408
  const ruleSaved = !!(
432
- persistentDecisionsAllowed &&
409
+ promptOptions.persistentDecisionsAllowed &&
433
410
  response.selectedPattern &&
434
411
  effectiveDenyScope
435
412
  );
@@ -470,9 +447,9 @@ export class PermissionChecker {
470
447
  }
471
448
 
472
449
  if (
473
- persistentDecisionsAllowed &&
474
- (response.decision === "always_allow" ||
475
- response.decision === "always_allow_high_risk") &&
450
+ promptOptions.persistentDecisionsAllowed &&
451
+ (decision === "always_allow" ||
452
+ decision === "always_allow_high_risk") &&
476
453
  response.selectedPattern
477
454
  ) {
478
455
  const ruleOptions: {
@@ -480,7 +457,7 @@ export class PermissionChecker {
480
457
  executionTarget?: string;
481
458
  } = {};
482
459
 
483
- if (response.decision === "always_allow_high_risk") {
460
+ if (decision === "always_allow_high_risk") {
484
461
  ruleOptions.allowHighRisk = true;
485
462
  }
486
463
 
@@ -492,7 +469,7 @@ export class PermissionChecker {
492
469
  // Only default to 'everywhere' for non-scoped tools (empty scopeOptions).
493
470
  // For scoped tools, require an explicit scope to prevent silent permission widening.
494
471
  const effectiveScope =
495
- scopeOptions.length === 0
472
+ promptOptions.scopeOptions.length === 0
496
473
  ? (response.selectedScope ?? "everywhere")
497
474
  : response.selectedScope;
498
475
  if (effectiveScope) {
@@ -511,13 +488,13 @@ export class PermissionChecker {
511
488
  // time-limited or conversation-scoped override. Subsequent tool
512
489
  // invocations in this conversation will auto-approve without
513
490
  // prompting (checked above in the temporary override block).
514
- if (response.decision === "allow_10m") {
491
+ if (decision === "allow_10m") {
515
492
  setTimedMode(context.conversationId);
516
493
  log.info(
517
494
  { toolName: name, conversationId: context.conversationId },
518
495
  "Activated timed (10m) temporary approval mode",
519
496
  );
520
- } else if (response.decision === "allow_conversation") {
497
+ } else if (decision === "allow_conversation") {
521
498
  setConversationMode(context.conversationId);
522
499
  log.info(
523
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.`;
@@ -331,7 +365,6 @@ export class SecretDetectionHandler {
331
365
  [], // no allowlist options
332
366
  [], // no scope options
333
367
  undefined, // no diff
334
- undefined, // not sandboxed
335
368
  context.conversationId,
336
369
  executionTarget,
337
370
  false, // no persistent decisions
@@ -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
+ }
@@ -3,7 +3,6 @@ import { randomUUID } from "node:crypto";
3
3
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
 
6
- import { getConfig } from "../../config/loader.js";
7
6
  import { computeSkillVersionHash } from "../../skills/version-hash.js";
8
7
  import { buildSanitizedEnv } from "../terminal/safe-env.js";
9
8
  import { wrapCommand } from "../terminal/sandbox.js";
@@ -138,11 +137,9 @@ function spawnRunner(
138
137
  const stderrChunks: Buffer[] = [];
139
138
  let timedOut = false;
140
139
 
141
- const config = getConfig();
142
- const sandboxConfig =
143
- context.sandboxOverride != null
144
- ? { ...config.sandbox, enabled: context.sandboxOverride }
145
- : config.sandbox;
140
+ // The assistant runs exclusively in Docker or platform-managed
141
+ // environments where the container provides isolation.
142
+ const sandboxConfig = { enabled: false } as const;
146
143
 
147
144
  const bunRunCmd = "bun run __skill_runner.ts";
148
145
  const wrapped = wrapCommand(bunRunCmd, runDir, sandboxConfig);
@@ -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
  };