@vellumai/assistant 0.6.6 → 0.7.0

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 (1346) hide show
  1. package/AGENTS.md +20 -0
  2. package/ARCHITECTURE.md +45 -36
  3. package/Dockerfile +26 -6
  4. package/README.md +8 -10
  5. package/__tests__/permissions/gateway-threshold-reader.test.ts +14 -20
  6. package/bun.lock +306 -119
  7. package/docs/architecture/memory.md +1 -90
  8. package/docs/architecture/security.md +15 -30
  9. package/docs/credential-execution-service.md +7 -5
  10. package/docs/skills.md +10 -10
  11. package/docs/stt-provider-onboarding.md +17 -45
  12. package/examples/plugins/echo/bun.lock +25 -0
  13. package/knip.json +8 -22
  14. package/node_modules/@vellumai/ces-client/bun.lock +33 -0
  15. package/node_modules/@vellumai/ces-client/package.json +25 -0
  16. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +631 -0
  17. package/node_modules/@vellumai/ces-client/src/__tests__/package-boundary.test.ts +138 -0
  18. package/node_modules/@vellumai/ces-client/src/credential-rpc.ts +13 -0
  19. package/node_modules/@vellumai/ces-client/src/http-credentials.ts +296 -0
  20. package/node_modules/@vellumai/ces-client/src/http-log-export.ts +111 -0
  21. package/node_modules/@vellumai/ces-client/src/index.ts +43 -0
  22. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +445 -0
  23. package/node_modules/@vellumai/credential-storage/src/__tests__/package-boundary.test.ts +32 -6
  24. package/node_modules/@vellumai/egress-proxy/src/__tests__/package-boundary.test.ts +32 -1
  25. package/node_modules/@vellumai/gateway-client/bun.lock +39 -0
  26. package/node_modules/@vellumai/gateway-client/package.json +23 -0
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +343 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/package-boundary.test.ts +140 -0
  29. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +422 -0
  30. package/node_modules/@vellumai/gateway-client/src/index.ts +35 -0
  31. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +331 -0
  32. package/node_modules/@vellumai/gateway-client/src/types.ts +131 -0
  33. package/node_modules/@vellumai/gateway-client/tsconfig.json +20 -0
  34. package/node_modules/@vellumai/{ces-contracts → service-contracts}/bun.lock +1 -1
  35. package/node_modules/@vellumai/{ces-contracts → service-contracts}/package.json +4 -2
  36. package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/__tests__/contracts.test.ts +5 -1
  37. package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +155 -0
  38. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +23 -0
  39. package/node_modules/@vellumai/service-contracts/src/index.ts +25 -0
  40. package/node_modules/@vellumai/{ces-contracts/src/index.ts → service-contracts/src/transport.ts} +6 -28
  41. package/node_modules/@vellumai/service-contracts/src/trust-rules.ts +116 -0
  42. package/node_modules/@vellumai/service-contracts/tsconfig.json +20 -0
  43. package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +891 -0
  44. package/node_modules/@vellumai/skill-host-contracts/bun.lock +24 -0
  45. package/node_modules/@vellumai/skill-host-contracts/package.json +18 -0
  46. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +91 -0
  47. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +1348 -0
  48. package/node_modules/@vellumai/skill-host-contracts/src/index.ts +6 -0
  49. package/node_modules/@vellumai/skill-host-contracts/src/runtime-mode.ts +11 -0
  50. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +32 -0
  51. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +333 -0
  52. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +444 -0
  53. package/node_modules/@vellumai/skill-host-contracts/tsconfig.json +20 -0
  54. package/node_modules/@vellumai/skill-host-contracts/tsconfig.test.json +12 -0
  55. package/openapi.yaml +2855 -556
  56. package/package.json +13 -7
  57. package/scripts/check-circular-deps.ts +80 -0
  58. package/scripts/generate-openapi.ts +24 -7
  59. package/{src/memory/graph/inspect.ts → scripts/memory-inspect.ts} +27 -27
  60. package/src/__tests__/access-request-decision.test.ts +2 -11
  61. package/src/__tests__/acp-session.test.ts +4 -150
  62. package/src/__tests__/actor-token-service.test.ts +17 -678
  63. package/src/__tests__/agent-loop-callsite-precedence.test.ts +2 -6
  64. package/src/__tests__/agent-loop-override-profile.test.ts +404 -0
  65. package/src/__tests__/agent-loop-thinking.test.ts +4 -4
  66. package/src/__tests__/agent-wake-override-profile.test.ts +261 -0
  67. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -1
  68. package/src/__tests__/anthropic-provider.test.ts +127 -15
  69. package/src/__tests__/app-routes-csp.test.ts +106 -55
  70. package/src/__tests__/approval-cascade.test.ts +3 -355
  71. package/src/__tests__/approval-conversation-turn.test.ts +3 -8
  72. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +1 -1
  73. package/src/__tests__/approval-primitive.test.ts +2 -1
  74. package/src/__tests__/approval-routes-http.test.ts +34 -451
  75. package/src/__tests__/assistant-events-sse-hardening.test.ts +73 -80
  76. package/src/__tests__/assistant-id-boundary-guard.test.ts +0 -3
  77. package/src/__tests__/attachment-upload-trusted-source.test.ts +139 -0
  78. package/src/__tests__/attachments-store.test.ts +46 -1
  79. package/src/__tests__/audit-log-rotation.test.ts +2 -1
  80. package/src/__tests__/auto-analysis-end-to-end.test.ts +8 -20
  81. package/src/__tests__/background-shell-bash.test.ts +227 -0
  82. package/src/__tests__/background-shell-host-bash.test.ts +474 -0
  83. package/src/__tests__/background-tool-registry.test.ts +145 -0
  84. package/src/__tests__/background-tool-routes.test.ts +175 -0
  85. package/src/__tests__/btw-routes.test.ts +147 -183
  86. package/src/__tests__/call-controller.test.ts +15 -2
  87. package/src/__tests__/call-conversation-messages.test.ts +2 -1
  88. package/src/__tests__/call-domain.test.ts +2 -2
  89. package/src/__tests__/call-pointer-messages.test.ts +11 -13
  90. package/src/__tests__/call-recovery.test.ts +2 -1
  91. package/src/__tests__/call-routes-http.test.ts +3 -14
  92. package/src/__tests__/call-store.test.ts +2 -1
  93. package/src/__tests__/cancel-resolves-conversation-key.test.ts +31 -62
  94. package/src/__tests__/canonical-guardian-store.test.ts +2 -2
  95. package/src/__tests__/catalog-files.test.ts +0 -26
  96. package/src/__tests__/ces-rpc-credential-backend.test.ts +1 -1
  97. package/src/__tests__/channel-approval-routes.test.ts +79 -49
  98. package/src/__tests__/channel-approval.test.ts +9 -7
  99. package/src/__tests__/channel-approvals.test.ts +9 -180
  100. package/src/__tests__/channel-delivery-store.test.ts +11 -10
  101. package/src/__tests__/channel-guardian.test.ts +14 -25
  102. package/src/__tests__/channel-readiness-service.test.ts +8 -6
  103. package/src/__tests__/channel-reply-delivery.test.ts +3 -19
  104. package/src/__tests__/channel-retry-sweep.test.ts +2 -5
  105. package/src/__tests__/checker.test.ts +274 -3921
  106. package/src/__tests__/circuit-breaker-pipeline.test.ts +1 -1
  107. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +208 -0
  108. package/src/__tests__/cli.test.ts +1 -38
  109. package/src/__tests__/compaction-events.test.ts +0 -1
  110. package/src/__tests__/compaction-pipeline.test.ts +1 -1
  111. package/src/__tests__/compaction-strip-metadata-clear.test.ts +2 -2
  112. package/src/__tests__/compaction-timeout-recovery.test.ts +1 -1
  113. package/src/__tests__/config-managed-gemini-defaults.test.ts +3 -7
  114. package/src/__tests__/config-model-image-provider.test.ts +0 -1
  115. package/src/__tests__/config-schema-cmd.test.ts +1 -1
  116. package/src/__tests__/config-schema.test.ts +30 -221
  117. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -25
  118. package/src/__tests__/contact-store-user-file.test.ts +2 -1
  119. package/src/__tests__/contacts-tools.test.ts +56 -29
  120. package/src/__tests__/contacts-write.test.ts +6 -61
  121. package/src/__tests__/context-search-agent-protocol.test.ts +230 -0
  122. package/src/__tests__/context-search-agent-runner.test.ts +998 -0
  123. package/src/__tests__/context-search-conversations-source.test.ts +320 -0
  124. package/src/__tests__/context-search-fanout.test.ts +380 -0
  125. package/src/__tests__/context-search-memory-source.test.ts +311 -0
  126. package/src/__tests__/context-search-pkb-source.test.ts +444 -0
  127. package/src/__tests__/context-search-types.test.ts +95 -0
  128. package/src/__tests__/context-search-workspace-source.test.ts +545 -0
  129. package/src/__tests__/context-window-manager.test.ts +25 -0
  130. package/src/__tests__/conversation-abort-tool-results.test.ts +10 -1
  131. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +631 -0
  132. package/src/__tests__/conversation-agent-loop-overflow.test.ts +15 -2
  133. package/src/__tests__/conversation-agent-loop.test.ts +24 -2
  134. package/src/__tests__/conversation-analysis-routes.test.ts +60 -82
  135. package/src/__tests__/conversation-attachments.test.ts +9 -20
  136. package/src/__tests__/conversation-attention-store.test.ts +2 -1
  137. package/src/__tests__/conversation-attention-telegram.test.ts +4 -2
  138. package/src/__tests__/conversation-clear-safety.test.ts +53 -95
  139. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -39
  140. package/src/__tests__/conversation-crud-inference-profile.test.ts +54 -0
  141. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +63 -157
  142. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  143. package/src/__tests__/conversation-disk-view.test.ts +5 -4
  144. package/src/__tests__/conversation-fork-crud.test.ts +26 -55
  145. package/src/__tests__/conversation-fork-route.test.ts +5 -74
  146. package/src/__tests__/conversation-inference-profile-list.test.ts +128 -0
  147. package/src/__tests__/conversation-inference-profile-route.test.ts +216 -0
  148. package/src/__tests__/conversation-init.benchmark.test.ts +4 -81
  149. package/src/__tests__/conversation-key-store-disk-view.test.ts +2 -1
  150. package/src/__tests__/conversation-lifecycle.test.ts +0 -1
  151. package/src/__tests__/conversation-list-source.test.ts +2 -2
  152. package/src/__tests__/conversation-load-history-repair.test.ts +0 -1
  153. package/src/__tests__/conversation-pairing.test.ts +0 -1
  154. package/src/__tests__/conversation-pre-run-repair.test.ts +137 -297
  155. package/src/__tests__/conversation-process-callsite.test.ts +0 -1
  156. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -1
  157. package/src/__tests__/conversation-queue.test.ts +1 -33
  158. package/src/__tests__/conversation-routes-disk-view.test.ts +124 -97
  159. package/src/__tests__/conversation-routes-guardian-reply.test.ts +80 -55
  160. package/src/__tests__/conversation-routes-slash-commands.test.ts +83 -12
  161. package/src/__tests__/conversation-runtime-assembly.test.ts +41 -84
  162. package/src/__tests__/conversation-slash-commands.test.ts +6 -43
  163. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  164. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  165. package/src/__tests__/conversation-speed-override.test.ts +0 -1
  166. package/src/__tests__/conversation-starter-routes.test.ts +177 -55
  167. package/src/__tests__/conversation-starters-cadence.test.ts +2 -2
  168. package/src/__tests__/conversation-store.test.ts +2 -375
  169. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
  170. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +6 -6
  171. package/src/__tests__/conversation-unread-route.test.ts +1 -1
  172. package/src/__tests__/conversation-usage.test.ts +2 -1
  173. package/src/__tests__/conversation-wipe.test.ts +2 -103
  174. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -1
  175. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  176. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  177. package/src/__tests__/conversations-defer-cli.test.ts +150 -0
  178. package/src/__tests__/credential-execution-admin-cli.test.ts +1 -1
  179. package/src/__tests__/credential-execution-api-key-propagation.test.ts +2 -2
  180. package/src/__tests__/credential-execution-approval-bridge.test.ts +22 -289
  181. package/src/__tests__/credential-execution-client.test.ts +1 -1
  182. package/src/__tests__/credential-execution-managed-contract.test.ts +1 -1
  183. package/src/__tests__/credential-security-invariants.test.ts +14 -0
  184. package/src/__tests__/credentials-cli.test.ts +45 -21
  185. package/src/__tests__/daemon-credential-client.test.ts +23 -108
  186. package/src/__tests__/db-acp-history.test.ts +284 -0
  187. package/src/__tests__/db-activation-state.test.ts +240 -0
  188. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +2 -1
  189. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +248 -0
  190. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +2 -1
  191. package/src/__tests__/db-memory-graph-event-date-repair.test.ts +116 -0
  192. package/src/__tests__/db-rename-inference-profile-snake-case-migration.test.ts +132 -0
  193. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  194. package/src/__tests__/delete-propagation.test.ts +3 -2
  195. package/src/__tests__/deterministic-verification-control-plane.test.ts +39 -32
  196. package/src/__tests__/dm-backfill.test.ts +3 -2
  197. package/src/__tests__/edit-propagation.test.ts +5 -7
  198. package/src/__tests__/embedding-managed-proxy-selection.test.ts +1 -1
  199. package/src/__tests__/empty-response-pipeline.test.ts +1 -1
  200. package/src/__tests__/events-client-registration.test.ts +297 -0
  201. package/src/__tests__/file-write-tool.test.ts +2 -4
  202. package/src/__tests__/filing-service.test.ts +144 -17
  203. package/src/__tests__/followup-tools.test.ts +2 -1
  204. package/src/__tests__/gateway-client-managed-outbound.test.ts +8 -12
  205. package/src/__tests__/gateway-only-enforcement.test.ts +2 -6
  206. package/src/__tests__/gateway-only-guard.test.ts +4 -3
  207. package/src/__tests__/gemini-provider.test.ts +276 -10
  208. package/src/__tests__/graph-extraction-event-date.test.ts +30 -0
  209. package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -1
  210. package/src/__tests__/guardian-action-followup-executor.test.ts +2 -2
  211. package/src/__tests__/guardian-action-followup-store.test.ts +2 -1
  212. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +9 -9
  213. package/src/__tests__/guardian-action-late-reply.test.ts +2 -1
  214. package/src/__tests__/guardian-action-store.test.ts +2 -1
  215. package/src/__tests__/guardian-action-sweep.test.ts +9 -8
  216. package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -1
  217. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +21 -118
  218. package/src/__tests__/guardian-dispatch.test.ts +14 -11
  219. package/src/__tests__/guardian-grant-minting.test.ts +9 -15
  220. package/src/__tests__/guardian-outbound-http.test.ts +71 -106
  221. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -2
  222. package/src/__tests__/guardian-routing-invariants.test.ts +34 -90
  223. package/src/__tests__/guardian-routing-state.test.ts +14 -22
  224. package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -2
  225. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +253 -0
  226. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +8 -4
  227. package/src/__tests__/heartbeat-service.test.ts +39 -21
  228. package/src/__tests__/helpers/call-route-handler.ts +72 -0
  229. package/src/__tests__/helpers/channel-test-adapter.ts +161 -0
  230. package/src/__tests__/helpers/gateway-classify-mock.ts +67 -0
  231. package/src/__tests__/helpers/mock-logger.ts +36 -0
  232. package/src/__tests__/history-repair-pipeline.test.ts +1 -1
  233. package/src/__tests__/home-state-routes.test.ts +10 -31
  234. package/src/__tests__/host-browser-e2e-cloud.test.ts +2 -1
  235. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +12 -2
  236. package/src/__tests__/host-browser-routes.test.ts +36 -91
  237. package/src/__tests__/host-browser-ws-events-e2e.test.ts +10 -2
  238. package/src/__tests__/host-proxy-interface.test.ts +3 -3
  239. package/src/__tests__/host-shell-tool.test.ts +2 -4
  240. package/src/__tests__/host-transfer-pending-interactions.test.ts +160 -0
  241. package/src/__tests__/host-transfer-proxy.test.ts +733 -0
  242. package/src/__tests__/http-conversation-lineage.test.ts +3 -2
  243. package/src/__tests__/http-user-message-parity.test.ts +20 -11
  244. package/src/__tests__/inbound-invite-redemption.test.ts +3 -2
  245. package/src/__tests__/injector-chain.test.ts +16 -17
  246. package/src/__tests__/inline-skill-load-permissions.test.ts +41 -206
  247. package/src/__tests__/install-skill-routing.test.ts +1 -1
  248. package/src/__tests__/invite-redemption-service.test.ts +2 -1
  249. package/src/__tests__/invite-routes-http.test.ts +80 -12
  250. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +2 -1
  251. package/src/__tests__/jobs-store-upsert-debounced.test.ts +2 -1
  252. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +157 -0
  253. package/src/__tests__/list-messages-attachments.test.ts +52 -55
  254. package/src/__tests__/list-messages-page-latest.test.ts +283 -0
  255. package/src/__tests__/list-messages-tool-merge.test.ts +16 -17
  256. package/src/__tests__/llm-call-pipeline.test.ts +7 -8
  257. package/src/__tests__/llm-context-normalization.test.ts +69 -4
  258. package/src/__tests__/llm-context-route-provider.test.ts +39 -113
  259. package/src/__tests__/llm-request-log-turn-query.test.ts +2 -1
  260. package/src/__tests__/llm-resolver.test.ts +211 -0
  261. package/src/__tests__/llm-schema.test.ts +57 -1
  262. package/src/__tests__/llm-usage-store.test.ts +2 -1
  263. package/src/__tests__/log-export-workspace.test.ts +28 -17
  264. package/src/__tests__/mcp-abort-signal.test.ts +2 -3
  265. package/src/__tests__/mcp-client-auth.test.ts +2 -3
  266. package/src/__tests__/memory-admin-recall.test.ts +221 -0
  267. package/src/__tests__/memory-recall-log-store.test.ts +2 -1
  268. package/src/__tests__/memory-retrieval-pipeline.test.ts +6 -8
  269. package/src/__tests__/memory-upsert-concurrency.test.ts +2 -1
  270. package/src/__tests__/migration-cross-version-compatibility.test.ts +14 -13
  271. package/src/__tests__/migration-export-http.test.ts +17 -17
  272. package/src/__tests__/migration-export-to-gcs.test.ts +491 -0
  273. package/src/__tests__/migration-import-commit-http.test.ts +16 -16
  274. package/src/__tests__/migration-import-from-gcs.test.ts +533 -0
  275. package/src/__tests__/migration-import-from-url.test.ts +16 -23
  276. package/src/__tests__/migration-import-preflight-http.test.ts +13 -13
  277. package/src/__tests__/migration-jobs-status.test.ts +164 -0
  278. package/src/__tests__/migration-validate-http.test.ts +48 -83
  279. package/src/__tests__/mock-gateway-ipc.ts +32 -62
  280. package/src/__tests__/model-intents.test.ts +15 -2
  281. package/src/__tests__/nl-approval-parser.test.ts +13 -17
  282. package/src/__tests__/non-member-access-request.test.ts +13 -5
  283. package/src/__tests__/notification-guardian-path.test.ts +15 -8
  284. package/src/__tests__/notification-schedule-notify-dedup.test.ts +2 -1
  285. package/src/__tests__/notification-telegram-adapter.test.ts +57 -55
  286. package/src/__tests__/oauth-apps-routes.test.ts +76 -122
  287. package/src/__tests__/oauth-cli.test.ts +14 -1
  288. package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
  289. package/src/__tests__/oauth-provider-visibility.test.ts +3 -1
  290. package/src/__tests__/oauth-providers-routes.test.ts +78 -101
  291. package/src/__tests__/oauth-store.test.ts +3 -1
  292. package/src/__tests__/oauth2-gateway-transport.test.ts +6 -3
  293. package/src/__tests__/openai-provider.test.ts +105 -6
  294. package/src/__tests__/openai-responses-provider.test.ts +146 -4
  295. package/src/__tests__/openrouter-provider-only.test.ts +22 -4
  296. package/src/__tests__/overflow-reduce-pipeline.test.ts +4 -9
  297. package/src/__tests__/permission-types.test.ts +3 -18
  298. package/src/__tests__/persistence-pipeline.test.ts +3 -2
  299. package/src/__tests__/pipeline-runner.test.ts +1 -1
  300. package/src/__tests__/platform-bash-auto-approve.test.ts +27 -20
  301. package/src/__tests__/platform.test.ts +11 -63
  302. package/src/__tests__/playbook-execution.test.ts +2 -1
  303. package/src/__tests__/playbook-tools.test.ts +2 -1
  304. package/src/__tests__/plugin-bootstrap.test.ts +51 -5
  305. package/src/__tests__/plugin-registry.test.ts +30 -0
  306. package/src/__tests__/plugin-route-contribution.test.ts +17 -11
  307. package/src/__tests__/plugin-skill-contribution.test.ts +3 -3
  308. package/src/__tests__/plugin-tool-contribution.test.ts +10 -4
  309. package/src/__tests__/plugin-types.test.ts +1 -1
  310. package/src/__tests__/pricing.test.ts +151 -2
  311. package/src/__tests__/profiler-routes.test.ts +112 -177
  312. package/src/__tests__/provider-send-message-override-profile.test.ts +223 -0
  313. package/src/__tests__/proxy-approval-callback.test.ts +6 -554
  314. package/src/__tests__/qdrant-collection-migration.test.ts +7 -7
  315. package/src/__tests__/reaction-persistence.test.ts +3 -2
  316. package/src/__tests__/rebuild-index-graph-nodes.test.ts +1 -1
  317. package/src/__tests__/recording-handler.test.ts +0 -2
  318. package/src/__tests__/registry.test.ts +1 -0
  319. package/src/__tests__/relay-server.test.ts +19 -4
  320. package/src/__tests__/require-fresh-approval.test.ts +19 -168
  321. package/src/__tests__/resolve-trust-class.test.ts +2 -1
  322. package/src/__tests__/retry-thinking-tool-choice.test.ts +19 -7
  323. package/src/__tests__/retry-verbosity-normalization.test.ts +139 -0
  324. package/src/__tests__/runtime-attachment-metadata.test.ts +26 -6
  325. package/src/__tests__/runtime-events-sse-parity.test.ts +12 -13
  326. package/src/__tests__/runtime-events-sse.test.ts +13 -21
  327. package/src/__tests__/schedule-routes.test.ts +226 -129
  328. package/src/__tests__/schedule-store.test.ts +119 -1
  329. package/src/__tests__/schedule-tools.test.ts +2 -1
  330. package/src/__tests__/scheduler-recurrence.test.ts +2 -1
  331. package/src/__tests__/scheduler-reuse-conversation.test.ts +2 -1
  332. package/src/__tests__/scheduler-wake.test.ts +356 -0
  333. package/src/__tests__/scoped-approval-grants.test.ts +2 -1
  334. package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -1
  335. package/src/__tests__/secret-detection-handler.test.ts +2 -9
  336. package/src/__tests__/secret-ingress-http.test.ts +38 -21
  337. package/src/__tests__/secret-routes-managed-proxy.test.ts +46 -102
  338. package/src/__tests__/secret-scanner-executor.test.ts +1 -2
  339. package/src/__tests__/send-endpoint-busy.test.ts +38 -25
  340. package/src/__tests__/sequence-store.test.ts +2 -1
  341. package/src/__tests__/server-history-render.test.ts +2 -2
  342. package/src/__tests__/service-contracts-import-guard.test.ts +185 -0
  343. package/src/__tests__/set-permission-mode.test.ts +0 -10
  344. package/src/__tests__/settings-routes.test.ts +35 -68
  345. package/src/__tests__/skill-boundary-guard.test.ts +105 -0
  346. package/src/__tests__/skill-load-inline-command.test.ts +2 -2
  347. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  348. package/src/__tests__/skill-runtime-path.test.ts +64 -0
  349. package/src/__tests__/skills-file-content-endpoint.test.ts +0 -2
  350. package/src/__tests__/slack-inbound-verification.test.ts +11 -2
  351. package/src/__tests__/slack-messaging-token-resolution.test.ts +1 -3
  352. package/src/__tests__/slack-reaction-approvals.test.ts +4 -4
  353. package/src/__tests__/slack-share-routes.test.ts +37 -72
  354. package/src/__tests__/subagent-call-site-routing.test.ts +79 -0
  355. package/src/__tests__/subagent-fork-spawn.test.ts +20 -28
  356. package/src/__tests__/subagent-notify-parent.test.ts +6 -29
  357. package/src/__tests__/subagent-role-registry.test.ts +3 -3
  358. package/src/__tests__/subagent-spawn-tool-fork.test.ts +52 -104
  359. package/src/__tests__/subagent-tools.test.ts +0 -1
  360. package/src/__tests__/suggestion-routes.test.ts +55 -62
  361. package/src/__tests__/task-compiler.test.ts +2 -1
  362. package/src/__tests__/task-management-tools.test.ts +2 -1
  363. package/src/__tests__/task-memory-cleanup.test.ts +2 -1
  364. package/src/__tests__/task-scheduler.test.ts +2 -1
  365. package/src/__tests__/telegram-config.test.ts +0 -1
  366. package/src/__tests__/terminal-tools.test.ts +5 -314
  367. package/src/__tests__/test-preload.ts +0 -11
  368. package/src/__tests__/thread-backfill.test.ts +3 -2
  369. package/src/__tests__/token-estimate-pipeline.test.ts +68 -15
  370. package/src/__tests__/tool-approval-handler.test.ts +21 -63
  371. package/src/__tests__/tool-audit-listener.test.ts +3 -3
  372. package/src/__tests__/tool-domain-event-publisher.test.ts +3 -3
  373. package/src/__tests__/tool-error-pipeline.test.ts +6 -6
  374. package/src/__tests__/tool-execute-pipeline.test.ts +6 -8
  375. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +64 -1
  376. package/src/__tests__/tool-executor-lifecycle-events.test.ts +28 -56
  377. package/src/__tests__/tool-executor.test.ts +322 -1633
  378. package/src/__tests__/tool-grant-request-escalation.test.ts +90 -311
  379. package/src/__tests__/tool-result-truncate-pipeline.test.ts +1 -1
  380. package/src/__tests__/trust-context-guards.test.ts +1 -1
  381. package/src/__tests__/trusted-contact-approval-notifier.test.ts +7 -15
  382. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +178 -354
  383. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +3 -2
  384. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -2
  385. package/src/__tests__/trusted-contact-verification.test.ts +2 -1
  386. package/src/__tests__/turn-boundary-resolution.test.ts +2 -1
  387. package/src/__tests__/twilio-routes.test.ts +25 -66
  388. package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -7
  389. package/src/__tests__/usage-routes.test.ts +73 -90
  390. package/src/__tests__/user-plugin-loader.test.ts +54 -12
  391. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +2 -2
  392. package/src/__tests__/verification-control-plane-policy.test.ts +95 -14
  393. package/src/__tests__/voice-ingress-preflight.test.ts +5 -5
  394. package/src/__tests__/voice-invite-redemption.test.ts +2 -1
  395. package/src/__tests__/voice-scoped-grant-consumer.test.ts +3 -3
  396. package/src/__tests__/voice-session-bridge.test.ts +285 -106
  397. package/src/__tests__/volume-security-guard.test.ts +0 -2
  398. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -1
  399. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +3 -1
  400. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +2 -1
  401. package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +1 -1
  402. package/src/__tests__/workspace-migration-052-seed-default-inference-profiles.test.ts +260 -0
  403. package/src/__tests__/workspace-migration-053-release-notes-acp-codex.test.ts +225 -0
  404. package/src/__tests__/workspace-migration-054-seed-recall-callsite.test.ts +235 -0
  405. package/src/__tests__/workspace-migration-055-release-notes-agentic-recall.test.ts +128 -0
  406. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +232 -0
  407. package/src/__tests__/workspace-migration-acp-sessions-ui.test.ts +144 -0
  408. package/src/__tests__/workspace-migration-drop-user-md.test.ts +1 -1
  409. package/src/__tests__/workspace-migration-memory-v2-init.test.ts +274 -0
  410. package/src/acp/__tests__/client-handler.test.ts +64 -0
  411. package/src/acp/__tests__/helpers/acp-config-stub.ts +62 -0
  412. package/src/acp/__tests__/helpers/which-stub.ts +45 -0
  413. package/src/acp/__tests__/session-manager-persistence.test.ts +366 -0
  414. package/src/acp/__tests__/session-manager-startup.test.ts +159 -0
  415. package/src/acp/__tests__/session-manager.test.ts +83 -0
  416. package/src/acp/client-handler.ts +23 -139
  417. package/src/acp/resolve-agent.test.ts +291 -0
  418. package/src/acp/resolve-agent.ts +176 -0
  419. package/src/acp/session-manager.ts +166 -7
  420. package/src/acp/types.ts +2 -50
  421. package/src/agent/loop.ts +37 -14
  422. package/src/agent/message-types.ts +0 -2
  423. package/src/approvals/AGENTS.md +1 -1
  424. package/src/approvals/__tests__/guardian-feed-event.test.ts +1 -9
  425. package/src/approvals/approval-primitive.ts +3 -20
  426. package/src/approvals/guardian-decision-primitive.ts +37 -68
  427. package/src/approvals/guardian-request-resolvers.ts +29 -103
  428. package/src/avatar/character-components.ts +6 -6
  429. package/src/{config/bundled-skills/settings/tools → avatar}/identity-avatar.ts +1 -1
  430. package/src/backup/__tests__/backup-worker.test.ts +0 -2
  431. package/src/backup/__tests__/paths.test.ts +3 -2
  432. package/src/backup/backup-worker.ts +1 -10
  433. package/src/backup/paths.ts +2 -18
  434. package/src/backup/restore.ts +7 -11
  435. package/src/browser/__tests__/operations.test.ts +0 -35
  436. package/src/browser/operations.ts +1 -47
  437. package/src/bundler/package-resolver.ts +2 -6
  438. package/src/calls/active-call-lease.ts +1 -1
  439. package/src/calls/call-constants.ts +1 -1
  440. package/src/calls/call-controller.ts +1 -5
  441. package/src/calls/call-domain.ts +14 -14
  442. package/src/calls/call-pointer-messages.ts +4 -9
  443. package/src/calls/call-store.ts +2 -1
  444. package/src/calls/guardian-action-sweep.ts +9 -25
  445. package/src/calls/guardian-dispatch.ts +1 -20
  446. package/src/calls/media-stream-audio-transcode.ts +2 -41
  447. package/src/calls/media-stream-server.ts +2 -3
  448. package/src/calls/media-stream-stt-session.ts +1 -3
  449. package/src/calls/relay-access-wait.ts +5 -8
  450. package/src/calls/relay-server.ts +15 -18
  451. package/src/calls/relay-setup-router.ts +2 -2
  452. package/src/calls/relay-verification.ts +4 -4
  453. package/src/calls/twilio-rest.ts +1 -1
  454. package/src/calls/twilio-routes.ts +160 -78
  455. package/src/calls/voice-control-protocol.ts +10 -10
  456. package/src/calls/voice-ingress-preflight.ts +2 -2
  457. package/src/calls/voice-session-bridge.ts +137 -42
  458. package/src/channels/__tests__/types.test.ts +25 -3
  459. package/src/channels/permission-profiles.ts +2 -72
  460. package/src/channels/types.ts +42 -26
  461. package/src/cli/AGENTS.md +1 -0
  462. package/src/cli/__tests__/notifications.test.ts +12 -10
  463. package/src/cli/commands/__tests__/attachment.test.ts +14 -8
  464. package/src/cli/commands/__tests__/backup.test.ts +3 -14
  465. package/src/cli/commands/__tests__/browser.test.ts +36 -31
  466. package/src/cli/commands/__tests__/cache.test.ts +23 -18
  467. package/src/cli/commands/__tests__/memory-v2.test.ts +396 -0
  468. package/src/cli/commands/__tests__/task.test.ts +36 -35
  469. package/src/cli/commands/__tests__/trust.test.ts +602 -0
  470. package/src/cli/commands/__tests__/ui-confirm.test.ts +14 -14
  471. package/src/cli/commands/__tests__/ui.test.ts +17 -17
  472. package/src/cli/commands/__tests__/watchers.test.ts +29 -29
  473. package/src/cli/commands/__tests__/webhooks.test.ts +544 -0
  474. package/src/cli/commands/attachment.ts +12 -8
  475. package/src/cli/commands/auth.ts +1 -1
  476. package/src/cli/commands/avatar.ts +192 -9
  477. package/src/cli/commands/backup.ts +14 -44
  478. package/src/cli/commands/browser.ts +52 -4
  479. package/src/cli/commands/cache.ts +7 -5
  480. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  481. package/src/cli/commands/clients.ts +11 -12
  482. package/src/cli/commands/completions.ts +1 -1
  483. package/src/cli/commands/contacts.ts +10 -10
  484. package/src/cli/commands/conversations-defer.ts +364 -0
  485. package/src/cli/commands/conversations-import.ts +2 -3
  486. package/src/cli/commands/conversations.ts +63 -53
  487. package/src/cli/commands/credential-execution.ts +1 -1
  488. package/src/cli/commands/credentials.ts +139 -5
  489. package/src/cli/commands/default-action.ts +1 -1
  490. package/src/cli/commands/domain.ts +2 -2
  491. package/src/cli/commands/email.ts +7 -7
  492. package/src/cli/commands/image-generation.ts +1 -1
  493. package/src/cli/commands/keys.ts +2 -2
  494. package/src/cli/commands/mcp.ts +1 -1
  495. package/src/cli/commands/memory-v2.ts +343 -0
  496. package/src/cli/commands/memory.ts +8 -8
  497. package/src/cli/commands/notifications.ts +21 -20
  498. package/src/cli/commands/oauth/__tests__/connect.test.ts +23 -5
  499. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +1 -1
  500. package/src/cli/commands/oauth/__tests__/mode.test.ts +1 -1
  501. package/src/cli/commands/oauth/__tests__/status.test.ts +1 -1
  502. package/src/cli/commands/oauth/__tests__/token.test.ts +1 -1
  503. package/src/cli/commands/oauth/connect.ts +2 -2
  504. package/src/cli/commands/oauth/shared.ts +29 -2
  505. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -6
  506. package/src/cli/commands/platform/__tests__/connect.test.ts +23 -11
  507. package/src/cli/commands/platform/__tests__/disconnect.test.ts +22 -10
  508. package/src/cli/commands/platform/__tests__/status.test.ts +22 -10
  509. package/src/cli/commands/platform/connect.ts +3 -3
  510. package/src/cli/commands/platform/disconnect.ts +4 -6
  511. package/src/cli/commands/platform/index.ts +12 -10
  512. package/src/cli/commands/routes.ts +7 -1
  513. package/src/cli/commands/sequence.ts +7 -7
  514. package/src/cli/commands/skills.ts +188 -82
  515. package/src/cli/commands/task.ts +12 -10
  516. package/src/cli/commands/trust.ts +460 -162
  517. package/src/cli/commands/ui.ts +3 -3
  518. package/src/cli/commands/usage.ts +10 -5
  519. package/src/cli/commands/watchers.ts +8 -8
  520. package/src/cli/commands/webhooks.ts +270 -0
  521. package/src/cli/lib/daemon-avatar-client.ts +37 -0
  522. package/src/cli/lib/daemon-credential-client.ts +27 -189
  523. package/src/cli/lib/ipc-params.ts +22 -0
  524. package/src/cli/program.ts +4 -0
  525. package/src/cli.ts +1 -61
  526. package/src/config/acp-defaults.test.ts +57 -0
  527. package/src/config/acp-defaults.ts +40 -0
  528. package/src/config/acp-schema.ts +1 -1
  529. package/src/config/assistant-feature-flags.ts +18 -142
  530. package/src/config/bundled-skills/acp/SKILL.md +44 -16
  531. package/src/config/bundled-skills/acp/TOOLS.json +45 -1
  532. package/src/config/bundled-skills/acp/tools/acp-list-agents.ts +12 -0
  533. package/src/config/bundled-skills/acp/tools/acp-steer.ts +12 -0
  534. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +14 -14
  535. package/src/config/bundled-skills/contacts/tools/contact-search.ts +1 -4
  536. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +11 -6
  537. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +6 -6
  538. package/src/config/bundled-skills/media-processing/services/reduce.ts +0 -13
  539. package/src/config/bundled-skills/messaging/tools/gmail-mime-helpers.ts +1 -1
  540. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  541. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -1
  542. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +1 -1
  543. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -1
  544. package/src/config/bundled-skills/settings/SKILL.md +2 -17
  545. package/src/config/bundled-skills/settings/TOOLS.json +0 -56
  546. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  547. package/src/config/bundled-tool-registry.ts +4 -6
  548. package/src/config/env.ts +7 -8
  549. package/src/config/feature-flag-registry.json +16 -24
  550. package/src/config/llm-resolver.ts +51 -33
  551. package/src/config/loader.ts +12 -15
  552. package/src/config/schema.ts +5 -72
  553. package/src/config/schemas/__tests__/filing.test.ts +58 -0
  554. package/src/config/schemas/__tests__/memory-v2.test.ts +186 -0
  555. package/src/config/schemas/filing.ts +12 -0
  556. package/src/config/schemas/host-browser.ts +2 -2
  557. package/src/config/schemas/inference.ts +0 -2
  558. package/src/config/schemas/ingress.ts +1 -1
  559. package/src/config/schemas/llm.ts +51 -9
  560. package/src/config/schemas/memory-storage.ts +1 -1
  561. package/src/config/schemas/memory-v2.ts +176 -0
  562. package/src/config/schemas/memory.ts +2 -0
  563. package/src/config/schemas/security.ts +0 -60
  564. package/src/config/schemas/services.ts +46 -7
  565. package/src/config/skills.ts +1 -1
  566. package/src/config/types.ts +0 -41
  567. package/src/contacts/contact-store.ts +2 -2
  568. package/src/contacts/contacts-write.ts +0 -38
  569. package/src/contacts/types.ts +8 -10
  570. package/src/context/token-estimator.ts +1 -1
  571. package/src/context/tool-result-truncation.ts +1 -1
  572. package/src/context/window-manager.ts +1 -1
  573. package/src/credential-execution/approval-bridge.ts +7 -69
  574. package/src/credential-execution/client.ts +17 -422
  575. package/src/credential-execution/feature-gates.ts +1 -2
  576. package/src/credential-execution/managed-catalog.ts +1 -1
  577. package/src/credential-health/credential-health-service.ts +1 -1
  578. package/src/daemon/__tests__/conversation-feed-event.test.ts +0 -13
  579. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
  580. package/src/daemon/__tests__/daemon-skill-host.test.ts +272 -0
  581. package/src/daemon/__tests__/meet-host-supervisor.test.ts +587 -0
  582. package/src/daemon/__tests__/meet-manifest-loader.test.ts +463 -0
  583. package/src/daemon/approval-generators.ts +2 -14
  584. package/src/daemon/classifier.ts +0 -106
  585. package/src/daemon/config-watcher.ts +14 -54
  586. package/src/daemon/connection-policy.ts +0 -14
  587. package/src/daemon/conversation-agent-loop-handlers.ts +37 -6
  588. package/src/daemon/conversation-agent-loop.ts +164 -53
  589. package/src/daemon/conversation-attachments.ts +5 -81
  590. package/src/daemon/conversation-error.ts +9 -5
  591. package/src/daemon/conversation-history.ts +1 -1
  592. package/src/daemon/conversation-launch.ts +1 -1
  593. package/src/daemon/conversation-messaging.ts +1 -1
  594. package/src/daemon/conversation-notifiers.ts +1 -1
  595. package/src/daemon/conversation-process.ts +9 -13
  596. package/src/daemon/conversation-runtime-assembly.ts +26 -88
  597. package/src/daemon/conversation-slash.ts +4 -160
  598. package/src/daemon/conversation-store.ts +368 -0
  599. package/src/daemon/conversation-surfaces.ts +5 -4
  600. package/src/daemon/conversation-tool-setup.ts +23 -172
  601. package/src/daemon/conversation.ts +46 -182
  602. package/src/daemon/daemon-control.ts +3 -3
  603. package/src/daemon/daemon-skill-host.ts +262 -0
  604. package/src/daemon/external-plugins-bootstrap.ts +67 -13
  605. package/src/daemon/handlers/config-channels.ts +2 -2
  606. package/src/daemon/handlers/config-embeddings.ts +1 -1
  607. package/src/daemon/handlers/config-ingress.ts +24 -2
  608. package/src/daemon/handlers/config-model.test.ts +17 -0
  609. package/src/daemon/handlers/config-model.ts +7 -52
  610. package/src/daemon/handlers/config-telegram.ts +6 -53
  611. package/src/daemon/handlers/config-voice.ts +1 -1
  612. package/src/daemon/handlers/conversations.ts +22 -156
  613. package/src/daemon/handlers/recording.ts +1 -1
  614. package/src/daemon/handlers/shared.ts +34 -35
  615. package/src/daemon/handlers/skills.ts +15 -23
  616. package/src/daemon/host-transfer-proxy.ts +500 -0
  617. package/src/daemon/lifecycle.ts +23 -258
  618. package/src/daemon/meet-host-startup.ts +51 -0
  619. package/src/daemon/meet-host-supervisor.ts +781 -0
  620. package/src/daemon/meet-manifest-loader.ts +410 -0
  621. package/src/daemon/memory-v2-startup.ts +35 -0
  622. package/src/daemon/message-protocol.ts +4 -7
  623. package/src/daemon/message-types/acp.ts +1 -0
  624. package/src/daemon/message-types/conversations.ts +16 -2
  625. package/src/daemon/message-types/host-transfer.ts +41 -0
  626. package/src/daemon/message-types/integrations.ts +6 -0
  627. package/src/daemon/message-types/messages.ts +14 -14
  628. package/src/daemon/message-types/schedules.ts +1 -0
  629. package/src/daemon/message-types/settings.ts +0 -6
  630. package/src/daemon/message-types/shared.ts +5 -2
  631. package/src/daemon/message-types/subagents.ts +2 -1
  632. package/src/daemon/message-types/workspace.ts +0 -2
  633. package/src/daemon/pkb-reminder-builder.test.ts +13 -12
  634. package/src/daemon/pkb-reminder-builder.ts +8 -16
  635. package/src/daemon/process-message.ts +616 -0
  636. package/src/daemon/providers-setup.ts +14 -6
  637. package/src/daemon/server.ts +79 -1274
  638. package/src/daemon/shutdown-handlers.ts +1 -1
  639. package/src/daemon/startup-error.ts +1 -1
  640. package/src/daemon/trust-context.ts +32 -0
  641. package/src/daemon/wake-target-adapter.ts +223 -0
  642. package/src/email/feature-gate.ts +1 -1
  643. package/src/events/domain-events.ts +1 -8
  644. package/src/events/tool-audit-listener.ts +2 -8
  645. package/src/events/tool-metrics-listener.ts +1 -4
  646. package/src/filing/filing-service.ts +194 -54
  647. package/src/followups/followup-store.ts +3 -71
  648. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +89 -21
  649. package/src/heartbeat/heartbeat-service.ts +32 -11
  650. package/src/home/__tests__/phase5-exit-criteria.test.ts +18 -1
  651. package/src/home/__tests__/rollup-producer.test.ts +67 -2
  652. package/src/home/assistant-feed-authoring.ts +8 -1
  653. package/src/home/feed-types.ts +1 -1
  654. package/src/home/relationship-state-writer.ts +1 -1
  655. package/src/home/rewrite-feed-title.ts +58 -0
  656. package/src/home/rollup-producer.ts +16 -3
  657. package/src/inbound/platform-callback-registration.ts +1 -17
  658. package/src/ipc/__tests__/attachment-ipc.test.ts +128 -66
  659. package/src/ipc/__tests__/browser-ipc.test.ts +75 -51
  660. package/src/ipc/__tests__/cache-ipc.test.ts +52 -107
  661. package/src/ipc/__tests__/cli-ipc.test.ts +9 -6
  662. package/src/ipc/__tests__/skill-server-bidirectional.test.ts +254 -0
  663. package/src/ipc/__tests__/skill-server.test.ts +182 -0
  664. package/src/ipc/__tests__/socket-path.test.ts +69 -23
  665. package/src/ipc/__tests__/ui-request-route.test.ts +241 -216
  666. package/src/ipc/__tests__/watcher-ipc.test.ts +33 -33
  667. package/src/ipc/assistant-server.ts +450 -0
  668. package/src/ipc/cli-client.ts +3 -3
  669. package/src/ipc/gateway-client.test.ts +131 -0
  670. package/src/ipc/gateway-client.ts +98 -123
  671. package/src/ipc/ipc-framing.ts +281 -0
  672. package/src/ipc/routes/__tests__/memory-v2-backfill.test.ts +152 -0
  673. package/src/ipc/routes/__tests__/memory-v2-validate.test.ts +219 -0
  674. package/src/ipc/routes/db-proxy.ts +73 -0
  675. package/src/ipc/routes/route-adapter.ts +32 -0
  676. package/src/ipc/routes/trust-rules.test.ts +218 -0
  677. package/src/ipc/skill-ipc-types.ts +13 -0
  678. package/src/ipc/skill-routes/__tests__/config.test.ts +146 -0
  679. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +402 -0
  680. package/src/ipc/skill-routes/__tests__/identity.test.ts +81 -0
  681. package/src/ipc/skill-routes/__tests__/log.test.ts +133 -0
  682. package/src/ipc/skill-routes/__tests__/memory.test.ts +178 -0
  683. package/src/ipc/skill-routes/__tests__/platform.test.ts +111 -0
  684. package/src/ipc/skill-routes/__tests__/providers.test.ts +265 -0
  685. package/src/ipc/skill-routes/__tests__/registries.test.ts +361 -0
  686. package/src/ipc/skill-routes/config.ts +47 -0
  687. package/src/ipc/skill-routes/events.ts +131 -0
  688. package/src/ipc/skill-routes/identity.ts +34 -0
  689. package/src/ipc/skill-routes/index.ts +37 -0
  690. package/src/ipc/skill-routes/log.ts +40 -0
  691. package/src/ipc/skill-routes/memory.ts +76 -0
  692. package/src/ipc/skill-routes/platform.ts +39 -0
  693. package/src/ipc/skill-routes/providers.ts +163 -0
  694. package/src/ipc/skill-routes/registries.ts +393 -0
  695. package/src/ipc/skill-server.ts +771 -0
  696. package/src/ipc/skill-socket-path.ts +20 -0
  697. package/src/ipc/socket-cleanup.ts +92 -0
  698. package/src/ipc/socket-path.ts +63 -32
  699. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +374 -0
  700. package/src/live-voice/__tests__/live-voice-archive.test.ts +525 -0
  701. package/src/live-voice/__tests__/live-voice-events.test.ts +473 -0
  702. package/src/live-voice/__tests__/live-voice-integration.test.ts +359 -0
  703. package/src/live-voice/__tests__/live-voice-metrics.test.ts +179 -0
  704. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +349 -0
  705. package/src/live-voice/__tests__/live-voice-stt.test.ts +244 -0
  706. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +337 -0
  707. package/src/live-voice/__tests__/live-voice-tts.test.ts +337 -0
  708. package/src/live-voice/__tests__/protocol.test.ts +295 -0
  709. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +421 -0
  710. package/src/live-voice/live-voice-archive.ts +758 -0
  711. package/src/live-voice/live-voice-metrics.ts +472 -0
  712. package/src/live-voice/live-voice-session-manager.ts +222 -0
  713. package/src/live-voice/live-voice-session.ts +1144 -0
  714. package/src/live-voice/live-voice-tts.ts +260 -0
  715. package/src/live-voice/protocol.ts +524 -0
  716. package/src/mcp/client.ts +2 -2
  717. package/src/media/types.ts +4 -4
  718. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +4 -28
  719. package/src/memory/__tests__/auto-analysis-guard.test.ts +2 -2
  720. package/src/memory/__tests__/conversation-analyze-job.test.ts +7 -62
  721. package/src/memory/__tests__/conversation-group-migration.test.ts +2 -2
  722. package/src/memory/__tests__/find-analysis-conversation.test.ts +2 -1
  723. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +235 -0
  724. package/src/memory/admin.ts +65 -7
  725. package/src/memory/app-git-service.ts +0 -14
  726. package/src/memory/attachments-store.ts +14 -16
  727. package/src/memory/auto-analysis-enqueue.ts +2 -17
  728. package/src/memory/canonical-guardian-store.ts +2 -1
  729. package/src/memory/channel-verification-sessions.ts +1 -1
  730. package/src/memory/checkpoints.ts +1 -1
  731. package/src/memory/context-search/agent-protocol.ts +424 -0
  732. package/src/memory/context-search/agent-runner.ts +1295 -0
  733. package/src/memory/context-search/format.ts +160 -0
  734. package/src/memory/context-search/limits.ts +106 -0
  735. package/src/memory/context-search/search.ts +387 -0
  736. package/src/memory/context-search/sources/conversations.ts +278 -0
  737. package/src/memory/context-search/sources/memory.ts +90 -0
  738. package/src/memory/context-search/sources/pkb.ts +468 -0
  739. package/src/memory/context-search/sources/workspace.ts +1255 -0
  740. package/src/memory/context-search/types.ts +49 -0
  741. package/src/memory/conversation-analyze-job.ts +3 -24
  742. package/src/memory/conversation-attention-store.ts +1 -1
  743. package/src/memory/conversation-bootstrap.ts +1 -1
  744. package/src/memory/conversation-crud.ts +69 -127
  745. package/src/memory/conversation-directories.ts +1 -11
  746. package/src/memory/conversation-display-order-migration.ts +11 -2
  747. package/src/memory/conversation-group-migration.ts +20 -4
  748. package/src/memory/conversation-key-store.ts +3 -4
  749. package/src/memory/conversation-queries.ts +13 -26
  750. package/src/memory/conversation-starter-validation.ts +88 -0
  751. package/src/memory/conversation-starters-cadence.ts +1 -1
  752. package/src/memory/conversation-title-service.ts +2 -1
  753. package/src/memory/db-init.ts +14 -4
  754. package/src/memory/db-maintenance.ts +1 -1
  755. package/src/memory/delivery-channels.ts +1 -14
  756. package/src/memory/delivery-crud.ts +2 -32
  757. package/src/memory/delivery-status.ts +1 -1
  758. package/src/memory/embedding-gemini.test.ts +4 -4
  759. package/src/memory/external-conversation-store.ts +1 -1
  760. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +412 -0
  761. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +225 -0
  762. package/src/memory/graph/bootstrap.test.ts +2 -7
  763. package/src/memory/graph/bootstrap.ts +2 -1
  764. package/src/memory/graph/capability-seed.ts +3 -3
  765. package/src/memory/graph/compaction.ts +1 -1
  766. package/src/memory/graph/consolidation.ts +13 -10
  767. package/src/memory/graph/conversation-graph-memory.ts +151 -1
  768. package/src/memory/graph/decay.ts +1 -1
  769. package/src/memory/graph/extraction.ts +53 -21
  770. package/src/memory/graph/graph-memory-state-store.ts +1 -1
  771. package/src/memory/graph/graph-search.test.ts +94 -2
  772. package/src/memory/graph/graph-search.ts +22 -7
  773. package/src/memory/graph/image-ref-utils.ts +1 -1
  774. package/src/memory/graph/retriever.test.ts +158 -4
  775. package/src/memory/graph/retriever.ts +17 -5
  776. package/src/memory/graph/store.test.ts +2 -1
  777. package/src/memory/graph/store.ts +1 -1
  778. package/src/memory/graph/tool-handlers.ts +73 -247
  779. package/src/memory/graph/tools.ts +35 -53
  780. package/src/memory/group-crud.ts +1 -2
  781. package/src/memory/guardian-action-store.ts +2 -1
  782. package/src/memory/guardian-approvals.ts +1 -1
  783. package/src/memory/guardian-rate-limits.ts +1 -1
  784. package/src/memory/indexer.ts +43 -17
  785. package/src/memory/invite-store.ts +1 -1
  786. package/src/memory/job-handlers/backfill.ts +1 -1
  787. package/src/memory/job-handlers/cleanup.ts +2 -1
  788. package/src/memory/job-handlers/conversation-starters.ts +18 -10
  789. package/src/memory/job-handlers/embedding.test.ts +2 -1
  790. package/src/memory/job-handlers/embedding.ts +1 -1
  791. package/src/memory/job-handlers/index-maintenance.ts +1 -1
  792. package/src/memory/job-handlers/summarization.ts +3 -3
  793. package/src/memory/job-utils.ts +3 -3
  794. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +362 -0
  795. package/src/memory/jobs/embed-concept-page.ts +210 -0
  796. package/src/memory/jobs/embed-pkb-file.test.ts +2 -1
  797. package/src/memory/jobs-store.ts +10 -2
  798. package/src/memory/jobs-worker.ts +58 -5
  799. package/src/memory/lifecycle-events-store.ts +1 -1
  800. package/src/memory/llm-request-log-store.ts +1 -1
  801. package/src/memory/llm-usage-store.ts +1 -1
  802. package/src/memory/media-store.ts +1 -1
  803. package/src/memory/memory-recall-log-store.ts +1 -1
  804. package/src/memory/migrations/038-actor-token-records.ts +3 -0
  805. package/src/memory/migrations/039-actor-refresh-token-records.ts +3 -0
  806. package/src/memory/migrations/226-schedule-wake-conversation-id.ts +11 -0
  807. package/src/memory/migrations/227-add-conversation-inference-profile.ts +18 -0
  808. package/src/memory/migrations/228-rename-inference-profile-snake-case.ts +27 -0
  809. package/src/memory/migrations/229-delete-private-conversations.test.ts +1087 -0
  810. package/src/memory/migrations/229-delete-private-conversations.ts +210 -0
  811. package/src/memory/migrations/230-acp-session-history.ts +41 -0
  812. package/src/memory/migrations/231-repair-memory-graph-event-dates.ts +128 -0
  813. package/src/memory/migrations/232-activation-state.ts +38 -0
  814. package/src/memory/migrations/index.ts +10 -0
  815. package/src/memory/migrations/registry.ts +7 -0
  816. package/src/memory/pkb/pkb-index.test.ts +4 -5
  817. package/src/memory/pkb/pkb-reconcile.test.ts +4 -5
  818. package/src/memory/pkb/pkb-search.test.ts +83 -3
  819. package/src/memory/pkb/pkb-search.ts +27 -14
  820. package/src/memory/published-pages-store.ts +1 -1
  821. package/src/memory/schema/acp.ts +30 -0
  822. package/src/memory/schema/conversations.ts +1 -1
  823. package/src/memory/schema/index.ts +1 -0
  824. package/src/memory/schema/infrastructure.ts +1 -32
  825. package/src/memory/schema/memory-graph.ts +36 -14
  826. package/src/memory/scoped-approval-grants.ts +2 -1
  827. package/src/memory/search/semantic.ts +2 -2
  828. package/src/memory/shared-app-links-store.ts +2 -1
  829. package/src/memory/tool-usage-store.ts +1 -1
  830. package/src/memory/trace-event-store.ts +2 -1
  831. package/src/memory/turn-events-store.ts +1 -1
  832. package/src/memory/v2/__tests__/activation-store.test.ts +202 -0
  833. package/src/memory/v2/__tests__/activation.test.ts +956 -0
  834. package/src/memory/v2/__tests__/backfill-jobs.test.ts +610 -0
  835. package/src/memory/v2/__tests__/consolidation-job.test.ts +395 -0
  836. package/src/memory/v2/__tests__/edges.test.ts +435 -0
  837. package/src/memory/v2/__tests__/injection.test.ts +792 -0
  838. package/src/memory/v2/__tests__/migration.test.ts +812 -0
  839. package/src/memory/v2/__tests__/page-store.test.ts +334 -0
  840. package/src/memory/v2/__tests__/qdrant.test.ts +438 -0
  841. package/src/memory/v2/__tests__/sim.test.ts +549 -0
  842. package/src/memory/v2/__tests__/skill-content.test.ts +85 -0
  843. package/src/memory/v2/__tests__/skill-qdrant.test.ts +657 -0
  844. package/src/memory/v2/__tests__/skill-store.test.ts +351 -0
  845. package/src/memory/v2/__tests__/sweep-job.test.ts +441 -0
  846. package/src/memory/v2/activation-store.ts +109 -0
  847. package/src/memory/v2/activation.ts +490 -0
  848. package/src/memory/v2/backfill-jobs.ts +442 -0
  849. package/src/memory/v2/consolidation-job.ts +304 -0
  850. package/src/memory/v2/edges.ts +217 -0
  851. package/src/memory/v2/injection.ts +307 -0
  852. package/src/memory/v2/migration.ts +654 -0
  853. package/src/memory/v2/now-text.ts +38 -0
  854. package/src/memory/v2/page-store.ts +245 -0
  855. package/src/memory/v2/prompts/consolidation.ts +185 -0
  856. package/src/memory/v2/prompts/sweep.ts +56 -0
  857. package/src/memory/v2/qdrant.ts +342 -0
  858. package/src/memory/v2/sim.ts +206 -0
  859. package/src/memory/v2/skill-content.ts +42 -0
  860. package/src/memory/v2/skill-qdrant.ts +395 -0
  861. package/src/memory/v2/skill-store.ts +128 -0
  862. package/src/memory/v2/sweep-job.ts +298 -0
  863. package/src/memory/v2/types.ts +116 -0
  864. package/src/memory/validation.ts +1 -1
  865. package/src/messaging/providers/index.ts +262 -0
  866. package/src/messaging/providers/slack/api.ts +242 -0
  867. package/src/messaging/providers/slack/message-metadata.ts +1 -1
  868. package/src/messaging/providers/slack/send.ts +383 -0
  869. package/src/messaging/providers/telegram-bot/adapter.ts +4 -42
  870. package/src/messaging/providers/telegram-bot/api.ts +253 -0
  871. package/src/messaging/providers/telegram-bot/client.ts +17 -58
  872. package/src/messaging/providers/telegram-bot/send.ts +232 -0
  873. package/src/messaging/providers/whatsapp/adapter.ts +4 -36
  874. package/src/messaging/providers/whatsapp/api.ts +319 -0
  875. package/src/messaging/providers/whatsapp/client.ts +4 -48
  876. package/src/messaging/providers/whatsapp/send.ts +209 -0
  877. package/src/notifications/adapters/slack.ts +5 -23
  878. package/src/notifications/adapters/telegram.ts +8 -29
  879. package/src/notifications/conversation-candidates.ts +1 -1
  880. package/src/notifications/conversation-seed-composer.ts +12 -6
  881. package/src/notifications/copy-composer.ts +1 -1
  882. package/src/notifications/decision-engine.ts +1 -1
  883. package/src/notifications/decisions-store.ts +1 -1
  884. package/src/notifications/deliveries-store.ts +2 -1
  885. package/src/notifications/deterministic-checks.ts +1 -1
  886. package/src/notifications/events-store.ts +1 -13
  887. package/src/notifications/preferences-store.ts +1 -1
  888. package/src/notifications/signal.ts +0 -9
  889. package/src/oauth/connection-resolver.ts +11 -2
  890. package/src/oauth/oauth-store.ts +2 -1
  891. package/src/outbound-proxy/index.ts +0 -1
  892. package/src/permissions/approval-policy.test.ts +149 -132
  893. package/src/permissions/approval-policy.ts +65 -91
  894. package/src/permissions/checker.test.ts +632 -0
  895. package/src/permissions/checker.ts +266 -459
  896. package/src/permissions/gateway-threshold-reader.ts +28 -47
  897. package/src/permissions/ipc-risk-types.ts +95 -0
  898. package/src/permissions/prompter.ts +4 -9
  899. package/src/permissions/risk-types.ts +24 -210
  900. package/src/permissions/types.ts +17 -47
  901. package/src/permissions/workspace-policy.ts +2 -4
  902. package/src/playbooks/playbook-compiler.ts +1 -1
  903. package/src/plugins/defaults/index.ts +1 -1
  904. package/src/plugins/defaults/injectors.ts +18 -21
  905. package/src/plugins/defaults/llm-call.ts +6 -9
  906. package/src/plugins/defaults/memory-retrieval.ts +1 -6
  907. package/src/plugins/defaults/overflow-reduce.ts +9 -5
  908. package/src/plugins/defaults/token-estimate.ts +2 -3
  909. package/src/plugins/registry.ts +61 -1
  910. package/src/plugins/types.ts +6 -7
  911. package/src/plugins/user-loader.ts +36 -10
  912. package/src/prompts/__tests__/system-prompt-memory-v2.test.ts +197 -0
  913. package/src/prompts/persona-resolver.ts +2 -4
  914. package/src/prompts/system-prompt.ts +39 -0
  915. package/src/prompts/templates/SOUL.md +3 -1
  916. package/src/providers/__tests__/provider-env-vars.test.ts +0 -21
  917. package/src/providers/__tests__/retry-callsite.test.ts +3 -6
  918. package/src/providers/anthropic/client.ts +71 -19
  919. package/src/providers/call-site-routing.ts +7 -3
  920. package/src/providers/fireworks/client.ts +3 -0
  921. package/src/providers/gemini/client.ts +96 -22
  922. package/src/providers/managed-proxy/context.ts +0 -12
  923. package/src/providers/model-catalog.ts +83 -8
  924. package/src/providers/model-intents.ts +7 -8
  925. package/src/providers/openai/chat-completions-provider.ts +37 -7
  926. package/src/providers/openai/responses-provider.ts +39 -4
  927. package/src/providers/openrouter/client.ts +4 -5
  928. package/src/providers/provider-env-vars.ts +4 -12
  929. package/src/providers/provider-send-message.ts +16 -11
  930. package/src/providers/registry.ts +1 -1
  931. package/src/providers/retry.ts +52 -23
  932. package/src/providers/speech-to-text/openai-whisper-stream.ts +1 -1
  933. package/src/providers/speech-to-text/openai-whisper.ts +3 -6
  934. package/src/providers/speech-to-text/provider-catalog.ts +75 -0
  935. package/src/providers/speech-to-text/xai.ts +5 -5
  936. package/src/providers/thinking-config.ts +34 -0
  937. package/src/providers/types.ts +22 -10
  938. package/src/runtime/AGENTS.md +10 -9
  939. package/src/runtime/__tests__/agent-wake.test.ts +33 -9
  940. package/src/runtime/__tests__/client-registry.test.ts +5 -27
  941. package/src/runtime/__tests__/interactive-ui.test.ts +157 -246
  942. package/src/runtime/access-request-helper.ts +9 -20
  943. package/src/runtime/actor-trust-resolver.ts +2 -2
  944. package/src/runtime/agent-wake.ts +174 -68
  945. package/src/runtime/approval-conversation-turn.ts +2 -15
  946. package/src/runtime/approval-message-composer.ts +11 -60
  947. package/src/runtime/assistant-event.ts +18 -66
  948. package/src/runtime/auth/__tests__/guard-tests.test.ts +6 -30
  949. package/src/runtime/auth/__tests__/middleware.test.ts +10 -10
  950. package/src/runtime/auth/__tests__/route-policy.test.ts +0 -8
  951. package/src/runtime/auth/context.ts +9 -0
  952. package/src/runtime/auth/middleware.ts +4 -4
  953. package/src/runtime/auth/route-policy.ts +195 -4
  954. package/src/runtime/auth/token-service.ts +1 -100
  955. package/src/runtime/capability-tokens.ts +89 -313
  956. package/src/runtime/channel-approval-types.ts +1 -6
  957. package/src/runtime/channel-approvals.ts +7 -79
  958. package/src/runtime/channel-readiness-service.ts +2 -2
  959. package/src/runtime/channel-reply-delivery.ts +2 -8
  960. package/src/runtime/channel-retry-sweep.ts +20 -17
  961. package/src/runtime/client-registry.ts +21 -28
  962. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -7
  963. package/src/runtime/gateway-client.ts +37 -378
  964. package/src/runtime/guardian-action-grant-minter.ts +2 -3
  965. package/src/runtime/guardian-action-message-composer.ts +11 -52
  966. package/src/runtime/guardian-action-service.ts +19 -7
  967. package/src/runtime/guardian-decision-types.ts +4 -65
  968. package/src/runtime/guardian-reply-router.ts +10 -19
  969. package/src/runtime/guardian-vellum-migration.ts +5 -64
  970. package/src/runtime/http-errors.ts +3 -0
  971. package/src/runtime/http-router.ts +50 -7
  972. package/src/runtime/http-server.ts +345 -1110
  973. package/src/runtime/http-types.ts +15 -98
  974. package/src/runtime/interactive-ui-types.ts +145 -0
  975. package/src/runtime/interactive-ui.ts +38 -196
  976. package/src/runtime/invite-redemption-service.ts +1 -1
  977. package/src/runtime/invite-redemption-templates.ts +1 -1
  978. package/src/runtime/local-actor-identity.ts +13 -43
  979. package/src/runtime/message-composer-types.ts +134 -0
  980. package/src/runtime/middleware/rate-limiter.ts +1 -1
  981. package/src/runtime/middleware/request-logger.ts +5 -2
  982. package/src/runtime/migrations/__tests__/job-registry.test.ts +346 -0
  983. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +16 -0
  984. package/src/runtime/migrations/job-registry.ts +281 -0
  985. package/src/runtime/migrations/vbundle-builder.ts +3 -4
  986. package/src/runtime/migrations/vbundle-importer.ts +1 -1
  987. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -13
  988. package/src/runtime/migrations/vbundle-tar-stream.ts +11 -3
  989. package/src/runtime/nl-approval-parser.ts +16 -21
  990. package/src/runtime/pending-interactions.ts +29 -12
  991. package/src/runtime/routes/__tests__/acp-routes.test.ts +395 -0
  992. package/src/runtime/routes/__tests__/backup-routes.test.ts +204 -320
  993. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +72 -4
  994. package/src/runtime/routes/__tests__/stt-routes.test.ts +182 -223
  995. package/src/runtime/routes/__tests__/suggest-trust-rule-routes.test.ts +230 -0
  996. package/src/{ipc/__tests__/task-ipc.test.ts → runtime/routes/__tests__/task-routes.test.ts} +116 -96
  997. package/src/runtime/routes/__tests__/tts-routes.test.ts +185 -289
  998. package/src/runtime/routes/access-request-decision.ts +25 -50
  999. package/src/runtime/routes/acp-routes.test.ts +371 -0
  1000. package/src/runtime/routes/acp-routes.ts +392 -166
  1001. package/src/runtime/routes/app-management-routes.ts +464 -660
  1002. package/src/runtime/routes/app-routes.ts +192 -177
  1003. package/src/runtime/routes/approval-routes.ts +116 -434
  1004. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +24 -84
  1005. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +3 -10
  1006. package/src/runtime/routes/attachment-routes.ts +409 -253
  1007. package/src/runtime/routes/audio-routes.ts +51 -18
  1008. package/src/runtime/routes/avatar-routes.ts +82 -75
  1009. package/src/runtime/routes/background-tool-routes.ts +94 -0
  1010. package/src/runtime/routes/backup-routes.ts +154 -336
  1011. package/src/runtime/routes/brain-graph-routes.ts +83 -110
  1012. package/src/runtime/routes/browser-routes.ts +141 -0
  1013. package/src/runtime/routes/btw-routes.ts +62 -106
  1014. package/src/runtime/routes/cache-routes.ts +96 -0
  1015. package/src/runtime/routes/call-routes.ts +208 -247
  1016. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +1 -1
  1017. package/src/runtime/routes/channel-delivery-routes.ts +25 -27
  1018. package/src/runtime/routes/channel-readiness-routes.ts +83 -120
  1019. package/src/runtime/routes/channel-route-definitions.ts +62 -0
  1020. package/src/runtime/routes/channel-route-shared.ts +14 -18
  1021. package/src/runtime/routes/channel-verification-routes.ts +207 -187
  1022. package/src/runtime/routes/client-routes.ts +48 -0
  1023. package/src/runtime/routes/contact-routes.ts +533 -407
  1024. package/src/runtime/routes/conversation-analysis-routes.ts +48 -49
  1025. package/src/runtime/routes/conversation-attention-routes.ts +55 -67
  1026. package/src/runtime/routes/conversation-list-routes.ts +265 -0
  1027. package/src/runtime/routes/conversation-management-routes.ts +626 -715
  1028. package/src/runtime/routes/conversation-query-routes.ts +510 -460
  1029. package/src/runtime/routes/conversation-routes.ts +456 -368
  1030. package/src/runtime/routes/conversation-starter-routes.ts +121 -71
  1031. package/src/runtime/routes/credential-prompt-routes.ts +124 -0
  1032. package/src/runtime/routes/debug-routes.ts +34 -39
  1033. package/src/runtime/routes/defer-routes.ts +230 -0
  1034. package/src/runtime/routes/diagnostics-routes.ts +79 -70
  1035. package/src/runtime/routes/documents-routes.ts +117 -106
  1036. package/src/runtime/routes/errors.ts +132 -0
  1037. package/src/runtime/routes/events-routes.ts +97 -58
  1038. package/src/runtime/routes/filing-routes.ts +65 -78
  1039. package/src/runtime/routes/global-search-routes.ts +51 -57
  1040. package/src/runtime/routes/group-routes.ts +199 -181
  1041. package/src/runtime/routes/guardian-action-routes.ts +103 -169
  1042. package/src/runtime/routes/guardian-approval-interception.ts +27 -58
  1043. package/src/runtime/routes/guardian-approval-prompt.ts +10 -21
  1044. package/src/runtime/routes/guardian-approval-reply-helpers.ts +2 -6
  1045. package/src/runtime/routes/guardian-expiry-sweep.ts +19 -36
  1046. package/src/runtime/routes/heartbeat-routes.ts +194 -209
  1047. package/src/runtime/routes/home-feed-routes.ts +85 -187
  1048. package/src/runtime/routes/home-state-routes.ts +27 -24
  1049. package/src/runtime/routes/host-bash-routes.ts +42 -52
  1050. package/src/runtime/routes/host-browser-routes.ts +38 -69
  1051. package/src/runtime/routes/host-cu-routes.ts +74 -70
  1052. package/src/runtime/routes/host-file-routes.ts +50 -60
  1053. package/src/runtime/routes/host-transfer-routes.ts +220 -0
  1054. package/src/runtime/routes/http-adapter.ts +172 -0
  1055. package/src/runtime/routes/identity-routes.ts +83 -79
  1056. package/src/runtime/routes/inbound-conversation.ts +11 -18
  1057. package/src/runtime/routes/inbound-message-handler.ts +74 -110
  1058. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +79 -138
  1059. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +2 -3
  1060. package/src/runtime/routes/inbound-stages/background-dispatch.ts +54 -90
  1061. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +25 -50
  1062. package/src/runtime/routes/inbound-stages/edit-intercept.ts +7 -7
  1063. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  1064. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +5 -6
  1065. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -24
  1066. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -10
  1067. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -4
  1068. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +3 -3
  1069. package/src/runtime/routes/inbound-stages/verification-intercept.ts +19 -26
  1070. package/src/runtime/routes/index.ts +197 -0
  1071. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +25 -32
  1072. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +22 -31
  1073. package/src/runtime/routes/integrations/slack/channel.ts +69 -66
  1074. package/src/runtime/routes/integrations/slack/share.ts +49 -58
  1075. package/src/runtime/routes/integrations/telegram.ts +91 -74
  1076. package/src/runtime/routes/integrations/twilio.ts +163 -240
  1077. package/src/runtime/routes/integrations/vercel.ts +57 -54
  1078. package/src/runtime/routes/interface-routes.ts +43 -0
  1079. package/src/runtime/routes/internal-oauth-routes.ts +56 -0
  1080. package/src/runtime/routes/internal-twilio-routes.ts +46 -0
  1081. package/src/runtime/routes/llm-context-normalization.ts +4 -2
  1082. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  1083. package/src/runtime/routes/log-export-routes.ts +90 -100
  1084. package/src/runtime/routes/memory-item-routes.test.ts +152 -175
  1085. package/src/runtime/routes/memory-item-routes.ts +243 -323
  1086. package/src/runtime/routes/memory-v2-routes.ts +193 -0
  1087. package/src/runtime/routes/migration-rollback-routes.ts +167 -212
  1088. package/src/runtime/routes/migration-routes.ts +877 -374
  1089. package/src/runtime/routes/notification-routes.ts +199 -70
  1090. package/src/runtime/routes/oauth-apps.ts +254 -251
  1091. package/src/runtime/routes/oauth-providers.ts +66 -57
  1092. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +60 -120
  1093. package/src/runtime/routes/playground/__tests__/guard.test.ts +34 -54
  1094. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +107 -151
  1095. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +41 -117
  1096. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +95 -138
  1097. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +115 -217
  1098. package/src/runtime/routes/playground/__tests__/state.test.ts +41 -90
  1099. package/src/runtime/routes/playground/conversation-not-found.ts +9 -11
  1100. package/src/runtime/routes/playground/force-compact.ts +41 -54
  1101. package/src/runtime/routes/playground/guard.ts +18 -19
  1102. package/src/runtime/routes/playground/helpers.ts +103 -0
  1103. package/src/runtime/routes/playground/index.ts +15 -25
  1104. package/src/runtime/routes/playground/inject-failures.ts +48 -64
  1105. package/src/runtime/routes/playground/reset-circuit.ts +31 -57
  1106. package/src/runtime/routes/playground/seed-conversation.ts +66 -92
  1107. package/src/runtime/routes/playground/seeded-conversations.ts +60 -64
  1108. package/src/runtime/routes/playground/state.ts +23 -24
  1109. package/src/runtime/routes/profiler-routes.ts +132 -167
  1110. package/src/runtime/routes/ps-routes.ts +120 -0
  1111. package/src/runtime/routes/recording-routes.ts +197 -258
  1112. package/src/runtime/routes/rename-conversation-routes.ts +89 -0
  1113. package/src/runtime/routes/schedule-routes.ts +238 -242
  1114. package/src/runtime/routes/secret-routes.ts +219 -265
  1115. package/src/runtime/routes/secrets-deps.ts +24 -0
  1116. package/src/runtime/routes/settings-routes.ts +361 -441
  1117. package/src/runtime/routes/skills-routes.ts +434 -469
  1118. package/src/runtime/routes/stt-routes.ts +196 -206
  1119. package/src/runtime/routes/subagents-routes.ts +125 -141
  1120. package/src/runtime/routes/suggest-trust-rule-routes.ts +244 -0
  1121. package/src/runtime/routes/surface-action-routes.ts +135 -190
  1122. package/src/runtime/routes/surface-content-routes.ts +84 -118
  1123. package/src/runtime/routes/task-routes.ts +354 -0
  1124. package/src/runtime/routes/telemetry-routes.ts +33 -49
  1125. package/src/runtime/routes/trace-event-routes.ts +55 -74
  1126. package/src/runtime/routes/trust-rules-routes.ts +147 -239
  1127. package/src/runtime/routes/tts-routes.ts +187 -169
  1128. package/src/runtime/routes/types.ts +139 -0
  1129. package/src/{ipc/routes/ui-request.ts → runtime/routes/ui-request-routes.ts} +23 -17
  1130. package/src/runtime/routes/upgrade-broadcast-routes.ts +156 -197
  1131. package/src/runtime/routes/usage-routes.ts +143 -169
  1132. package/src/runtime/routes/user-routes.ts +102 -18
  1133. package/src/runtime/routes/wake-conversation-routes.ts +49 -0
  1134. package/src/{ipc/routes/watcher.ts → runtime/routes/watcher-routes.ts} +84 -39
  1135. package/src/runtime/routes/wipe-conversation-routes.ts +89 -0
  1136. package/src/runtime/routes/work-items-routes.test.ts +10 -20
  1137. package/src/runtime/routes/work-items-routes.ts +418 -433
  1138. package/src/runtime/routes/workspace-commit-routes.ts +30 -61
  1139. package/src/runtime/routes/workspace-routes.test.ts +254 -381
  1140. package/src/runtime/routes/workspace-routes.ts +238 -246
  1141. package/src/runtime/runtime-mode.ts +8 -1
  1142. package/src/runtime/services/__tests__/analyze-conversation.test.ts +80 -118
  1143. package/src/runtime/services/analyze-conversation.ts +14 -41
  1144. package/src/runtime/services/conversation-serializer.ts +181 -0
  1145. package/src/runtime/trust-context-resolver.ts +3 -2
  1146. package/src/runtime/verification-outbound-actions.ts +13 -49
  1147. package/src/schedule/schedule-store.ts +64 -2
  1148. package/src/schedule/scheduler.ts +101 -0
  1149. package/src/security/ces-credential-client.ts +32 -169
  1150. package/src/security/ces-rpc-credential-backend.ts +1 -1
  1151. package/src/security/credential-backend.ts +6 -6
  1152. package/src/security/oauth-completion-page.ts +1 -1
  1153. package/src/security/oauth2.ts +3 -6
  1154. package/src/sequence/analytics.ts +1 -1
  1155. package/src/sequence/guardrails.ts +3 -3
  1156. package/src/sequence/store.ts +2 -1
  1157. package/src/signals/bash.ts +1 -1
  1158. package/src/signals/event-stream.ts +1 -1
  1159. package/src/skills/catalog-cache.ts +7 -0
  1160. package/src/skills/catalog-files.ts +0 -5
  1161. package/src/skills/catalog-install.ts +28 -18
  1162. package/src/skills/category-inference.ts +0 -11
  1163. package/src/skills/clawhub.ts +2 -2
  1164. package/src/skills/managed-store.ts +2 -2
  1165. package/src/skills/remote-skill-policy.ts +6 -7
  1166. package/src/subagent/index.ts +2 -6
  1167. package/src/subagent/manager.ts +27 -23
  1168. package/src/subagent/types.ts +9 -0
  1169. package/src/tasks/SPEC.md +2 -2
  1170. package/src/tasks/task-compiler.ts +1 -1
  1171. package/src/tasks/task-runner.ts +2 -22
  1172. package/src/tasks/task-store.ts +1 -1
  1173. package/src/tools/acp/list-agents.test.ts +115 -0
  1174. package/src/tools/acp/list-agents.ts +31 -0
  1175. package/src/tools/acp/spawn.test.ts +379 -0
  1176. package/src/tools/acp/spawn.ts +142 -62
  1177. package/src/tools/acp/steer.test.ts +101 -0
  1178. package/src/tools/acp/steer.ts +38 -0
  1179. package/src/tools/background-tool-registry.ts +98 -0
  1180. package/src/tools/browser/browser-execution.ts +34 -7
  1181. package/src/tools/browser/browser-manager.ts +1 -8
  1182. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +1 -1
  1183. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +3 -1
  1184. package/src/tools/browser/cdp-client/types.ts +4 -1
  1185. package/src/tools/computer-use/definitions.ts +1 -1
  1186. package/src/tools/credential-execution/make-authenticated-request.ts +2 -2
  1187. package/src/tools/credential-execution/manage-secure-command-tool.ts +1 -1
  1188. package/src/tools/credential-execution/run-authenticated-command.ts +2 -2
  1189. package/src/tools/credentials/broker-types.ts +2 -1
  1190. package/src/tools/document/editor-template.ts +1 -1
  1191. package/src/tools/execution-timeout.ts +1 -1
  1192. package/src/tools/executor.ts +10 -15
  1193. package/src/tools/host-filesystem/transfer.test.ts +268 -0
  1194. package/src/tools/host-filesystem/transfer.ts +234 -0
  1195. package/src/tools/host-terminal/host-shell.ts +189 -11
  1196. package/src/tools/mcp/mcp-tool-factory.ts +1 -1
  1197. package/src/tools/memory/register.test.ts +161 -1
  1198. package/src/tools/memory/register.ts +19 -34
  1199. package/src/tools/permission-checker.ts +18 -219
  1200. package/src/tools/policy-context.ts +1 -8
  1201. package/src/tools/registry.ts +16 -1
  1202. package/src/tools/secret-detection-handler.ts +13 -103
  1203. package/src/tools/shared/shell-output.ts +4 -1
  1204. package/src/tools/side-effects.ts +2 -2
  1205. package/src/tools/skills/execute.ts +1 -1
  1206. package/src/tools/subagent/spawn.ts +35 -11
  1207. package/src/tools/terminal/safe-env.ts +9 -1
  1208. package/src/tools/terminal/shell.ts +161 -31
  1209. package/src/tools/tool-approval-handler.ts +4 -70
  1210. package/src/tools/tool-input-summary.ts +10 -0
  1211. package/src/tools/types.ts +143 -163
  1212. package/src/tools/ui-surface/definitions.ts +2 -2
  1213. package/src/util/debounce.ts +0 -21
  1214. package/src/util/errors.ts +0 -8
  1215. package/src/util/log-redact.ts +0 -1
  1216. package/src/util/platform.ts +85 -124
  1217. package/src/util/pricing.ts +109 -6
  1218. package/src/watcher/engine.ts +42 -20
  1219. package/src/watcher/watcher-store.ts +2 -1
  1220. package/src/work-items/work-item-store.ts +1 -1
  1221. package/src/workspace/git-service.ts +1 -6
  1222. package/src/workspace/migrations/006-services-config.ts +10 -1
  1223. package/src/workspace/migrations/017-seed-persona-dirs.ts +1 -1
  1224. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +1 -1
  1225. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +1 -1
  1226. package/src/workspace/migrations/031-drop-user-md.ts +1 -1
  1227. package/src/workspace/migrations/045-release-notes-meet-avatar.ts +3 -4
  1228. package/src/workspace/migrations/052-seed-default-inference-profiles.ts +150 -0
  1229. package/src/workspace/migrations/053-release-notes-acp-codex.ts +107 -0
  1230. package/src/workspace/migrations/054-seed-recall-callsite.ts +102 -0
  1231. package/src/workspace/migrations/055-release-notes-agentic-recall.ts +63 -0
  1232. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +65 -0
  1233. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +98 -0
  1234. package/src/workspace/migrations/058-release-notes-acp-sessions-ui.ts +71 -0
  1235. package/src/workspace/migrations/059-move-pid-to-workspace.ts +53 -0
  1236. package/src/workspace/migrations/060-memory-v2-init.ts +53 -0
  1237. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +1 -1
  1238. package/src/workspace/migrations/registry.ts +18 -0
  1239. package/src/workspace/migrations/runner.ts +2 -2
  1240. package/src/workspace/provider-commit-message-generator.ts +1 -1
  1241. package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +0 -471
  1242. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +0 -436
  1243. package/src/__tests__/cli-command-risk-guard.test.ts +0 -368
  1244. package/src/__tests__/config-watcher-feature-flags.test.ts +0 -211
  1245. package/src/__tests__/conversation-approval-overrides.test.ts +0 -207
  1246. package/src/__tests__/conversation-host-access-routes.test.ts +0 -229
  1247. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +0 -226
  1248. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +0 -167
  1249. package/src/__tests__/ephemeral-permissions.test.ts +0 -474
  1250. package/src/__tests__/extension-id-sync-guard.test.ts +0 -241
  1251. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +0 -374
  1252. package/src/__tests__/native-host-marker-sync-guard.test.ts +0 -157
  1253. package/src/__tests__/pairing-concurrent.test.ts +0 -84
  1254. package/src/__tests__/pairing-routes.test.ts +0 -181
  1255. package/src/__tests__/parser.test.ts +0 -595
  1256. package/src/__tests__/permission-checker-host-gate.test.ts +0 -488
  1257. package/src/__tests__/permission-controls-v2-flag.test.ts +0 -55
  1258. package/src/__tests__/permission-mode.test.ts +0 -89
  1259. package/src/__tests__/provider-env-vars-scope.test.ts +0 -52
  1260. package/src/__tests__/risk-classifier-parity.test.ts +0 -230
  1261. package/src/__tests__/shell-identity.test.ts +0 -236
  1262. package/src/__tests__/shell-parser-fuzz.test.ts +0 -629
  1263. package/src/__tests__/shell-parser-property.test.ts +0 -936
  1264. package/src/__tests__/starter-bundle.test.ts +0 -173
  1265. package/src/__tests__/stt-catalog-parity.test.ts +0 -282
  1266. package/src/__tests__/task-runner.test.ts +0 -224
  1267. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -354
  1268. package/src/__tests__/trust-store-pattern-matches.test.ts +0 -29
  1269. package/src/__tests__/trust-store.test.ts +0 -2013
  1270. package/src/__tests__/v2-consent-policy.test.ts +0 -103
  1271. package/src/browser/identifiers.ts +0 -51
  1272. package/src/cli/db.ts +0 -1
  1273. package/src/config/bundled-skills/settings/tools/avatar-get.ts +0 -40
  1274. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +0 -64
  1275. package/src/config/bundled-skills/settings/tools/avatar-update.ts +0 -88
  1276. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +0 -127
  1277. package/src/daemon/approved-devices-store.ts +0 -110
  1278. package/src/daemon/external-skills-bootstrap.ts +0 -41
  1279. package/src/daemon/message-types/trust.ts +0 -71
  1280. package/src/daemon/pairing-store.ts +0 -229
  1281. package/src/ipc/cli-server.ts +0 -252
  1282. package/src/ipc/routes/attachment.ts +0 -114
  1283. package/src/ipc/routes/browser-context.ts +0 -63
  1284. package/src/ipc/routes/browser.ts +0 -97
  1285. package/src/ipc/routes/cache.ts +0 -96
  1286. package/src/ipc/routes/get-contact.ts +0 -16
  1287. package/src/ipc/routes/index.ts +0 -35
  1288. package/src/ipc/routes/list-clients.ts +0 -31
  1289. package/src/ipc/routes/merge-contacts.ts +0 -17
  1290. package/src/ipc/routes/notification.ts +0 -133
  1291. package/src/ipc/routes/rename-conversation.ts +0 -59
  1292. package/src/ipc/routes/search-contacts.ts +0 -19
  1293. package/src/ipc/routes/task-queue.ts +0 -226
  1294. package/src/ipc/routes/task.ts +0 -173
  1295. package/src/ipc/routes/upsert-contact.ts +0 -25
  1296. package/src/ipc/routes/wake-conversation.ts +0 -19
  1297. package/src/memory/db.ts +0 -23
  1298. package/src/permissions/arg-parser.test.ts +0 -161
  1299. package/src/permissions/arg-parser.ts +0 -141
  1300. package/src/permissions/bash-risk-classifier.test.ts +0 -1620
  1301. package/src/permissions/bash-risk-classifier.ts +0 -950
  1302. package/src/permissions/command-registry.test.ts +0 -774
  1303. package/src/permissions/command-registry.ts +0 -1005
  1304. package/src/permissions/defaults.ts +0 -314
  1305. package/src/permissions/file-risk-classifier.test.ts +0 -535
  1306. package/src/permissions/file-risk-classifier.ts +0 -274
  1307. package/src/permissions/permission-mode.ts +0 -24
  1308. package/src/permissions/schedule-risk-classifier.test.ts +0 -129
  1309. package/src/permissions/schedule-risk-classifier.ts +0 -85
  1310. package/src/permissions/shell-identity.ts +0 -297
  1311. package/src/permissions/skill-risk-classifier.test.ts +0 -311
  1312. package/src/permissions/skill-risk-classifier.ts +0 -214
  1313. package/src/permissions/trust-client.ts +0 -359
  1314. package/src/permissions/trust-store-interface.ts +0 -100
  1315. package/src/permissions/trust-store.ts +0 -1330
  1316. package/src/permissions/v2-consent-policy.ts +0 -87
  1317. package/src/permissions/web-risk-classifier.test.ts +0 -170
  1318. package/src/permissions/web-risk-classifier.ts +0 -89
  1319. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +0 -715
  1320. package/src/runtime/__tests__/capability-tokens.test.ts +0 -258
  1321. package/src/runtime/actor-refresh-token-store.ts +0 -156
  1322. package/src/runtime/actor-token-store.ts +0 -207
  1323. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -264
  1324. package/src/runtime/auth/credential-service.ts +0 -352
  1325. package/src/runtime/conversation-approval-overrides.ts +0 -86
  1326. package/src/runtime/routes/browser-extension-pair-routes.ts +0 -575
  1327. package/src/runtime/routes/channel-routes.ts +0 -112
  1328. package/src/runtime/routes/contact-routes.test.ts +0 -298
  1329. package/src/runtime/routes/guardian-bootstrap-routes.ts +0 -175
  1330. package/src/runtime/routes/guardian-refresh-routes.ts +0 -79
  1331. package/src/runtime/routes/invite-routes.ts +0 -280
  1332. package/src/runtime/routes/pairing-routes.ts +0 -431
  1333. package/src/runtime/routes/playground/deps.ts +0 -56
  1334. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +0 -67
  1335. package/src/runtime/services/analyze-deps-singleton.ts +0 -32
  1336. package/src/tasks/ephemeral-permissions.ts +0 -55
  1337. package/src/tools/terminal/parser.ts +0 -623
  1338. package/src/types/qrcode.d.ts +0 -13
  1339. package/src/util/network-info.ts +0 -55
  1340. /package/node_modules/@vellumai/{ces-contracts → ces-client}/tsconfig.json +0 -0
  1341. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/__tests__/grants.test.ts +0 -0
  1342. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/error.ts +0 -0
  1343. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/grants.ts +0 -0
  1344. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/handles.ts +0 -0
  1345. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/rendering.ts +0 -0
  1346. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/rpc.ts +0 -0
@@ -1,11 +1,8 @@
1
1
  import {
2
- afterAll,
3
- afterEach,
4
2
  beforeEach,
5
3
  describe,
6
4
  expect,
7
5
  mock,
8
- spyOn,
9
6
  test,
10
7
  } from "bun:test";
11
8
 
@@ -13,14 +10,11 @@ import type {
13
10
  AllowlistOption,
14
11
  PolicyContext,
15
12
  ScopeOption,
16
- TrustRule,
17
13
  } from "../permissions/types.js";
18
14
  import { RiskLevel } from "../permissions/types.js";
19
15
  import type {
20
16
  Tool,
21
17
  ToolExecutionResult,
22
- ToolLifecycleEvent,
23
- ToolPermissionPromptEvent,
24
18
  } from "../tools/types.js";
25
19
 
26
20
  const mockConfig = {
@@ -50,9 +44,7 @@ const mockConfig = {
50
44
  action: "warn" as const,
51
45
  entropyThreshold: 4.0,
52
46
  },
53
- permissions: {
54
- mode: "workspace" as const,
55
- },
47
+ permissions: {},
56
48
  };
57
49
 
58
50
  let fakeToolResult: ToolExecutionResult = { content: "ok", isError: false };
@@ -92,13 +84,11 @@ let cachedAssessmentOverride:
92
84
  riskLevel: string;
93
85
  reason: string;
94
86
  scopeOptions: Array<{ pattern: string; label: string }>;
87
+ directoryScopeOptions?: Array<{ scope: string; label: string }>;
95
88
  matchType: string;
96
89
  }
97
90
  | undefined;
98
91
 
99
- /** Spy on addRule to capture calls without replacing the real implementation. */
100
- let addRuleSpy: ReturnType<typeof spyOn> | undefined;
101
-
102
92
  mock.module("../config/loader.js", () => ({
103
93
  getConfig: () => mockConfig,
104
94
  loadConfig: () => mockConfig,
@@ -174,7 +164,6 @@ mock.module("../tools/terminal/sandbox.js", () => ({
174
164
  }));
175
165
 
176
166
  import { PermissionPrompter } from "../permissions/prompter.js";
177
- import * as trustStore from "../permissions/trust-store.js";
178
167
  import { isSideEffectTool, ToolExecutor } from "../tools/executor.js";
179
168
  import type { ToolContext } from "../tools/types.js";
180
169
 
@@ -196,10 +185,6 @@ function makePrompter(): PermissionPrompter {
196
185
  } as unknown as PermissionPrompter;
197
186
  }
198
187
 
199
- afterAll(() => {
200
- mock.restore();
201
- });
202
-
203
188
  describe("ToolExecutor allowedToolNames gating", () => {
204
189
  beforeEach(() => {
205
190
  fakeToolResult = { content: "ok", isError: false };
@@ -208,10 +193,6 @@ describe("ToolExecutor allowedToolNames gating", () => {
208
193
  checkResultOverride = undefined;
209
194
  checkFnOverride = undefined;
210
195
  cachedAssessmentOverride = undefined;
211
- if (addRuleSpy) {
212
- addRuleSpy.mockRestore();
213
- addRuleSpy = undefined;
214
- }
215
196
  });
216
197
 
217
198
  test("executes normally when allowedToolNames is not set", async () => {
@@ -285,10 +266,6 @@ describe("ToolExecutor policy context plumbing", () => {
285
266
  checkResultOverride = undefined;
286
267
  checkFnOverride = undefined;
287
268
  cachedAssessmentOverride = undefined;
288
- if (addRuleSpy) {
289
- addRuleSpy.mockRestore();
290
- addRuleSpy = undefined;
291
- }
292
269
  });
293
270
 
294
271
  test("passes PolicyContext with executionTarget for skill-origin tools", async () => {
@@ -316,7 +293,7 @@ describe("ToolExecutor policy context plumbing", () => {
316
293
  const result = await executor.execute(
317
294
  "skill_tool",
318
295
  { action: "run" },
319
- makeContext(),
296
+ makeContext({ requireFreshApproval: true }),
320
297
  );
321
298
 
322
299
  expect(result.isError).toBe(false);
@@ -324,7 +301,6 @@ describe("ToolExecutor policy context plumbing", () => {
324
301
  expect(lastCheckArgs!.policyContext).toEqual({
325
302
  conversationId: "conversation-1",
326
303
  executionContext: "conversation",
327
- ephemeralRules: undefined,
328
304
  executionTarget: "sandbox",
329
305
  });
330
306
  });
@@ -337,7 +313,7 @@ describe("ToolExecutor policy context plumbing", () => {
337
313
  const result = await executor.execute(
338
314
  "file_read",
339
315
  { path: "test.txt" },
340
- makeContext(),
316
+ makeContext({ requireFreshApproval: true }),
341
317
  );
342
318
 
343
319
  expect(result.isError).toBe(false);
@@ -345,7 +321,6 @@ describe("ToolExecutor policy context plumbing", () => {
345
321
  expect(lastCheckArgs!.policyContext).toEqual({
346
322
  conversationId: "conversation-1",
347
323
  executionContext: "conversation",
348
- ephemeralRules: undefined,
349
324
  });
350
325
  });
351
326
 
@@ -371,7 +346,7 @@ describe("ToolExecutor policy context plumbing", () => {
371
346
  const result = await executor.execute(
372
347
  "file_read",
373
348
  { path: "test.txt" },
374
- makeContext(),
349
+ makeContext({ requireFreshApproval: true }),
375
350
  );
376
351
 
377
352
  expect(result.isError).toBe(false);
@@ -379,7 +354,6 @@ describe("ToolExecutor policy context plumbing", () => {
379
354
  expect(lastCheckArgs!.policyContext).toEqual({
380
355
  conversationId: "conversation-1",
381
356
  executionContext: "conversation",
382
- ephemeralRules: undefined,
383
357
  });
384
358
  });
385
359
 
@@ -408,7 +382,7 @@ describe("ToolExecutor policy context plumbing", () => {
408
382
  const result = await executor.execute(
409
383
  "host_skill_tool",
410
384
  { action: "run" },
411
- makeContext(),
385
+ makeContext({ requireFreshApproval: true }),
412
386
  );
413
387
 
414
388
  expect(result.isError).toBe(false);
@@ -416,7 +390,6 @@ describe("ToolExecutor policy context plumbing", () => {
416
390
  expect(lastCheckArgs!.policyContext).toEqual({
417
391
  conversationId: "conversation-1",
418
392
  executionContext: "conversation",
419
- ephemeralRules: undefined,
420
393
  executionTarget: "host",
421
394
  });
422
395
  });
@@ -442,1060 +415,225 @@ describe("ToolExecutor policy context plumbing", () => {
442
415
  };
443
416
 
444
417
  const executor = new ToolExecutor(makePrompter());
445
- const result = await executor.execute("no_target_tool", {}, makeContext());
418
+ const result = await executor.execute("no_target_tool", {}, makeContext({ requireFreshApproval: true }));
446
419
 
447
420
  expect(result.isError).toBe(false);
448
421
  expect(lastCheckArgs).toBeDefined();
449
422
  expect(lastCheckArgs!.policyContext).toEqual({
450
423
  conversationId: "conversation-1",
451
424
  executionContext: "conversation",
452
- ephemeralRules: undefined,
453
425
  executionTarget: undefined,
454
426
  });
455
427
  });
456
428
  });
457
429
 
458
- /**
459
- * Helper: create a prompter that returns a specific decision with pattern/scope.
460
- */
461
- function makePrompterWithDecision(
462
- decision: string,
463
- selectedPattern?: string,
464
- selectedScope?: string,
465
- ): PermissionPrompter {
466
- return {
467
- prompt: async () => ({ decision, selectedPattern, selectedScope }),
468
- resolveConfirmation: () => {},
469
- updateSender: () => {},
470
- dispose: () => {},
471
- } as unknown as PermissionPrompter;
472
- }
430
+ // ---------------------------------------------------------------------------
431
+ // isSideEffectTool classifier
432
+ // ---------------------------------------------------------------------------
473
433
 
474
- describe("ToolExecutor contextual rule creation", () => {
475
- beforeEach(() => {
476
- fakeToolResult = { content: "ok", isError: false };
477
- lastCheckArgs = undefined;
478
- getToolOverride = undefined;
479
- checkResultOverride = undefined;
480
- checkFnOverride = undefined;
481
- scopeOptionsOverride = undefined;
482
- if (addRuleSpy) {
483
- addRuleSpy.mockRestore();
484
- addRuleSpy = undefined;
434
+ describe("isSideEffectTool", () => {
435
+ describe("returns true for side-effect tools", () => {
436
+ const sideEffectTools = [
437
+ "file_write",
438
+ "file_edit",
439
+ "host_file_write",
440
+ "host_file_edit",
441
+ "bash",
442
+ "host_bash",
443
+ "web_fetch",
444
+ "document_create",
445
+ "document_update",
446
+ "schedule_create",
447
+ "schedule_update",
448
+ "schedule_delete",
449
+ ];
450
+
451
+ for (const toolName of sideEffectTools) {
452
+ test(toolName, () => {
453
+ expect(isSideEffectTool(toolName)).toBe(true);
454
+ });
485
455
  }
486
456
  });
487
457
 
488
- function setupAddRuleSpy() {
489
- addRuleSpy = spyOn(trustStore, "addRule").mockImplementation(
490
- (
491
- tool: string,
492
- pattern: string,
493
- scope: string,
494
- decision = "allow",
495
- priority = 100,
496
- options?: { executionTarget?: string },
497
- ) => {
498
- return {
499
- id: "spy-rule-id",
500
- tool,
501
- pattern,
502
- scope,
503
- decision,
504
- priority,
505
- createdAt: Date.now(),
506
- ...options,
507
- } as TrustRule;
508
- },
509
- );
510
- return addRuleSpy;
511
- }
512
-
513
- test("always_allow for a skill tool captures execution target in the rule", async () => {
514
- checkResultOverride = { decision: "prompt", reason: "test prompt" };
515
- const spy = setupAddRuleSpy();
516
-
517
- getToolOverride = (name: string) => {
518
- if (name === "unknown_tool") return undefined;
519
- return {
520
- name,
521
- description: "skill tool",
522
- category: "skill",
523
- defaultRiskLevel: RiskLevel.Low,
524
- origin: "skill" as const,
525
- ownerSkillId: "my-skill-42",
526
- ownerSkillVersionHash: "sha256-deadbeef",
527
- executionTarget: "sandbox" as const,
528
- getDefinition: () => ({
529
- name,
530
- description: "skill tool",
531
- input_schema: { type: "object" as const, properties: {} },
532
- }),
533
- execute: async () => fakeToolResult,
534
- };
535
- };
458
+ describe("returns false for non-side-effect tools", () => {
459
+ const readOnlyTools = [
460
+ "file_read",
461
+ "memory_recall",
462
+ "memory_manage",
463
+ "web_search",
464
+ "browser_navigate",
465
+ "browser_click",
466
+ "browser_type",
467
+ "browser_press_key",
468
+ "browser_close",
469
+ "browser_attach",
470
+ "browser_detach",
471
+ "browser_fill_credential",
472
+ "browser_snapshot",
473
+ "browser_screenshot",
474
+ "browser_wait_for",
475
+ "browser_extract",
476
+ "skill_load",
477
+ "schedule_list",
478
+ ];
536
479
 
537
- const prompter = makePrompterWithDecision(
538
- "always_allow",
539
- "skill_tool:*",
540
- "/tmp/project",
541
- );
542
- const executor = new ToolExecutor(prompter);
543
- const result = await executor.execute(
544
- "skill_tool",
545
- { action: "run" },
546
- makeContext(),
547
- );
480
+ for (const toolName of readOnlyTools) {
481
+ test(toolName, () => {
482
+ expect(isSideEffectTool(toolName)).toBe(false);
483
+ });
484
+ }
485
+ });
548
486
 
549
- expect(result.isError).toBe(false);
550
- expect(spy).toHaveBeenCalledTimes(1);
551
- const [tool, pattern, scope, decision, _priority, options] =
552
- spy.mock.calls[0];
553
- expect(tool).toBe("skill_tool");
554
- expect(pattern).toBe("skill_tool:*");
555
- expect(scope).toBe("/tmp/project");
556
- expect(decision).toBe("allow");
557
- expect(options).toBeDefined();
558
- expect(options.executionTarget).toBe("sandbox");
487
+ test("returns false for unknown tool names", () => {
488
+ expect(isSideEffectTool("nonexistent_tool")).toBe(false);
489
+ expect(isSideEffectTool("")).toBe(false);
559
490
  });
560
491
 
561
- test("always_allow captures execution target without allowHighRisk", async () => {
562
- checkResultOverride = { decision: "prompt", reason: "test prompt" };
563
- const spy = setupAddRuleSpy();
492
+ describe("action-aware classification for mixed-action tools", () => {
493
+ test("credential_store store is a side-effect", () => {
494
+ expect(isSideEffectTool("credential_store", { action: "store" })).toBe(
495
+ true,
496
+ );
497
+ });
564
498
 
565
- getToolOverride = (name: string) => {
566
- if (name === "unknown_tool") return undefined;
567
- return {
568
- name,
569
- description: "high-risk skill tool",
570
- category: "skill",
571
- defaultRiskLevel: RiskLevel.High,
572
- origin: "skill" as const,
573
- ownerSkillId: "dangerous-skill",
574
- ownerSkillVersionHash: "sha256-abc",
575
- executionTarget: "host" as const,
576
- getDefinition: () => ({
577
- name,
578
- description: "high-risk skill tool",
579
- input_schema: { type: "object" as const, properties: {} },
580
- }),
581
- execute: async () => fakeToolResult,
582
- };
583
- };
499
+ test("credential_store delete is a side-effect", () => {
500
+ expect(isSideEffectTool("credential_store", { action: "delete" })).toBe(
501
+ true,
502
+ );
503
+ });
584
504
 
585
- const prompter = makePrompterWithDecision(
586
- "always_allow",
587
- "risky_tool:*",
588
- "everywhere",
589
- );
590
- const executor = new ToolExecutor(prompter);
591
- const result = await executor.execute("risky_tool", {}, makeContext());
505
+ test("credential_store prompt is a side-effect", () => {
506
+ expect(isSideEffectTool("credential_store", { action: "prompt" })).toBe(
507
+ true,
508
+ );
509
+ });
592
510
 
593
- expect(result.isError).toBe(false);
594
- expect(spy).toHaveBeenCalledTimes(1);
595
- const [tool, pattern, scope, decision, _priority, options] =
596
- spy.mock.calls[0];
597
- expect(tool).toBe("risky_tool");
598
- expect(pattern).toBe("risky_tool:*");
599
- expect(scope).toBe("everywhere");
600
- expect(decision).toBe("allow");
601
- expect(options).toBeDefined();
602
- expect(options.executionTarget).toBe("host");
511
+ test("credential_store list is NOT a side-effect", () => {
512
+ expect(isSideEffectTool("credential_store", { action: "list" })).toBe(
513
+ false,
514
+ );
515
+ });
516
+
517
+ test("credential_store without input is NOT a side-effect", () => {
518
+ expect(isSideEffectTool("credential_store")).toBe(false);
519
+ });
603
520
  });
521
+ });
522
+
523
+ // ---------------------------------------------------------------------------
524
+ // forcePromptSideEffects enforcement (PR 30)
525
+ // ---------------------------------------------------------------------------
604
526
 
605
- test("always_allow for a core tool creates rule without options", async () => {
606
- checkResultOverride = { decision: "prompt", reason: "test prompt" };
607
- const spy = setupAddRuleSpy();
527
+ describe("ToolExecutor forcePromptSideEffects enforcement", () => {
528
+ let promptCalled: boolean;
608
529
 
609
- // Default getTool returns core tools with no origin field
530
+ beforeEach(() => {
531
+ fakeToolResult = { content: "ok", isError: false };
532
+ lastCheckArgs = undefined;
610
533
  getToolOverride = undefined;
534
+ checkResultOverride = undefined;
535
+ checkFnOverride = undefined;
536
+ cachedAssessmentOverride = undefined;
537
+ promptCalled = false;
538
+ });
611
539
 
612
- const prompter = makePrompterWithDecision(
613
- "always_allow",
614
- "git *",
615
- "/tmp/project",
616
- );
617
- const executor = new ToolExecutor(prompter);
540
+ /**
541
+ * Prompter that tracks whether it was called and always allows.
542
+ */
543
+ function makeTrackingPrompter(): PermissionPrompter {
544
+ return {
545
+ prompt: async () => {
546
+ promptCalled = true;
547
+ return { decision: "allow" as const };
548
+ },
549
+ resolveConfirmation: () => {},
550
+ updateSender: () => {},
551
+ dispose: () => {},
552
+ } as unknown as PermissionPrompter;
553
+ }
554
+
555
+ test("side-effect tool with allow rule is forced to prompt when forcePromptSideEffects is true", async () => {
556
+ // check() returns allow (simulating a matched trust rule)
557
+ checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
558
+
559
+ const executor = new ToolExecutor(makeTrackingPrompter());
618
560
  const result = await executor.execute(
619
561
  "bash",
620
- { command: "git status" },
621
- makeContext(),
562
+ { command: "echo hello" },
563
+ makeContext({ forcePromptSideEffects: true }),
622
564
  );
623
565
 
624
566
  expect(result.isError).toBe(false);
625
- expect(spy).toHaveBeenCalledTimes(1);
626
- const [tool, pattern, scope, decision, _priority, options] =
627
- spy.mock.calls[0];
628
- expect(tool).toBe("bash");
629
- expect(pattern).toBe("git *");
630
- expect(scope).toBe("/tmp/project");
631
- expect(decision).toBe("allow");
632
- // No options since there's no execution target for core tools
633
- expect(options).toBeUndefined();
567
+ // The prompter must have been called despite the allow rule
568
+ expect(promptCalled).toBe(true);
634
569
  });
635
570
 
636
- test("always_allow without selectedPattern does not create a rule", async () => {
637
- checkResultOverride = { decision: "prompt", reason: "test prompt" };
638
- const spy = setupAddRuleSpy();
571
+ test("deny decision is preserved (not converted to prompt) even with forcePromptSideEffects", async () => {
572
+ checkResultOverride = {
573
+ decision: "deny",
574
+ reason: "Policy denies this tool",
575
+ };
639
576
 
640
- const prompter = makePrompterWithDecision(
641
- "always_allow",
642
- undefined,
643
- "/tmp/project",
644
- );
645
- const executor = new ToolExecutor(prompter);
577
+ const executor = new ToolExecutor(makeTrackingPrompter());
646
578
  const result = await executor.execute(
647
- "file_read",
648
- { path: "test.txt" },
649
- makeContext(),
579
+ "bash",
580
+ { command: "rm -rf /" },
581
+ makeContext({ forcePromptSideEffects: true }),
650
582
  );
651
583
 
652
- expect(result.isError).toBe(false);
653
- expect(spy).not.toHaveBeenCalled();
584
+ // Should be denied, not prompted
585
+ expect(result.isError).toBe(true);
586
+ expect(result.content).toBe("Policy denies this tool");
587
+ expect(promptCalled).toBe(false);
654
588
  });
655
589
 
656
- test("always_allow without selectedScope for scoped tool does not create a rule", async () => {
657
- checkResultOverride = { decision: "prompt", reason: "test prompt" };
658
- const spy = setupAddRuleSpy();
590
+ test("non-side-effect tool is unchanged even with forcePromptSideEffects", async () => {
591
+ // check() returns allow for a read-only tool
592
+ checkResultOverride = { decision: "allow", reason: "Allowed by default" };
659
593
 
660
- const prompter = makePrompterWithDecision(
661
- "always_allow",
662
- "file_read:*",
663
- undefined,
664
- );
665
- const executor = new ToolExecutor(prompter);
594
+ const executor = new ToolExecutor(makeTrackingPrompter());
666
595
  const result = await executor.execute(
667
596
  "file_read",
668
- { path: "test.txt" },
669
- makeContext(),
597
+ { path: "README.md" },
598
+ makeContext({ forcePromptSideEffects: true }),
670
599
  );
671
600
 
672
601
  expect(result.isError).toBe(false);
673
- expect(spy).not.toHaveBeenCalled();
602
+ // Prompter should NOT be called — file_read is not a side-effect tool
603
+ expect(promptCalled).toBe(false);
674
604
  });
675
605
 
676
- test("always_allow without selectedScope for non-scoped tool creates rule with everywhere scope", async () => {
677
- checkResultOverride = { decision: "prompt", reason: "test prompt" };
678
- scopeOptionsOverride = [];
679
- const spy = setupAddRuleSpy();
606
+ test("side-effect tool is auto-allowed when forcePromptSideEffects is false", async () => {
607
+ checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
680
608
 
681
- const prompter = makePrompterWithDecision(
682
- "always_allow",
683
- "some_tool:*",
684
- undefined,
609
+ const executor = new ToolExecutor(makeTrackingPrompter());
610
+ const result = await executor.execute(
611
+ "file_write",
612
+ { path: "test.txt", content: "data" },
613
+ makeContext({ forcePromptSideEffects: false }),
685
614
  );
686
- const executor = new ToolExecutor(prompter);
687
- const result = await executor.execute("some_tool", {}, makeContext());
688
615
 
689
616
  expect(result.isError).toBe(false);
690
- expect(spy).toHaveBeenCalledTimes(1);
691
- const [, , scope] = spy.mock.calls[0];
692
- expect(scope).toBe("everywhere");
617
+ // No prompt — standard behavior when forcePromptSideEffects is off
618
+ expect(promptCalled).toBe(false);
693
619
  });
694
620
 
695
- test("always_allow for core tool creates rule without execution target", async () => {
696
- checkResultOverride = { decision: "prompt", reason: "test prompt" };
697
- const spy = setupAddRuleSpy();
698
- getToolOverride = undefined;
621
+ test("side-effect tool is auto-allowed when forcePromptSideEffects is undefined", async () => {
622
+ checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
699
623
 
700
- const prompter = makePrompterWithDecision(
701
- "always_allow",
702
- "sudo *",
703
- "everywhere",
704
- );
705
- const executor = new ToolExecutor(prompter);
624
+ const executor = new ToolExecutor(makeTrackingPrompter());
706
625
  const result = await executor.execute(
707
- "bash",
708
- { command: "sudo apt update" },
709
- makeContext(),
626
+ "file_edit",
627
+ { path: "test.txt", old_string: "a", new_string: "b" },
628
+ makeContext(), // forcePromptSideEffects not set
710
629
  );
711
630
 
712
631
  expect(result.isError).toBe(false);
713
- expect(spy).toHaveBeenCalledTimes(1);
714
- const [, , , , , options] = spy.mock.calls[0];
715
- // Core tools have no executionTarget, so ruleOptions is empty → undefined
716
- expect(options).toBeUndefined();
632
+ expect(promptCalled).toBe(false);
717
633
  });
718
634
 
719
- test("skill tool with host execution target records executionTarget in rule", async () => {
720
- checkResultOverride = { decision: "prompt", reason: "test prompt" };
721
- const spy = setupAddRuleSpy();
722
-
723
- getToolOverride = (name: string) => {
724
- if (name === "unknown_tool") return undefined;
725
- return {
726
- name,
727
- description: "host skill tool",
728
- category: "skill",
729
- defaultRiskLevel: RiskLevel.Low,
730
- origin: "skill" as const,
731
- ownerSkillId: "host-skill",
732
- ownerSkillVersionHash: "host-hash-v1",
733
- executionTarget: "host" as const,
734
- getDefinition: () => ({
735
- name,
736
- description: "host skill tool",
737
- input_schema: { type: "object" as const, properties: {} },
738
- }),
739
- execute: async () => fakeToolResult,
740
- };
741
- };
742
-
743
- const prompter = makePrompterWithDecision(
744
- "always_allow",
745
- "host_action:*",
746
- "/tmp/project",
747
- );
748
- const executor = new ToolExecutor(prompter);
749
- const result = await executor.execute(
750
- "host_action",
751
- { action: "click" },
752
- makeContext(),
753
- );
754
-
755
- expect(result.isError).toBe(false);
756
- expect(spy).toHaveBeenCalledTimes(1);
757
- const [, , , , , options] = spy.mock.calls[0];
758
- expect(options).toBeDefined();
759
- expect(options.executionTarget).toBe("host");
760
- });
761
- });
762
-
763
- describe("ToolExecutor strict mode + high-risk integration (PR 25)", () => {
764
- beforeEach(() => {
765
- fakeToolResult = { content: "ok", isError: false };
766
- lastCheckArgs = undefined;
767
- getToolOverride = undefined;
768
- checkResultOverride = undefined;
769
- checkFnOverride = undefined;
770
- cachedAssessmentOverride = undefined;
771
- if (addRuleSpy) {
772
- addRuleSpy.mockRestore();
773
- addRuleSpy = undefined;
774
- }
775
- });
776
-
777
- function setupAddRuleSpy() {
778
- addRuleSpy = spyOn(trustStore, "addRule").mockImplementation(
779
- (
780
- tool: string,
781
- pattern: string,
782
- scope: string,
783
- decision = "allow",
784
- priority = 100,
785
- options?: { executionTarget?: string },
786
- ) => {
787
- return {
788
- id: "spy-rule-id",
789
- tool,
790
- pattern,
791
- scope,
792
- decision,
793
- priority,
794
- createdAt: Date.now(),
795
- ...options,
796
- } as TrustRule;
797
- },
798
- );
799
- return addRuleSpy;
800
- }
801
-
802
- test("always_allow creates rule with execution target for high-risk skill tool", async () => {
803
- checkResultOverride = {
804
- decision: "prompt",
805
- reason: "High risk: always requires approval",
806
- };
807
- const spy = setupAddRuleSpy();
808
-
809
- getToolOverride = (name: string) => {
810
- if (name === "unknown_tool") return undefined;
811
- return {
812
- name,
813
- description: "high-risk skill tool",
814
- category: "skill",
815
- defaultRiskLevel: RiskLevel.High,
816
- origin: "skill" as const,
817
- ownerSkillId: "deploy-skill",
818
- ownerSkillVersionHash: "sha256-deploy-v1",
819
- executionTarget: "host" as const,
820
- getDefinition: () => ({
821
- name,
822
- description: "high-risk skill tool",
823
- input_schema: { type: "object" as const, properties: {} },
824
- }),
825
- execute: async () => fakeToolResult,
826
- };
827
- };
828
-
829
- const prompter = makePrompterWithDecision(
830
- "always_allow",
831
- "deploy_tool:*",
832
- "everywhere",
833
- );
834
- const executor = new ToolExecutor(prompter);
835
- const result = await executor.execute(
836
- "deploy_tool",
837
- { target: "prod" },
838
- makeContext(),
839
- );
840
-
841
- expect(result.isError).toBe(false);
842
- expect(spy).toHaveBeenCalledTimes(1);
843
- const [tool, pattern, scope, decision, _priority, options] =
844
- spy.mock.calls[0];
845
- expect(tool).toBe("deploy_tool");
846
- expect(pattern).toBe("deploy_tool:*");
847
- expect(scope).toBe("everywhere");
848
- expect(decision).toBe("allow");
849
- // The key integration assertion: execution target is captured
850
- expect(options.executionTarget).toBeDefined();
851
- expect(options.executionTarget).toBe("host");
852
- });
853
-
854
- test("always_allow creates rule with execution target for skill tool", async () => {
855
- checkResultOverride = { decision: "prompt", reason: "test prompt" };
856
- const spy = setupAddRuleSpy();
857
-
858
- getToolOverride = (name: string) => {
859
- if (name === "unknown_tool") return undefined;
860
- return {
861
- name,
862
- description: "high-risk skill tool",
863
- category: "skill",
864
- defaultRiskLevel: RiskLevel.High,
865
- origin: "skill" as const,
866
- ownerSkillId: "risky-skill",
867
- ownerSkillVersionHash: "sha256-risky",
868
- executionTarget: "sandbox" as const,
869
- getDefinition: () => ({
870
- name,
871
- description: "high-risk skill tool",
872
- input_schema: { type: "object" as const, properties: {} },
873
- }),
874
- execute: async () => fakeToolResult,
875
- };
876
- };
877
-
878
- // User chooses always_allow — rule is created with execution target.
879
- const prompter = makePrompterWithDecision(
880
- "always_allow",
881
- "risky_op:*",
882
- "/tmp/project",
883
- );
884
- const executor = new ToolExecutor(prompter);
885
- const result = await executor.execute("risky_op", {}, makeContext());
886
-
887
- expect(result.isError).toBe(false);
888
- expect(spy).toHaveBeenCalledTimes(1);
889
- const [, , , , , options] = spy.mock.calls[0];
890
- expect(options).toBeDefined();
891
- // executionTarget should be present
892
- expect(options.executionTarget).toBe("sandbox");
893
- // Rule should have execution target
894
- // allowHighRisk is no longer persisted
895
- });
896
-
897
- test("executor forwards policyContext to check() for version-bound skill tool", async () => {
898
- getToolOverride = (name: string) => {
899
- if (name === "unknown_tool") return undefined;
900
- return {
901
- name,
902
- description: "versioned skill tool",
903
- category: "skill",
904
- defaultRiskLevel: RiskLevel.Low,
905
- origin: "skill" as const,
906
- ownerSkillId: "versioned-skill",
907
- ownerSkillVersionHash: "v3:content-hash-xyz",
908
- executionTarget: "sandbox" as const,
909
- getDefinition: () => ({
910
- name,
911
- description: "versioned skill tool",
912
- input_schema: { type: "object" as const, properties: {} },
913
- }),
914
- execute: async () => fakeToolResult,
915
- };
916
- };
917
-
918
- const executor = new ToolExecutor(makePrompter());
919
- await executor.execute("versioned_tool", { action: "test" }, makeContext());
920
-
921
- expect(lastCheckArgs).toBeDefined();
922
- expect(lastCheckArgs!.policyContext).toEqual({
923
- conversationId: "conversation-1",
924
- executionContext: "conversation",
925
- ephemeralRules: undefined,
926
- executionTarget: "sandbox",
927
- });
928
- });
929
-
930
- // ── Skill mutation approval regression tests (PR 30) ──────────
931
-
932
- test("always_allow for skill source write creates rule with execution target", async () => {
933
- checkResultOverride = {
934
- decision: "prompt",
935
- reason: "High risk: always requires approval",
936
- };
937
- const spy = setupAddRuleSpy();
938
-
939
- getToolOverride = (name: string) => {
940
- if (name === "unknown_tool") return undefined;
941
- return {
942
- name,
943
- description: "skill tool that writes to skill source",
944
- category: "skill",
945
- defaultRiskLevel: RiskLevel.High,
946
- origin: "skill" as const,
947
- ownerSkillId: "code-editor-skill",
948
- ownerSkillVersionHash: "sha256-v1-original",
949
- executionTarget: "sandbox" as const,
950
- getDefinition: () => ({
951
- name,
952
- description: "skill source writer",
953
- input_schema: { type: "object" as const, properties: {} },
954
- }),
955
- execute: async () => fakeToolResult,
956
- };
957
- };
958
-
959
- const prompter = makePrompterWithDecision(
960
- "always_allow",
961
- "file_write:*/skills/**",
962
- "everywhere",
963
- );
964
- const executor = new ToolExecutor(prompter);
965
- const result = await executor.execute(
966
- "file_write",
967
- { path: "/tmp/skills/my-skill/executor.ts" },
968
- makeContext(),
969
- );
970
-
971
- expect(result.isError).toBe(false);
972
- expect(spy).toHaveBeenCalledTimes(1);
973
- const [tool, pattern, scope, decision, , options] = spy.mock.calls[0];
974
- expect(tool).toBe("file_write");
975
- expect(pattern).toBe("file_write:*/skills/**");
976
- expect(scope).toBe("everywhere");
977
- expect(decision).toBe("allow");
978
- expect(options.executionTarget).toBeDefined();
979
- expect(options.executionTarget).toBe("sandbox");
980
- });
981
-
982
- test("always_allow for skill source write creates rule with execution target (baseline)", async () => {
983
- checkResultOverride = {
984
- decision: "prompt",
985
- reason: "High risk: always requires approval",
986
- };
987
- const spy = setupAddRuleSpy();
988
-
989
- getToolOverride = (name: string) => {
990
- if (name === "unknown_tool") return undefined;
991
- return {
992
- name,
993
- description: "skill tool that writes to skill source",
994
- category: "skill",
995
- defaultRiskLevel: RiskLevel.High,
996
- origin: "skill" as const,
997
- ownerSkillId: "editor-skill",
998
- ownerSkillVersionHash: "sha256-editor-v1",
999
- executionTarget: "sandbox" as const,
1000
- getDefinition: () => ({
1001
- name,
1002
- description: "skill source writer",
1003
- input_schema: { type: "object" as const, properties: {} },
1004
- }),
1005
- execute: async () => fakeToolResult,
1006
- };
1007
- };
1008
-
1009
- // User chooses always_allow
1010
- const prompter = makePrompterWithDecision(
1011
- "always_allow",
1012
- "file_write:*/skills/**",
1013
- "/tmp/project",
1014
- );
1015
- const executor = new ToolExecutor(prompter);
1016
- const result = await executor.execute(
1017
- "file_write",
1018
- { path: "/tmp/skills/my-skill/executor.ts" },
1019
- makeContext(),
1020
- );
1021
-
1022
- expect(result.isError).toBe(false);
1023
- expect(spy).toHaveBeenCalledTimes(1);
1024
- const [, , , , , options] = spy.mock.calls[0];
1025
- expect(options).toBeDefined();
1026
- expect(options.executionTarget).toBe("sandbox");
1027
- // Execution target is captured from the tool context
1028
- // allowHighRisk is no longer persisted
1029
- });
1030
-
1031
- test("skill version is captured in rule for future version-bound matching", async () => {
1032
- checkResultOverride = {
1033
- decision: "prompt",
1034
- reason: "High risk: always requires approval",
1035
- };
1036
- const spy = setupAddRuleSpy();
1037
-
1038
- getToolOverride = (name: string) => {
1039
- if (name === "unknown_tool") return undefined;
1040
- return {
1041
- name,
1042
- description: "versioned skill tool",
1043
- category: "skill",
1044
- defaultRiskLevel: RiskLevel.High,
1045
- origin: "skill" as const,
1046
- ownerSkillId: "versioned-editor",
1047
- ownerSkillVersionHash: "v3:content-hash-xyz789",
1048
- executionTarget: "sandbox" as const,
1049
- getDefinition: () => ({
1050
- name,
1051
- description: "versioned skill editor",
1052
- input_schema: { type: "object" as const, properties: {} },
1053
- }),
1054
- execute: async () => fakeToolResult,
1055
- };
1056
- };
1057
-
1058
- const prompter = makePrompterWithDecision(
1059
- "always_allow",
1060
- "file_edit:*/skills/**",
1061
- "everywhere",
1062
- );
1063
- const executor = new ToolExecutor(prompter);
1064
- const result = await executor.execute(
1065
- "file_edit",
1066
- { path: "/tmp/skills/my-skill/SKILL.md" },
1067
- makeContext(),
1068
- );
1069
-
1070
- expect(result.isError).toBe(false);
1071
- expect(spy).toHaveBeenCalledTimes(1);
1072
- const [tool, , , , , options] = spy.mock.calls[0];
1073
- expect(tool).toBe("file_edit");
1074
- expect(options.executionTarget).toBeDefined();
1075
- expect(options.executionTarget).toBe("sandbox");
1076
- });
1077
-
1078
- test("executor forwards policyContext with version for skill source mutation", async () => {
1079
- getToolOverride = (name: string) => {
1080
- if (name === "unknown_tool") return undefined;
1081
- return {
1082
- name,
1083
- description: "skill source editor",
1084
- category: "skill",
1085
- defaultRiskLevel: RiskLevel.High,
1086
- origin: "skill" as const,
1087
- ownerSkillId: "editor-skill",
1088
- ownerSkillVersionHash: "sha256-v2-updated",
1089
- executionTarget: "sandbox" as const,
1090
- getDefinition: () => ({
1091
- name,
1092
- description: "skill editor",
1093
- input_schema: { type: "object" as const, properties: {} },
1094
- }),
1095
- execute: async () => fakeToolResult,
1096
- };
1097
- };
1098
-
1099
- const executor = new ToolExecutor(makePrompter());
1100
- await executor.execute(
1101
- "file_write",
1102
- { path: "/tmp/skills/my-skill/index.ts" },
1103
- makeContext(),
1104
- );
1105
-
1106
- expect(lastCheckArgs).toBeDefined();
1107
- expect(lastCheckArgs!.policyContext).toEqual({
1108
- conversationId: "conversation-1",
1109
- executionContext: "conversation",
1110
- ephemeralRules: undefined,
1111
- executionTarget: "sandbox",
1112
- });
1113
- });
1114
-
1115
- test("executor creates rule on always_allow with full context", async () => {
1116
- checkResultOverride = {
1117
- decision: "prompt",
1118
- reason: "High risk: always requires approval",
1119
- };
1120
- const spy = setupAddRuleSpy();
1121
-
1122
- getToolOverride = (name: string) => {
1123
- if (name === "unknown_tool") return undefined;
1124
- return {
1125
- name,
1126
- description: "admin skill tool",
1127
- category: "skill",
1128
- defaultRiskLevel: RiskLevel.High,
1129
- origin: "skill" as const,
1130
- ownerSkillId: "admin-skill",
1131
- ownerSkillVersionHash: "sha256-admin-v2",
1132
- executionTarget: "host" as const,
1133
- getDefinition: () => ({
1134
- name,
1135
- description: "admin skill tool",
1136
- input_schema: { type: "object" as const, properties: {} },
1137
- }),
1138
- execute: async () => fakeToolResult,
1139
- };
1140
- };
1141
-
1142
- const prompter = makePrompterWithDecision(
1143
- "always_allow",
1144
- "admin_action:*",
1145
- "everywhere",
1146
- );
1147
- const executor = new ToolExecutor(prompter);
1148
- const result = await executor.execute(
1149
- "admin_action",
1150
- { op: "restart" },
1151
- makeContext(),
1152
- );
1153
-
1154
- expect(result.isError).toBe(false);
1155
- expect(spy).toHaveBeenCalledTimes(1);
1156
- const [tool, pattern, scope, decision, , options] = spy.mock.calls[0];
1157
-
1158
- // Verify complete integration of all fields
1159
- expect(tool).toBe("admin_action");
1160
- expect(pattern).toBe("admin_action:*");
1161
- expect(scope).toBe("everywhere");
1162
- expect(decision).toBe("allow");
1163
- expect(options.executionTarget).toBeDefined();
1164
- expect(options.executionTarget).toBe("host");
1165
- });
1166
- });
1167
-
1168
- // ---------------------------------------------------------------------------
1169
- // isSideEffectTool classifier
1170
- // ---------------------------------------------------------------------------
1171
-
1172
- describe("isSideEffectTool", () => {
1173
- describe("returns true for side-effect tools", () => {
1174
- const sideEffectTools = [
1175
- "file_write",
1176
- "file_edit",
1177
- "host_file_write",
1178
- "host_file_edit",
1179
- "bash",
1180
- "host_bash",
1181
- "web_fetch",
1182
- "document_create",
1183
- "document_update",
1184
- "schedule_create",
1185
- "schedule_update",
1186
- "schedule_delete",
1187
- ];
1188
-
1189
- for (const toolName of sideEffectTools) {
1190
- test(toolName, () => {
1191
- expect(isSideEffectTool(toolName)).toBe(true);
1192
- });
1193
- }
1194
- });
1195
-
1196
- describe("returns false for non-side-effect tools", () => {
1197
- const readOnlyTools = [
1198
- "file_read",
1199
- "memory_recall",
1200
- "memory_manage",
1201
- "web_search",
1202
- "browser_navigate",
1203
- "browser_click",
1204
- "browser_type",
1205
- "browser_press_key",
1206
- "browser_close",
1207
- "browser_attach",
1208
- "browser_detach",
1209
- "browser_fill_credential",
1210
- "browser_snapshot",
1211
- "browser_screenshot",
1212
- "browser_wait_for",
1213
- "browser_extract",
1214
- "skill_load",
1215
- "schedule_list",
1216
- ];
1217
-
1218
- for (const toolName of readOnlyTools) {
1219
- test(toolName, () => {
1220
- expect(isSideEffectTool(toolName)).toBe(false);
1221
- });
1222
- }
1223
- });
1224
-
1225
- test("returns false for unknown tool names", () => {
1226
- expect(isSideEffectTool("nonexistent_tool")).toBe(false);
1227
- expect(isSideEffectTool("")).toBe(false);
1228
- });
1229
-
1230
- describe("action-aware classification for mixed-action tools", () => {
1231
- test("credential_store store is a side-effect", () => {
1232
- expect(isSideEffectTool("credential_store", { action: "store" })).toBe(
1233
- true,
1234
- );
1235
- });
1236
-
1237
- test("credential_store delete is a side-effect", () => {
1238
- expect(isSideEffectTool("credential_store", { action: "delete" })).toBe(
1239
- true,
1240
- );
1241
- });
1242
-
1243
- test("credential_store prompt is a side-effect", () => {
1244
- expect(isSideEffectTool("credential_store", { action: "prompt" })).toBe(
1245
- true,
1246
- );
1247
- });
1248
-
1249
- test("credential_store list is NOT a side-effect", () => {
1250
- expect(isSideEffectTool("credential_store", { action: "list" })).toBe(
1251
- false,
1252
- );
1253
- });
1254
-
1255
- test("credential_store without input is NOT a side-effect", () => {
1256
- expect(isSideEffectTool("credential_store")).toBe(false);
1257
- });
1258
- });
1259
- });
1260
-
1261
- // Baseline: allow rules can auto-allow file_edit for the guardian persona
1262
- // today (no forced prompting). The mock check() delegates to
1263
- // findHighestPriorityRule (via spy) so a regression in trust-rule matching
1264
- // would cause this test to fail instead of being masked by a blanket
1265
- // mock-allow.
1266
- describe("ToolExecutor baseline: allow rule auto-allows file_edit guardian persona", () => {
1267
- const guardianPersonaPath = "/Users/alice/.vellum/workspace/users/alice.md";
1268
- let ruleSpy: ReturnType<typeof spyOn> | undefined;
1269
-
1270
- beforeEach(() => {
1271
- fakeToolResult = { content: "ok", isError: false };
1272
- lastCheckArgs = undefined;
1273
- getToolOverride = undefined;
1274
- checkResultOverride = undefined;
1275
- checkFnOverride = undefined;
1276
- cachedAssessmentOverride = undefined;
1277
- if (addRuleSpy) {
1278
- addRuleSpy.mockRestore();
1279
- addRuleSpy = undefined;
1280
- }
1281
-
1282
- // Simulate a trust rule that allows file_edit on the guardian's per-user
1283
- // persona file by stubbing findHighestPriorityRule. This mirrors the
1284
- // default allow rules that the trust-store creates for the guardian
1285
- // persona file (see permissions/defaults.ts).
1286
- ruleSpy = spyOn(trustStore, "findHighestPriorityRule").mockImplementation(
1287
- (tool: string, commands: string[], _scope: string) => {
1288
- if (tool !== "file_edit") return null;
1289
- for (const cmd of commands) {
1290
- if (cmd === `file_edit:${guardianPersonaPath}`) {
1291
- return {
1292
- id: "default:allow-file_edit-guardian-persona",
1293
- tool: "file_edit",
1294
- pattern: `file_edit:${guardianPersonaPath}`,
1295
- scope: "everywhere",
1296
- decision: "allow" as const,
1297
- priority: 100,
1298
- createdAt: Date.now(),
1299
- };
1300
- }
1301
- }
1302
- return null;
1303
- },
1304
- );
1305
-
1306
- // Wire the mock check() to delegate to findHighestPriorityRule, replicating
1307
- // the real check() logic for Medium-risk tools (file_edit).
1308
- checkFnOverride = async (toolName, input, workingDir) => {
1309
- const filePath =
1310
- (input.path as string) ?? (input.file_path as string) ?? "";
1311
- const resolved = filePath.startsWith("/")
1312
- ? filePath
1313
- : `${workingDir}/${filePath}`;
1314
- const candidates = [`${toolName}:${resolved}`];
1315
- const matched = trustStore.findHighestPriorityRule(
1316
- toolName,
1317
- candidates,
1318
- workingDir,
1319
- );
1320
- if (matched && matched.decision === "allow") {
1321
- return {
1322
- decision: "allow",
1323
- reason: `Matched trust rule: ${matched.pattern}`,
1324
- };
1325
- }
1326
- return { decision: "prompt", reason: "Medium risk: requires approval" };
1327
- };
1328
- });
1329
-
1330
- afterEach(() => {
1331
- checkFnOverride = undefined;
1332
- if (ruleSpy) {
1333
- ruleSpy.mockRestore();
1334
- ruleSpy = undefined;
1335
- }
1336
- });
1337
-
1338
- test("file_edit to guardian persona is auto-allowed via trust rule", async () => {
1339
- const executor = new ToolExecutor(makePrompter());
1340
- const result = await executor.execute(
1341
- "file_edit",
1342
- { path: guardianPersonaPath, content: "hello" },
1343
- makeContext(),
1344
- );
1345
- expect(result.isError).toBe(false);
1346
- expect(result.content).toBe("ok");
1347
- // Confirm checker was called with the correct tool name
1348
- expect(lastCheckArgs).toBeDefined();
1349
- expect(lastCheckArgs!.toolName).toBe("file_edit");
1350
- // Confirm findHighestPriorityRule was consulted
1351
- expect(ruleSpy).toHaveBeenCalled();
1352
- });
1353
-
1354
- test("file_edit to a non-guardian-persona path is NOT auto-allowed without a matching rule", async () => {
1355
- let promptCalled = false;
1356
- const trackingPrompter = {
1357
- prompt: async () => {
1358
- promptCalled = true;
1359
- return { decision: "allow" as const };
1360
- },
1361
- resolveConfirmation: () => {},
1362
- updateSender: () => {},
1363
- dispose: () => {},
1364
- } as unknown as PermissionPrompter;
1365
-
1366
- const executor = new ToolExecutor(trackingPrompter);
1367
- const result = await executor.execute(
1368
- "file_edit",
1369
- { path: "/tmp/project/other.md", content: "hello" },
1370
- makeContext(),
1371
- );
1372
- // check() returned 'prompt' (no matching trust rule for other.md),
1373
- // so the executor must have called the prompter.
1374
- expect(promptCalled).toBe(true);
1375
- expect(result.isError).toBe(false);
1376
- expect(lastCheckArgs).toBeDefined();
1377
- expect(lastCheckArgs!.toolName).toBe("file_edit");
1378
- });
1379
- });
1380
-
1381
- // ---------------------------------------------------------------------------
1382
- // forcePromptSideEffects enforcement (PR 30)
1383
- // ---------------------------------------------------------------------------
1384
-
1385
- describe("ToolExecutor forcePromptSideEffects enforcement", () => {
1386
- let promptCalled: boolean;
1387
-
1388
- beforeEach(() => {
1389
- fakeToolResult = { content: "ok", isError: false };
1390
- lastCheckArgs = undefined;
1391
- getToolOverride = undefined;
1392
- checkResultOverride = undefined;
1393
- checkFnOverride = undefined;
1394
- cachedAssessmentOverride = undefined;
1395
- promptCalled = false;
1396
- if (addRuleSpy) {
1397
- addRuleSpy.mockRestore();
1398
- addRuleSpy = undefined;
1399
- }
1400
- });
1401
-
1402
- /**
1403
- * Prompter that tracks whether it was called and always allows.
1404
- */
1405
- function makeTrackingPrompter(): PermissionPrompter {
1406
- return {
1407
- prompt: async () => {
1408
- promptCalled = true;
1409
- return { decision: "allow" as const };
1410
- },
1411
- resolveConfirmation: () => {},
1412
- updateSender: () => {},
1413
- dispose: () => {},
1414
- } as unknown as PermissionPrompter;
1415
- }
1416
-
1417
- test("side-effect tool with allow rule is forced to prompt when forcePromptSideEffects is true", async () => {
1418
- // check() returns allow (simulating a matched trust rule)
1419
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1420
-
1421
- const executor = new ToolExecutor(makeTrackingPrompter());
1422
- const result = await executor.execute(
1423
- "bash",
1424
- { command: "echo hello" },
1425
- makeContext({ forcePromptSideEffects: true }),
1426
- );
1427
-
1428
- expect(result.isError).toBe(false);
1429
- // The prompter must have been called despite the allow rule
1430
- expect(promptCalled).toBe(true);
1431
- });
1432
-
1433
- test("deny decision is preserved (not converted to prompt) even with forcePromptSideEffects", async () => {
1434
- checkResultOverride = {
1435
- decision: "deny",
1436
- reason: "Policy denies this tool",
1437
- };
1438
-
1439
- const executor = new ToolExecutor(makeTrackingPrompter());
1440
- const result = await executor.execute(
1441
- "bash",
1442
- { command: "rm -rf /" },
1443
- makeContext({ forcePromptSideEffects: true }),
1444
- );
1445
-
1446
- // Should be denied, not prompted
1447
- expect(result.isError).toBe(true);
1448
- expect(result.content).toBe("Policy denies this tool");
1449
- expect(promptCalled).toBe(false);
1450
- });
1451
-
1452
- test("non-side-effect tool is unchanged even with forcePromptSideEffects", async () => {
1453
- // check() returns allow for a read-only tool
1454
- checkResultOverride = { decision: "allow", reason: "Allowed by default" };
1455
-
1456
- const executor = new ToolExecutor(makeTrackingPrompter());
1457
- const result = await executor.execute(
1458
- "file_read",
1459
- { path: "README.md" },
1460
- makeContext({ forcePromptSideEffects: true }),
1461
- );
1462
-
1463
- expect(result.isError).toBe(false);
1464
- // Prompter should NOT be called — file_read is not a side-effect tool
1465
- expect(promptCalled).toBe(false);
1466
- });
1467
-
1468
- test("side-effect tool is auto-allowed when forcePromptSideEffects is false", async () => {
1469
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1470
-
1471
- const executor = new ToolExecutor(makeTrackingPrompter());
1472
- const result = await executor.execute(
1473
- "file_write",
1474
- { path: "test.txt", content: "data" },
1475
- makeContext({ forcePromptSideEffects: false }),
1476
- );
1477
-
1478
- expect(result.isError).toBe(false);
1479
- // No prompt — standard behavior when forcePromptSideEffects is off
1480
- expect(promptCalled).toBe(false);
1481
- });
1482
-
1483
- test("side-effect tool is auto-allowed when forcePromptSideEffects is undefined", async () => {
1484
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1485
-
1486
- const executor = new ToolExecutor(makeTrackingPrompter());
1487
- const result = await executor.execute(
1488
- "file_edit",
1489
- { path: "test.txt", old_string: "a", new_string: "b" },
1490
- makeContext(), // forcePromptSideEffects not set
1491
- );
1492
-
1493
- expect(result.isError).toBe(false);
1494
- expect(promptCalled).toBe(false);
1495
- });
1496
-
1497
- test("all side-effect tool types are forced to prompt", async () => {
1498
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
635
+ test("all side-effect tool types are forced to prompt", async () => {
636
+ checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1499
637
 
1500
638
  const sideEffectTools = [
1501
639
  { name: "file_write", input: { path: "x", content: "y" } },
@@ -1511,15 +649,6 @@ describe("ToolExecutor forcePromptSideEffects enforcement", () => {
1511
649
  { name: "bash", input: { command: "echo hi" } },
1512
650
  { name: "host_bash", input: { command: "echo hi" } },
1513
651
  { name: "web_fetch", input: { url: "https://example.com" } },
1514
- { name: "browser_navigate", input: { url: "https://example.com" } },
1515
- { name: "browser_click", input: { selector: "#btn" } },
1516
- { name: "browser_type", input: { selector: "#input", text: "hello" } },
1517
- { name: "browser_press_key", input: { key: "Enter" } },
1518
- { name: "browser_close", input: {} },
1519
- {
1520
- name: "browser_fill_credential",
1521
- input: { selector: "#pwd", credential: "test" },
1522
- },
1523
652
  { name: "document_create", input: { title: "doc", content: "body" } },
1524
653
  { name: "document_update", input: { id: "doc-1", content: "updated" } },
1525
654
  {
@@ -1559,508 +688,133 @@ describe("ToolExecutor forcePromptSideEffects enforcement", () => {
1559
688
  dispose: () => {},
1560
689
  } as unknown as PermissionPrompter;
1561
690
 
1562
- const executor = new ToolExecutor(countingPrompter);
1563
- const result = await executor.execute(
1564
- "bash",
1565
- { command: "ls" },
1566
- makeContext({ forcePromptSideEffects: true }),
1567
- );
1568
-
1569
- expect(result.isError).toBe(false);
1570
- // Should only prompt once — forcePromptSideEffects doesn't add a second prompt
1571
- // when check() already returned 'prompt'
1572
- expect(promptCount).toBe(1);
1573
- });
1574
-
1575
- // ── Guardian persona security invariant (PR 31) ──────────
1576
-
1577
- test("file_edit to guardian persona forces prompt in private conversation even with matching trust rule", async () => {
1578
- // This is a key security invariant: the guardian persona file contains
1579
- // the user's persistent memory. In a private conversation
1580
- // (forcePromptSideEffects=true), edits to it must always require explicit
1581
- // approval, even when a trust rule matches.
1582
- checkResultOverride = {
1583
- decision: "allow",
1584
- reason: "Matched trust rule: file_edit:*/users/*.md",
1585
- };
1586
-
1587
- const executor = new ToolExecutor(makeTrackingPrompter());
1588
- const result = await executor.execute(
1589
- "file_edit",
1590
- {
1591
- path: "/Users/alice/.vellum/workspace/users/alice.md",
1592
- old_string: "old pref",
1593
- new_string: "new pref",
1594
- },
1595
- makeContext({ forcePromptSideEffects: true }),
1596
- );
1597
-
1598
- expect(result.isError).toBe(false);
1599
- // file_edit is a side-effect tool, so forcePromptSideEffects must trigger prompting
1600
- expect(promptCalled).toBe(true);
1601
- });
1602
-
1603
- test("host_file_edit to guardian persona forces prompt in private conversation", async () => {
1604
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1605
-
1606
- const executor = new ToolExecutor(makeTrackingPrompter());
1607
- const result = await executor.execute(
1608
- "host_file_edit",
1609
- {
1610
- path: "/Users/alice/.vellum/workspace/users/alice.md",
1611
- old_string: "x",
1612
- new_string: "y",
1613
- },
1614
- makeContext({ forcePromptSideEffects: true }),
1615
- );
1616
-
1617
- expect(result.isError).toBe(false);
1618
- expect(promptCalled).toBe(true);
1619
- });
1620
-
1621
- // ── Always-mutating document tools (PR fix5) ──────────
1622
-
1623
- test("document_create forces prompt in private conversation", async () => {
1624
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1625
-
1626
- const executor = new ToolExecutor(makeTrackingPrompter());
1627
- const result = await executor.execute(
1628
- "document_create",
1629
- { title: "New Doc", content: "hello" },
1630
- makeContext({ forcePromptSideEffects: true }),
1631
- );
1632
-
1633
- expect(result.isError).toBe(false);
1634
- expect(promptCalled).toBe(true);
1635
- });
1636
-
1637
- test("document_update forces prompt in private conversation", async () => {
1638
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1639
-
1640
- const executor = new ToolExecutor(makeTrackingPrompter());
1641
- const result = await executor.execute(
1642
- "document_update",
1643
- { id: "doc-1", content: "updated" },
1644
- makeContext({ forcePromptSideEffects: true }),
1645
- );
1646
-
1647
- expect(result.isError).toBe(false);
1648
- expect(promptCalled).toBe(true);
1649
- });
1650
-
1651
- // ── Always-mutating schedule tools (PR fix7) ──────────
1652
-
1653
- test("schedule_create forces prompt in private conversation", async () => {
1654
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1655
-
1656
- const executor = new ToolExecutor(makeTrackingPrompter());
1657
- const result = await executor.execute(
1658
- "schedule_create",
1659
- { name: "Morning standup", cron: "0 9 * * 1-5" },
1660
- makeContext({ forcePromptSideEffects: true }),
1661
- );
1662
-
1663
- expect(result.isError).toBe(false);
1664
- expect(promptCalled).toBe(true);
1665
- });
1666
-
1667
- test("schedule_update forces prompt in private conversation", async () => {
1668
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1669
-
1670
- const executor = new ToolExecutor(makeTrackingPrompter());
1671
- const result = await executor.execute(
1672
- "schedule_update",
1673
- { id: "sched-1", cron: "0 10 * * 1-5" },
1674
- makeContext({ forcePromptSideEffects: true }),
1675
- );
1676
-
1677
- expect(result.isError).toBe(false);
1678
- expect(promptCalled).toBe(true);
1679
- });
1680
-
1681
- test("schedule_delete forces prompt in private conversation", async () => {
1682
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1683
-
1684
- const executor = new ToolExecutor(makeTrackingPrompter());
1685
- const result = await executor.execute(
1686
- "schedule_delete",
1687
- { id: "sched-1" },
1688
- makeContext({ forcePromptSideEffects: true }),
1689
- );
1690
-
1691
- expect(result.isError).toBe(false);
1692
- expect(promptCalled).toBe(true);
1693
- });
1694
-
1695
- // ── Credential store action-aware (PR fix9) ──────────
1696
-
1697
- test("credential_store store forces prompt in private conversation", async () => {
1698
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1699
-
1700
- const executor = new ToolExecutor(makeTrackingPrompter());
1701
- const result = await executor.execute(
1702
- "credential_store",
1703
- { action: "store", name: "api-key", value: "sk-secret-123" },
1704
- makeContext({ forcePromptSideEffects: true }),
1705
- );
1706
-
1707
- expect(result.isError).toBe(false);
1708
- expect(promptCalled).toBe(true);
1709
- });
1710
-
1711
- test("credential_store delete forces prompt in private conversation", async () => {
1712
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1713
-
1714
- const executor = new ToolExecutor(makeTrackingPrompter());
1715
- const result = await executor.execute(
1716
- "credential_store",
1717
- { action: "delete", name: "api-key" },
1718
- makeContext({ forcePromptSideEffects: true }),
1719
- );
1720
-
1721
- expect(result.isError).toBe(false);
1722
- expect(promptCalled).toBe(true);
1723
- });
1724
-
1725
- test("credential_store list does NOT force prompt in private conversation", async () => {
1726
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1727
-
1728
- const executor = new ToolExecutor(makeTrackingPrompter());
1729
- const result = await executor.execute(
1730
- "credential_store",
1731
- { action: "list" },
1732
- makeContext({ forcePromptSideEffects: true }),
1733
- );
1734
-
1735
- expect(result.isError).toBe(false);
1736
- // list is read-only — must NOT trigger forced prompting
1737
- expect(promptCalled).toBe(false);
1738
- });
1739
-
1740
- // ── Workspace mode + forcePromptSideEffects interaction ──────────
1741
-
1742
- test("workspace mode allow → prompt promotion still works for side-effect tools in private conversations", async () => {
1743
- // Simulate workspace mode returning 'allow' for a workspace-scoped file_write
1744
- checkResultOverride = {
1745
- decision: "allow",
1746
- reason: "Workspace mode: workspace-scoped operation auto-allowed",
1747
- };
1748
-
1749
- const executor = new ToolExecutor(makeTrackingPrompter());
1750
- const result = await executor.execute(
1751
- "file_write",
1752
- { path: "/tmp/project/test.txt", content: "data" },
1753
- makeContext({ forcePromptSideEffects: true }),
1754
- );
1755
-
1756
- expect(result.isError).toBe(false);
1757
- // file_write is a side-effect tool, so forcePromptSideEffects must promote
1758
- // the workspace mode allow → prompt, requiring explicit user approval
1759
- expect(promptCalled).toBe(true);
1760
- });
1761
-
1762
- test("schedule_create forces prompt in private conversation", async () => {
1763
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1764
-
1765
- const executor = new ToolExecutor(makeTrackingPrompter());
1766
- const result = await executor.execute(
1767
- "schedule_create",
1768
- {
1769
- name: "test schedule",
1770
- expression: "0 9 * * *",
1771
- message: "test",
1772
- },
1773
- makeContext({ forcePromptSideEffects: true }),
1774
- );
1775
-
1776
- expect(result.isError).toBe(false);
1777
- expect(promptCalled).toBe(true);
1778
- });
1779
-
1780
- test("schedule_list does NOT force prompt in private conversation", async () => {
1781
- checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1782
-
1783
- const executor = new ToolExecutor(makeTrackingPrompter());
1784
- const result = await executor.execute(
1785
- "schedule_list",
1786
- {},
1787
- makeContext({ forcePromptSideEffects: true }),
1788
- );
1789
-
1790
- expect(result.isError).toBe(false);
1791
- // list is read-only — must NOT trigger forced prompting
1792
- expect(promptCalled).toBe(false);
1793
- });
1794
- });
1795
-
1796
- // ---------------------------------------------------------------------------
1797
- // persistentDecisionsAllowed contract
1798
- // ---------------------------------------------------------------------------
1799
-
1800
- describe("ToolExecutor persistentDecisionsAllowed contract", () => {
1801
- beforeEach(() => {
1802
- fakeToolResult = { content: "ok", isError: false };
1803
- lastCheckArgs = undefined;
1804
- getToolOverride = undefined;
1805
- checkResultOverride = {
1806
- decision: "prompt",
1807
- reason: "Requires explicit approval",
1808
- };
1809
- checkFnOverride = undefined;
1810
- if (addRuleSpy) {
1811
- addRuleSpy.mockRestore();
1812
- addRuleSpy = undefined;
1813
- }
1814
- });
1815
-
1816
- function setupAddRuleSpy() {
1817
- addRuleSpy = spyOn(trustStore, "addRule").mockImplementation(
1818
- (
1819
- tool: string,
1820
- pattern: string,
1821
- scope: string,
1822
- decision = "allow",
1823
- priority = 100,
1824
- options?: { executionTarget?: string },
1825
- ) => {
1826
- return {
1827
- id: "spy-rule-id",
1828
- tool,
1829
- pattern,
1830
- scope,
1831
- decision,
1832
- priority,
1833
- createdAt: Date.now(),
1834
- ...options,
1835
- } as TrustRule;
1836
- },
1837
- );
1838
- return addRuleSpy;
1839
- }
1840
-
1841
- test("proxied bash always_allow saves a trust rule (no special-casing)", async () => {
1842
- const spy = setupAddRuleSpy();
1843
-
1844
- const prompter = makePrompterWithDecision(
1845
- "always_allow",
1846
- "bash:*",
1847
- "/tmp/project",
1848
- );
1849
- const executor = new ToolExecutor(prompter);
691
+ const executor = new ToolExecutor(countingPrompter);
1850
692
  const result = await executor.execute(
1851
693
  "bash",
1852
- { command: "curl https://example.com", network_mode: "proxied" },
1853
- makeContext(),
694
+ { command: "ls" },
695
+ makeContext({ forcePromptSideEffects: true }),
1854
696
  );
1855
697
 
1856
698
  expect(result.isError).toBe(false);
1857
- expect(spy).toHaveBeenCalledTimes(1);
699
+ // Should only prompt once — forcePromptSideEffects doesn't add a second prompt
700
+ // when check() already returned 'prompt'
701
+ expect(promptCount).toBe(1);
1858
702
  });
1859
703
 
1860
- test("non-proxied bash always_allow saves a trust rule", async () => {
1861
- const spy = setupAddRuleSpy();
704
+ // ── Always-mutating schedule tools ──────────
1862
705
 
1863
- const prompter = makePrompterWithDecision(
1864
- "always_allow",
1865
- "bash:*",
1866
- "/tmp/project",
1867
- );
1868
- const executor = new ToolExecutor(prompter);
706
+ test("schedule_delete forces prompt under forcePromptSideEffects", async () => {
707
+ checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
708
+
709
+ const executor = new ToolExecutor(makeTrackingPrompter());
1869
710
  const result = await executor.execute(
1870
- "bash",
1871
- { command: "git status" },
1872
- makeContext(),
711
+ "schedule_delete",
712
+ { id: "sched-1" },
713
+ makeContext({ forcePromptSideEffects: true }),
1873
714
  );
1874
715
 
1875
716
  expect(result.isError).toBe(false);
1876
- expect(spy).toHaveBeenCalledTimes(1);
717
+ expect(promptCalled).toBe(true);
1877
718
  });
1878
719
 
1879
- test("proxied bash always_deny saves a deny rule (no special-casing)", async () => {
1880
- const spy = setupAddRuleSpy();
1881
-
1882
- const prompter = makePrompterWithDecision(
1883
- "always_deny",
1884
- "bash:*",
1885
- "/tmp/project",
1886
- );
1887
- const executor = new ToolExecutor(prompter);
1888
- const result = await executor.execute(
1889
- "bash",
1890
- { command: "curl https://evil.com", network_mode: "proxied" },
1891
- makeContext(),
1892
- );
720
+ // ── Credential store action-aware (PR fix9) ──────────
1893
721
 
1894
- expect(result.isError).toBe(true);
1895
- expect(spy).toHaveBeenCalledTimes(1);
1896
- });
722
+ test("credential_store store forces prompt under forcePromptSideEffects", async () => {
723
+ checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1897
724
 
1898
- test("persistentDecisionsAllowed: true is emitted in lifecycle event for proxied bash", async () => {
1899
- let capturedEvent: ToolPermissionPromptEvent | undefined;
1900
- const prompter = makePrompterWithDecision("allow");
1901
- const executor = new ToolExecutor(prompter);
725
+ const executor = new ToolExecutor(makeTrackingPrompter());
1902
726
  const result = await executor.execute(
1903
- "bash",
1904
- { command: "curl https://example.com", network_mode: "proxied" },
1905
- makeContext({
1906
- onToolLifecycleEvent: (event: ToolLifecycleEvent) => {
1907
- if (event.type === "permission_prompt") {
1908
- capturedEvent = event;
1909
- }
1910
- },
1911
- }),
727
+ "credential_store",
728
+ { action: "store", name: "api-key", value: "sk-secret-123" },
729
+ makeContext({ forcePromptSideEffects: true }),
1912
730
  );
1913
731
 
1914
732
  expect(result.isError).toBe(false);
1915
- expect(capturedEvent).toBeDefined();
1916
- expect(capturedEvent!.persistentDecisionsAllowed).toBe(true);
733
+ expect(promptCalled).toBe(true);
1917
734
  });
1918
735
 
1919
- test("persistentDecisionsAllowed: true is emitted in lifecycle event for non-proxied bash", async () => {
1920
- let capturedEvent: ToolPermissionPromptEvent | undefined;
1921
- const prompter = makePrompterWithDecision("allow");
1922
- const executor = new ToolExecutor(prompter);
736
+ test("credential_store delete forces prompt under forcePromptSideEffects", async () => {
737
+ checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
738
+
739
+ const executor = new ToolExecutor(makeTrackingPrompter());
1923
740
  const result = await executor.execute(
1924
- "bash",
1925
- { command: "echo hello" },
1926
- makeContext({
1927
- onToolLifecycleEvent: (event: ToolLifecycleEvent) => {
1928
- if (event.type === "permission_prompt") {
1929
- capturedEvent = event;
1930
- }
1931
- },
1932
- }),
741
+ "credential_store",
742
+ { action: "delete", name: "api-key" },
743
+ makeContext({ forcePromptSideEffects: true }),
1933
744
  );
1934
745
 
1935
746
  expect(result.isError).toBe(false);
1936
- expect(capturedEvent).toBeDefined();
1937
- expect(capturedEvent!.persistentDecisionsAllowed).toBe(true);
747
+ expect(promptCalled).toBe(true);
1938
748
  });
1939
749
 
1940
- test("persistentDecisionsAllowed is true for proxied bash in prompter confirmation_request", async () => {
1941
- let capturedPersistent: unknown;
1942
- const prompter = {
1943
- prompt: async (
1944
- _toolName: string,
1945
- _input: Record<string, unknown>,
1946
- _riskLevel: string,
1947
- _allowlistOptions: AllowlistOption[],
1948
- _scopeOptions: ScopeOption[],
1949
- _diff: unknown,
1950
- _conversationId: unknown,
1951
- _executionTarget: unknown,
1952
- persistentDecisionsAllowed: boolean | undefined,
1953
- ) => {
1954
- capturedPersistent = persistentDecisionsAllowed;
1955
- return { decision: "allow" as const };
1956
- },
1957
- resolveConfirmation: () => {},
1958
- updateSender: () => {},
1959
- dispose: () => {},
1960
- } as unknown as PermissionPrompter;
750
+ test("credential_store list does NOT force prompt under forcePromptSideEffects", async () => {
751
+ checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
1961
752
 
1962
- const executor = new ToolExecutor(prompter);
753
+ const executor = new ToolExecutor(makeTrackingPrompter());
1963
754
  const result = await executor.execute(
1964
- "bash",
1965
- { command: "curl https://example.com", network_mode: "proxied" },
1966
- makeContext(),
755
+ "credential_store",
756
+ { action: "list" },
757
+ makeContext({ forcePromptSideEffects: true }),
1967
758
  );
1968
759
 
1969
760
  expect(result.isError).toBe(false);
1970
- expect(capturedPersistent).toBe(true);
761
+ // list is read-only — must NOT trigger forced prompting
762
+ expect(promptCalled).toBe(false);
1971
763
  });
1972
764
 
1973
- test("bash always_allow saves a trust rule regardless of network_mode", async () => {
1974
- const spy = setupAddRuleSpy();
1975
-
1976
- // 1. Proxied bash always_allow -> rule IS saved
1977
- const p1 = makePrompterWithDecision(
1978
- "always_allow",
1979
- "bash:curl*",
1980
- "/tmp/project",
1981
- );
1982
- const e1 = new ToolExecutor(p1);
1983
- const r1 = await e1.execute(
1984
- "bash",
1985
- { command: "curl https://api.example.com", network_mode: "proxied" },
1986
- makeContext(),
1987
- );
1988
- expect(r1.isError).toBe(false);
1989
- expect(spy).toHaveBeenCalledTimes(1);
1990
- spy.mockClear();
1991
-
1992
- // 2. Non-proxied bash always_allow -> rule IS saved
1993
- const p2 = makePrompterWithDecision(
1994
- "always_allow",
1995
- "bash:git*",
1996
- "/tmp/project",
1997
- );
1998
- const e2 = new ToolExecutor(p2);
1999
- const r2 = await e2.execute("bash", { command: "git push" }, makeContext());
2000
- expect(r2.isError).toBe(false);
2001
- expect(spy).toHaveBeenCalledTimes(1);
2002
- });
765
+ // ── Workspace mode + forcePromptSideEffects interaction ──────────
2003
766
 
2004
- test("bash always_deny saves a deny rule regardless of network_mode", async () => {
2005
- const spy = setupAddRuleSpy();
767
+ test("workspace mode allow prompt promotion still works for side-effect tools under forcePromptSideEffects", async () => {
768
+ // Simulate workspace mode returning 'allow' for a workspace-scoped file_write
769
+ checkResultOverride = {
770
+ decision: "allow",
771
+ reason: "Workspace-scoped low-risk operation auto-allowed",
772
+ };
2006
773
 
2007
- const prompter = makePrompterWithDecision(
2008
- "always_deny",
2009
- "bash:rm*",
2010
- "/tmp/project",
2011
- );
2012
- const executor = new ToolExecutor(prompter);
774
+ const executor = new ToolExecutor(makeTrackingPrompter());
2013
775
  const result = await executor.execute(
2014
- "bash",
2015
- { command: "rm -rf /" },
2016
- makeContext(),
776
+ "file_write",
777
+ { path: "/tmp/project/test.txt", content: "data" },
778
+ makeContext({ forcePromptSideEffects: true }),
2017
779
  );
2018
780
 
2019
- expect(result.isError).toBe(true);
2020
- expect(spy).toHaveBeenCalledTimes(1);
2021
- expect(spy.mock.calls[0][0]).toBe("bash");
2022
- expect(spy.mock.calls[0][1]).toBe("bash:rm*");
2023
- expect(spy.mock.calls[0][3]).toBe("deny");
781
+ expect(result.isError).toBe(false);
782
+ // file_write is a side-effect tool, so forcePromptSideEffects must promote
783
+ // the workspace mode allow → prompt, requiring explicit user approval
784
+ expect(promptCalled).toBe(true);
2024
785
  });
2025
786
 
2026
- test('proxied bash denied result message includes "rule saved" suffix', async () => {
2027
- setupAddRuleSpy();
787
+ test("schedule_create forces prompt under forcePromptSideEffects", async () => {
788
+ checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
2028
789
 
2029
- const prompter = makePrompterWithDecision(
2030
- "always_deny",
2031
- "bash:*",
2032
- "/tmp/project",
2033
- );
2034
- const executor = new ToolExecutor(prompter);
790
+ const executor = new ToolExecutor(makeTrackingPrompter());
2035
791
  const result = await executor.execute(
2036
- "bash",
2037
- { command: "curl https://malicious.com", network_mode: "proxied" },
2038
- makeContext(),
792
+ "schedule_create",
793
+ {
794
+ name: "test schedule",
795
+ expression: "0 9 * * *",
796
+ message: "test",
797
+ },
798
+ makeContext({ forcePromptSideEffects: true }),
2039
799
  );
2040
800
 
2041
- expect(result.isError).toBe(true);
2042
- expect(result.content).toContain("Permission denied by user");
2043
- expect(result.content).toContain("rule was saved");
801
+ expect(result.isError).toBe(false);
802
+ expect(promptCalled).toBe(true);
2044
803
  });
2045
804
 
2046
- test('non-proxied bash denied result message includes "rule saved" suffix', async () => {
2047
- setupAddRuleSpy();
805
+ test("schedule_list does NOT force prompt under forcePromptSideEffects", async () => {
806
+ checkResultOverride = { decision: "allow", reason: "Matched trust rule" };
2048
807
 
2049
- const prompter = makePrompterWithDecision(
2050
- "always_deny",
2051
- "bash:rm*",
2052
- "/tmp/project",
2053
- );
2054
- const executor = new ToolExecutor(prompter);
808
+ const executor = new ToolExecutor(makeTrackingPrompter());
2055
809
  const result = await executor.execute(
2056
- "bash",
2057
- { command: "rm -rf /" },
2058
- makeContext(),
810
+ "schedule_list",
811
+ {},
812
+ makeContext({ forcePromptSideEffects: true }),
2059
813
  );
2060
814
 
2061
- expect(result.isError).toBe(true);
2062
- expect(result.content).toContain("Permission denied by user");
2063
- expect(result.content).toContain("rule was saved");
815
+ expect(result.isError).toBe(false);
816
+ // list is read-only — must NOT trigger forced prompting
817
+ expect(promptCalled).toBe(false);
2064
818
  });
2065
819
  });
2066
820
 
@@ -2136,146 +890,6 @@ describe("buildSanitizedEnv — baseline: credential exclusion", () => {
2136
890
  });
2137
891
  });
2138
892
 
2139
- // ---------------------------------------------------------------------------
2140
- // Persistent-allow lifecycle: roundtrip and auto-allow on subsequent invocation
2141
- // ---------------------------------------------------------------------------
2142
-
2143
- describe("ToolExecutor persistent-allow lifecycle", () => {
2144
- beforeEach(() => {
2145
- fakeToolResult = { content: "ok", isError: false };
2146
- lastCheckArgs = undefined;
2147
- getToolOverride = undefined;
2148
- checkResultOverride = undefined;
2149
- checkFnOverride = undefined;
2150
- cachedAssessmentOverride = undefined;
2151
- if (addRuleSpy) {
2152
- addRuleSpy.mockRestore();
2153
- addRuleSpy = undefined;
2154
- }
2155
- });
2156
-
2157
- function setupAddRuleSpy() {
2158
- addRuleSpy = spyOn(trustStore, "addRule").mockImplementation(
2159
- (
2160
- tool: string,
2161
- pattern: string,
2162
- scope: string,
2163
- decision = "allow",
2164
- priority = 100,
2165
- options?: { executionTarget?: string },
2166
- ) => {
2167
- return {
2168
- id: "spy-rule-id",
2169
- tool,
2170
- pattern,
2171
- scope,
2172
- decision,
2173
- priority,
2174
- createdAt: Date.now(),
2175
- ...options,
2176
- } as TrustRule;
2177
- },
2178
- );
2179
- return addRuleSpy;
2180
- }
2181
-
2182
- test("persistent-allow roundtrip: always_allow saves rule and allows tool", async () => {
2183
- // Simulate check() returning 'prompt' so the executor asks the user
2184
- checkResultOverride = {
2185
- decision: "prompt",
2186
- reason: "Medium risk: requires approval",
2187
- };
2188
- const spy = setupAddRuleSpy();
2189
-
2190
- // User responds with always_allow, selecting a pattern and scope
2191
- const prompter = makePrompterWithDecision(
2192
- "always_allow",
2193
- "git *",
2194
- "/tmp/project",
2195
- );
2196
- const executor = new ToolExecutor(prompter);
2197
- const result = await executor.execute(
2198
- "bash",
2199
- { command: "git status" },
2200
- makeContext(),
2201
- );
2202
-
2203
- // The tool should have been allowed to proceed
2204
- expect(result.isError).toBe(false);
2205
- expect(result.content).toBe("ok");
2206
-
2207
- // addRule should have been called with the correct arguments
2208
- expect(spy).toHaveBeenCalledTimes(1);
2209
- const [tool, pattern, scope, decision] = spy.mock.calls[0];
2210
- expect(tool).toBe("bash");
2211
- expect(pattern).toBe("git *");
2212
- expect(scope).toBe("/tmp/project");
2213
- expect(decision).toBe("allow");
2214
- });
2215
-
2216
- test("auto-allow on subsequent invocation: matching rule skips prompt", async () => {
2217
- // Simulate a previously saved rule by making check() return 'allow'
2218
- // with a matched rule (as findHighestPriorityRule would).
2219
- checkResultOverride = {
2220
- decision: "allow",
2221
- reason: "Matched trust rule: git *",
2222
- };
2223
-
2224
- let promptCalled = false;
2225
- const trackingPrompter = {
2226
- prompt: async () => {
2227
- promptCalled = true;
2228
- return { decision: "allow" as const };
2229
- },
2230
- resolveConfirmation: () => {},
2231
- updateSender: () => {},
2232
- dispose: () => {},
2233
- } as unknown as PermissionPrompter;
2234
-
2235
- const executor = new ToolExecutor(trackingPrompter);
2236
- const result = await executor.execute(
2237
- "bash",
2238
- { command: "git status" },
2239
- makeContext(),
2240
- );
2241
-
2242
- // The tool should be auto-allowed
2243
- expect(result.isError).toBe(false);
2244
- expect(result.content).toBe("ok");
2245
-
2246
- // The prompter should NOT have been called — the rule auto-allowed
2247
- expect(promptCalled).toBe(false);
2248
- });
2249
-
2250
- test("always_allow with everywhere scope saves rule and allows tool", async () => {
2251
- checkResultOverride = {
2252
- decision: "prompt",
2253
- reason: "Medium risk: requires approval",
2254
- };
2255
- const spy = setupAddRuleSpy();
2256
-
2257
- const prompter = makePrompterWithDecision(
2258
- "always_allow",
2259
- "file_write:*",
2260
- "everywhere",
2261
- );
2262
- const executor = new ToolExecutor(prompter);
2263
- const result = await executor.execute(
2264
- "file_write",
2265
- { path: "/tmp/test.txt", content: "hello" },
2266
- makeContext(),
2267
- );
2268
-
2269
- expect(result.isError).toBe(false);
2270
- expect(spy).toHaveBeenCalledTimes(1);
2271
- const [tool, pattern, scope, decision] = spy.mock.calls[0];
2272
- expect(tool).toBe("file_write");
2273
- expect(pattern).toBe("file_write:*");
2274
- expect(scope).toBe("everywhere");
2275
- expect(decision).toBe("allow");
2276
- });
2277
- });
2278
-
2279
893
  describe("integration regressions — prompt payload (PR 11)", () => {
2280
894
  beforeEach(() => {
2281
895
  fakeToolResult = { content: "ok", isError: false };
@@ -2310,7 +924,7 @@ describe("integration regressions — prompt payload (PR 11)", () => {
2310
924
  } as unknown as PermissionPrompter;
2311
925
 
2312
926
  const executor = new ToolExecutor(prompter);
2313
- await executor.execute("bash", { command: "npm install" }, makeContext());
927
+ await executor.execute("bash", { command: "npm install" }, makeContext({ forcePromptSideEffects: true }));
2314
928
 
2315
929
  // Verify that the prompter received allowlist options
2316
930
  expect(capturedAllowlist).toBeDefined();
@@ -2339,10 +953,6 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
2339
953
  checkResultOverride = undefined;
2340
954
  checkFnOverride = undefined;
2341
955
  cachedAssessmentOverride = undefined;
2342
- if (addRuleSpy) {
2343
- addRuleSpy.mockRestore();
2344
- addRuleSpy = undefined;
2345
- }
2346
956
  });
2347
957
 
2348
958
  test("auto-approved tool result includes risk metadata when classifier assessment exists", async () => {
@@ -2360,7 +970,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
2360
970
  const result = await executor.execute(
2361
971
  "file_read",
2362
972
  { path: "README.md" },
2363
- makeContext(),
973
+ makeContext({ requireFreshApproval: true }),
2364
974
  );
2365
975
 
2366
976
  expect(result.isError).toBe(false);
@@ -2403,7 +1013,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
2403
1013
  const result = await executor.execute(
2404
1014
  "bash",
2405
1015
  { command: "rm -rf /" },
2406
- makeContext(),
1016
+ makeContext({ requireFreshApproval: true }),
2407
1017
  );
2408
1018
 
2409
1019
  expect(result.isError).toBe(true);
@@ -2433,7 +1043,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
2433
1043
  const result = await executor.execute(
2434
1044
  "bash",
2435
1045
  { command: "npm install lodash" },
2436
- makeContext(),
1046
+ makeContext({ requireFreshApproval: true }),
2437
1047
  );
2438
1048
 
2439
1049
  expect(result.isError).toBe(false);
@@ -2442,4 +1052,83 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
2442
1052
  expect(result.riskReason).toBe("Package manager installation");
2443
1053
  expect(result.riskScopeOptions).toHaveLength(2);
2444
1054
  });
1055
+
1056
+ test("tool result includes riskDirectoryScopeOptions when classifier emits directoryScopeOptions", async () => {
1057
+ cachedAssessmentOverride = {
1058
+ riskLevel: "medium",
1059
+ reason: "Writes to file in workspace",
1060
+ scopeOptions: [
1061
+ {
1062
+ pattern: "file_write:/workspace/scratch/out.txt",
1063
+ label: "This file only",
1064
+ },
1065
+ ],
1066
+ directoryScopeOptions: [
1067
+ { scope: "/workspace/scratch", label: "In scratch/" },
1068
+ { scope: "/workspace", label: "Anywhere in workspace/" },
1069
+ { scope: "everywhere", label: "Everywhere" },
1070
+ ],
1071
+ matchType: "registry",
1072
+ };
1073
+
1074
+ const executor = new ToolExecutor(makePrompter());
1075
+ const result = await executor.execute(
1076
+ "file_read",
1077
+ { path: "/workspace/scratch/out.txt" },
1078
+ makeContext({ requireFreshApproval: true }),
1079
+ );
1080
+
1081
+ expect(result.isError).toBe(false);
1082
+ expect(result.riskDirectoryScopeOptions).toEqual([
1083
+ { scope: "/workspace/scratch", label: "In scratch/" },
1084
+ { scope: "/workspace", label: "Anywhere in workspace/" },
1085
+ { scope: "everywhere", label: "Everywhere" },
1086
+ ]);
1087
+ });
1088
+
1089
+ test("tool result omits riskDirectoryScopeOptions when classifier does not emit directoryScopeOptions", async () => {
1090
+ cachedAssessmentOverride = {
1091
+ riskLevel: "low",
1092
+ reason: "Read-only operation",
1093
+ scopeOptions: [],
1094
+ // directoryScopeOptions intentionally omitted
1095
+ matchType: "registry",
1096
+ };
1097
+
1098
+ const executor = new ToolExecutor(makePrompter());
1099
+ const result = await executor.execute(
1100
+ "file_read",
1101
+ { path: "README.md" },
1102
+ makeContext(),
1103
+ );
1104
+
1105
+ expect(result.isError).toBe(false);
1106
+ expect(result.riskDirectoryScopeOptions).toBeUndefined();
1107
+ });
1108
+
1109
+ test("riskScopeOptions and riskDirectoryScopeOptions are independent — one does not clobber the other", async () => {
1110
+ cachedAssessmentOverride = {
1111
+ riskLevel: "medium",
1112
+ reason: "Filesystem write",
1113
+ scopeOptions: [
1114
+ { pattern: "file_write:/tmp/foo.txt", label: "This file" },
1115
+ ],
1116
+ directoryScopeOptions: [{ scope: "/tmp", label: "Anywhere in tmp/" }],
1117
+ matchType: "registry",
1118
+ };
1119
+
1120
+ const executor = new ToolExecutor(makePrompter());
1121
+ const result = await executor.execute(
1122
+ "file_read",
1123
+ { path: "/tmp/foo.txt" },
1124
+ makeContext({ requireFreshApproval: true }),
1125
+ );
1126
+
1127
+ expect(result.riskScopeOptions).toEqual([
1128
+ { pattern: "file_write:/tmp/foo.txt", label: "This file" },
1129
+ ]);
1130
+ expect(result.riskDirectoryScopeOptions).toEqual([
1131
+ { scope: "/tmp", label: "Anywhere in tmp/" },
1132
+ ]);
1133
+ });
2445
1134
  });