@vellumai/assistant 0.3.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 (1068) hide show
  1. package/.dockerignore +27 -0
  2. package/.env.example +22 -0
  3. package/Dockerfile +99 -0
  4. package/Dockerfile.sandbox +5 -0
  5. package/README.md +248 -0
  6. package/bun.lock +1723 -0
  7. package/bunfig.toml +2 -0
  8. package/docs/skills.md +158 -0
  9. package/drizzle/0000_dizzy_maggott.sql +301 -0
  10. package/drizzle/meta/0000_snapshot.json +1999 -0
  11. package/drizzle/meta/_journal.json +13 -0
  12. package/drizzle.config.ts +7 -0
  13. package/eslint.config.mjs +17 -0
  14. package/hook-templates/debug-prompt-logger/hook.json +7 -0
  15. package/hook-templates/debug-prompt-logger/run.sh +68 -0
  16. package/knip.json +9 -0
  17. package/package.json +70 -0
  18. package/scripts/capture-x-graphql.ts +545 -0
  19. package/scripts/ipc/check-contract-inventory.ts +104 -0
  20. package/scripts/ipc/check-swift-decoder-drift.ts +166 -0
  21. package/scripts/ipc/generate-swift.ts +492 -0
  22. package/scripts/test-filesystem-tools.sh +48 -0
  23. package/scripts/test.sh +127 -0
  24. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2485 -0
  25. package/src/__tests__/account-registry.test.ts +245 -0
  26. package/src/__tests__/active-skill-tools.test.ts +378 -0
  27. package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
  28. package/src/__tests__/agent-loop-thinking.test.ts +81 -0
  29. package/src/__tests__/agent-loop.test.ts +1135 -0
  30. package/src/__tests__/anthropic-provider.test.ts +778 -0
  31. package/src/__tests__/app-builder-tool-scripts.test.ts +290 -0
  32. package/src/__tests__/app-bundler.test.ts +292 -0
  33. package/src/__tests__/app-executors.test.ts +613 -0
  34. package/src/__tests__/app-git-history.test.ts +176 -0
  35. package/src/__tests__/app-git-service.test.ts +169 -0
  36. package/src/__tests__/app-open-proxy.test.ts +62 -0
  37. package/src/__tests__/asset-materialize-tool.test.ts +452 -0
  38. package/src/__tests__/asset-search-tool.test.ts +477 -0
  39. package/src/__tests__/assistant-attachment-directive.test.ts +401 -0
  40. package/src/__tests__/assistant-attachments.test.ts +437 -0
  41. package/src/__tests__/assistant-event-hub.test.ts +226 -0
  42. package/src/__tests__/assistant-event.test.ts +123 -0
  43. package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
  44. package/src/__tests__/attachments-store.test.ts +476 -0
  45. package/src/__tests__/attachments.test.ts +134 -0
  46. package/src/__tests__/audit-log-rotation.test.ts +154 -0
  47. package/src/__tests__/browser-fill-credential.test.ts +309 -0
  48. package/src/__tests__/browser-manager.test.ts +203 -0
  49. package/src/__tests__/browser-runtime-check.test.ts +55 -0
  50. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +68 -0
  51. package/src/__tests__/browser-skill-endstate.test.ts +195 -0
  52. package/src/__tests__/bundle-scanner.test.ts +313 -0
  53. package/src/__tests__/call-bridge.test.ts +517 -0
  54. package/src/__tests__/call-constants.test.ts +40 -0
  55. package/src/__tests__/call-domain.test.ts +163 -0
  56. package/src/__tests__/call-orchestrator.test.ts +625 -0
  57. package/src/__tests__/call-recovery.test.ts +518 -0
  58. package/src/__tests__/call-routes-http.test.ts +699 -0
  59. package/src/__tests__/call-state-machine.test.ts +143 -0
  60. package/src/__tests__/call-state.test.ts +174 -0
  61. package/src/__tests__/call-store.test.ts +691 -0
  62. package/src/__tests__/channel-approval-routes.test.ts +2356 -0
  63. package/src/__tests__/channel-approval.test.ts +299 -0
  64. package/src/__tests__/channel-approvals.test.ts +521 -0
  65. package/src/__tests__/channel-delivery-store.test.ts +447 -0
  66. package/src/__tests__/channel-guardian.test.ts +1005 -0
  67. package/src/__tests__/checker.test.ts +3519 -0
  68. package/src/__tests__/clarification-resolver.test.ts +159 -0
  69. package/src/__tests__/classifier.test.ts +67 -0
  70. package/src/__tests__/claude-code-skill-regression.test.ts +127 -0
  71. package/src/__tests__/claude-code-tool-profiles.test.ts +88 -0
  72. package/src/__tests__/cli-discover.test.ts +85 -0
  73. package/src/__tests__/cli.test.ts +26 -0
  74. package/src/__tests__/clipboard.test.ts +80 -0
  75. package/src/__tests__/commit-guarantee.test.ts +335 -0
  76. package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
  77. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  78. package/src/__tests__/computer-use-session-compaction.test.ts +132 -0
  79. package/src/__tests__/computer-use-session-lifecycle.test.ts +293 -0
  80. package/src/__tests__/computer-use-session-working-dir.test.ts +117 -0
  81. package/src/__tests__/computer-use-skill-baseline.test.ts +74 -0
  82. package/src/__tests__/computer-use-skill-endstate.test.ts +89 -0
  83. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +217 -0
  84. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +107 -0
  85. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +54 -0
  86. package/src/__tests__/computer-use-tools.test.ts +250 -0
  87. package/src/__tests__/config-schema.test.ts +1462 -0
  88. package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
  89. package/src/__tests__/conflict-policy.test.ts +121 -0
  90. package/src/__tests__/conflict-store.test.ts +332 -0
  91. package/src/__tests__/connection-policy.test.ts +102 -0
  92. package/src/__tests__/contacts-tools.test.ts +331 -0
  93. package/src/__tests__/context-memory-e2e.test.ts +434 -0
  94. package/src/__tests__/context-token-estimator.test.ts +135 -0
  95. package/src/__tests__/context-window-manager.test.ts +376 -0
  96. package/src/__tests__/contradiction-checker.test.ts +314 -0
  97. package/src/__tests__/conversation-store.test.ts +612 -0
  98. package/src/__tests__/credential-broker-browser-fill.test.ts +517 -0
  99. package/src/__tests__/credential-broker-server-use.test.ts +554 -0
  100. package/src/__tests__/credential-broker.test.ts +167 -0
  101. package/src/__tests__/credential-host-pattern-match.test.ts +104 -0
  102. package/src/__tests__/credential-metadata-store.test.ts +779 -0
  103. package/src/__tests__/credential-policy-validate.test.ts +121 -0
  104. package/src/__tests__/credential-resolve.test.ts +328 -0
  105. package/src/__tests__/credential-security-e2e.test.ts +352 -0
  106. package/src/__tests__/credential-security-invariants.test.ts +583 -0
  107. package/src/__tests__/credential-selection.test.ts +354 -0
  108. package/src/__tests__/credential-vault-unit.test.ts +780 -0
  109. package/src/__tests__/credential-vault.test.ts +852 -0
  110. package/src/__tests__/daemon-assistant-events.test.ts +164 -0
  111. package/src/__tests__/daemon-server-session-init.test.ts +522 -0
  112. package/src/__tests__/date-context.test.ts +373 -0
  113. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  114. package/src/__tests__/delete-managed-skill-tool.test.ts +97 -0
  115. package/src/__tests__/diff.test.ts +121 -0
  116. package/src/__tests__/domain-normalize.test.ts +112 -0
  117. package/src/__tests__/domain-policy.test.ts +124 -0
  118. package/src/__tests__/doordash-client.test.ts +186 -0
  119. package/src/__tests__/doordash-session.test.ts +152 -0
  120. package/src/__tests__/dynamic-page-surface.test.ts +91 -0
  121. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +132 -0
  122. package/src/__tests__/edit-engine.test.ts +180 -0
  123. package/src/__tests__/elevenlabs-client.test.ts +271 -0
  124. package/src/__tests__/email-cli.test.ts +283 -0
  125. package/src/__tests__/encrypted-store.test.ts +332 -0
  126. package/src/__tests__/entity-extractor.test.ts +190 -0
  127. package/src/__tests__/ephemeral-permissions.test.ts +362 -0
  128. package/src/__tests__/evaluate-typescript-tool.test.ts +286 -0
  129. package/src/__tests__/event-bus.test.ts +222 -0
  130. package/src/__tests__/file-edit-tool.test.ts +122 -0
  131. package/src/__tests__/file-ops-service.test.ts +330 -0
  132. package/src/__tests__/file-read-tool.test.ts +75 -0
  133. package/src/__tests__/file-write-tool.test.ts +113 -0
  134. package/src/__tests__/filesystem-tools.test.ts +579 -0
  135. package/src/__tests__/fixtures/credential-security-fixtures.ts +181 -0
  136. package/src/__tests__/fixtures/media-reuse-fixtures.ts +126 -0
  137. package/src/__tests__/fixtures/mock-signup-server.ts +387 -0
  138. package/src/__tests__/fixtures/proxy-fixtures.ts +147 -0
  139. package/src/__tests__/followup-tools.test.ts +303 -0
  140. package/src/__tests__/forbidden-legacy-symbols.test.ts +71 -0
  141. package/src/__tests__/fuzzy-match-property.test.ts +216 -0
  142. package/src/__tests__/fuzzy-match.test.ts +138 -0
  143. package/src/__tests__/gateway-only-enforcement.test.ts +631 -0
  144. package/src/__tests__/gemini-image-service.test.ts +261 -0
  145. package/src/__tests__/gemini-provider.test.ts +651 -0
  146. package/src/__tests__/get-weather.test.ts +318 -0
  147. package/src/__tests__/gmail-integration.test.ts +73 -0
  148. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
  149. package/src/__tests__/handlers-cu-observation-blob.test.ts +352 -0
  150. package/src/__tests__/handlers-ipc-blob-probe.test.ts +191 -0
  151. package/src/__tests__/handlers-slack-config.test.ts +200 -0
  152. package/src/__tests__/handlers-task-submit-slash.test.ts +38 -0
  153. package/src/__tests__/handlers-telegram-config.test.ts +968 -0
  154. package/src/__tests__/handlers-twilio-config.test.ts +659 -0
  155. package/src/__tests__/handlers-twitter-config.test.ts +858 -0
  156. package/src/__tests__/headless-browser-interactions.test.ts +536 -0
  157. package/src/__tests__/headless-browser-navigate.test.ts +211 -0
  158. package/src/__tests__/headless-browser-read-tools.test.ts +261 -0
  159. package/src/__tests__/headless-browser-snapshot.test.ts +185 -0
  160. package/src/__tests__/history-repair-observability.test.ts +56 -0
  161. package/src/__tests__/history-repair.test.ts +510 -0
  162. package/src/__tests__/home-base-bootstrap.test.ts +82 -0
  163. package/src/__tests__/hooks-blocking.test.ts +128 -0
  164. package/src/__tests__/hooks-cli.test.ts +144 -0
  165. package/src/__tests__/hooks-config.test.ts +93 -0
  166. package/src/__tests__/hooks-discovery.test.ts +199 -0
  167. package/src/__tests__/hooks-integration.test.ts +189 -0
  168. package/src/__tests__/hooks-manager.test.ts +187 -0
  169. package/src/__tests__/hooks-runner.test.ts +182 -0
  170. package/src/__tests__/hooks-settings.test.ts +154 -0
  171. package/src/__tests__/hooks-templates.test.ts +137 -0
  172. package/src/__tests__/hooks-ts-runner.test.ts +125 -0
  173. package/src/__tests__/hooks-watch.test.ts +100 -0
  174. package/src/__tests__/host-file-edit-tool.test.ts +228 -0
  175. package/src/__tests__/host-file-read-tool.test.ts +123 -0
  176. package/src/__tests__/host-file-write-tool.test.ts +136 -0
  177. package/src/__tests__/host-shell-tool.test.ts +562 -0
  178. package/src/__tests__/ingress-reconcile.test.ts +581 -0
  179. package/src/__tests__/ingress-url-consistency.test.ts +214 -0
  180. package/src/__tests__/intent-routing.test.ts +259 -0
  181. package/src/__tests__/ipc-blob-store.test.ts +315 -0
  182. package/src/__tests__/ipc-contract-inventory.test.ts +54 -0
  183. package/src/__tests__/ipc-contract.test.ts +74 -0
  184. package/src/__tests__/ipc-protocol.test.ts +113 -0
  185. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  186. package/src/__tests__/ipc-snapshot.test.ts +1769 -0
  187. package/src/__tests__/ipc-validate.test.ts +407 -0
  188. package/src/__tests__/key-migration.test.ts +206 -0
  189. package/src/__tests__/keychain.test.ts +258 -0
  190. package/src/__tests__/llm-usage-store.test.ts +221 -0
  191. package/src/__tests__/managed-skill-lifecycle.test.ts +257 -0
  192. package/src/__tests__/managed-store.test.ts +608 -0
  193. package/src/__tests__/media-generate-image.test.ts +238 -0
  194. package/src/__tests__/media-reuse-story.e2e.test.ts +676 -0
  195. package/src/__tests__/media-visibility-policy.test.ts +141 -0
  196. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +235 -0
  197. package/src/__tests__/memory-lifecycle-e2e.test.ts +481 -0
  198. package/src/__tests__/memory-query-builder.test.ts +59 -0
  199. package/src/__tests__/memory-recall-quality.test.ts +846 -0
  200. package/src/__tests__/memory-regressions.experimental.test.ts +538 -0
  201. package/src/__tests__/memory-regressions.test.ts +4435 -0
  202. package/src/__tests__/memory-retrieval-budget.test.ts +49 -0
  203. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  204. package/src/__tests__/migration-cli-flows.test.ts +169 -0
  205. package/src/__tests__/migration-ordering.test.ts +249 -0
  206. package/src/__tests__/mock-signup-server.test.ts +528 -0
  207. package/src/__tests__/oauth-callback-registry.test.ts +92 -0
  208. package/src/__tests__/oauth2-gateway-transport.test.ts +285 -0
  209. package/src/__tests__/onboarding-starter-tasks.test.ts +176 -0
  210. package/src/__tests__/onboarding-template-contract.test.ts +58 -0
  211. package/src/__tests__/openai-provider.test.ts +753 -0
  212. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  213. package/src/__tests__/parser.test.ts +472 -0
  214. package/src/__tests__/path-classifier.test.ts +73 -0
  215. package/src/__tests__/path-policy.test.ts +435 -0
  216. package/src/__tests__/platform-move-helper.test.ts +99 -0
  217. package/src/__tests__/platform-socket-path.test.ts +52 -0
  218. package/src/__tests__/platform-workspace-migration.test.ts +1000 -0
  219. package/src/__tests__/platform.test.ts +131 -0
  220. package/src/__tests__/playbook-execution.test.ts +502 -0
  221. package/src/__tests__/playbook-tools.test.ts +340 -0
  222. package/src/__tests__/prebuilt-home-base-seed.test.ts +75 -0
  223. package/src/__tests__/pricing.test.ts +256 -0
  224. package/src/__tests__/profile-compiler.test.ts +374 -0
  225. package/src/__tests__/provider-commit-message-generator.test.ts +342 -0
  226. package/src/__tests__/provider-registry-ollama.test.ts +16 -0
  227. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  228. package/src/__tests__/proxy-approval-callback.test.ts +601 -0
  229. package/src/__tests__/public-ingress-urls.test.ts +256 -0
  230. package/src/__tests__/qdrant-manager.test.ts +267 -0
  231. package/src/__tests__/ratelimit.test.ts +297 -0
  232. package/src/__tests__/recurrence-engine-rruleset.test.ts +175 -0
  233. package/src/__tests__/recurrence-engine.test.ts +78 -0
  234. package/src/__tests__/recurrence-types.test.ts +79 -0
  235. package/src/__tests__/registry.test.ts +494 -0
  236. package/src/__tests__/relay-server.test.ts +688 -0
  237. package/src/__tests__/reminder-store.test.ts +223 -0
  238. package/src/__tests__/reminder.test.ts +229 -0
  239. package/src/__tests__/request-file-tool.test.ts +158 -0
  240. package/src/__tests__/run-orchestrator-assistant-events.test.ts +227 -0
  241. package/src/__tests__/run-orchestrator.test.ts +425 -0
  242. package/src/__tests__/runtime-attachment-metadata.test.ts +189 -0
  243. package/src/__tests__/runtime-events-sse-parity.test.ts +343 -0
  244. package/src/__tests__/runtime-events-sse.test.ts +162 -0
  245. package/src/__tests__/runtime-runs-http.test.ts +438 -0
  246. package/src/__tests__/runtime-runs.test.ts +260 -0
  247. package/src/__tests__/sandbox-diagnostics.test.ts +408 -0
  248. package/src/__tests__/sandbox-host-parity.test.ts +950 -0
  249. package/src/__tests__/scaffold-managed-skill-tool.test.ts +253 -0
  250. package/src/__tests__/schedule-store.test.ts +484 -0
  251. package/src/__tests__/schedule-tools.test.ts +783 -0
  252. package/src/__tests__/scheduler-recurrence.test.ts +430 -0
  253. package/src/__tests__/script-proxy-certs.test.ts +90 -0
  254. package/src/__tests__/script-proxy-connect-tunnel.test.ts +177 -0
  255. package/src/__tests__/script-proxy-decision-trace.test.ts +156 -0
  256. package/src/__tests__/script-proxy-http-forwarder.test.ts +281 -0
  257. package/src/__tests__/script-proxy-injection-runtime.test.ts +401 -0
  258. package/src/__tests__/script-proxy-mitm-handler.test.ts +407 -0
  259. package/src/__tests__/script-proxy-policy-runtime.test.ts +287 -0
  260. package/src/__tests__/script-proxy-policy.test.ts +310 -0
  261. package/src/__tests__/script-proxy-rewrite-specificity.test.ts +135 -0
  262. package/src/__tests__/script-proxy-router.test.ts +180 -0
  263. package/src/__tests__/script-proxy-session-manager.test.ts +382 -0
  264. package/src/__tests__/script-proxy-session-runtime.test.ts +113 -0
  265. package/src/__tests__/secret-allowlist.test.ts +230 -0
  266. package/src/__tests__/secret-ingress-handler.test.ts +110 -0
  267. package/src/__tests__/secret-onetime-send.test.ts +130 -0
  268. package/src/__tests__/secret-prompt-log-hygiene.test.ts +106 -0
  269. package/src/__tests__/secret-response-routing.test.ts +93 -0
  270. package/src/__tests__/secret-scanner-executor.test.ts +348 -0
  271. package/src/__tests__/secret-scanner.test.ts +900 -0
  272. package/src/__tests__/secure-keys.test.ts +323 -0
  273. package/src/__tests__/server-history-render.test.ts +431 -0
  274. package/src/__tests__/session-abort-tool-results.test.ts +240 -0
  275. package/src/__tests__/session-conflict-gate.test.ts +1136 -0
  276. package/src/__tests__/session-error.test.ts +369 -0
  277. package/src/__tests__/session-evictor.test.ts +188 -0
  278. package/src/__tests__/session-init.benchmark.test.ts +465 -0
  279. package/src/__tests__/session-load-history-repair.test.ts +222 -0
  280. package/src/__tests__/session-pre-run-repair.test.ts +213 -0
  281. package/src/__tests__/session-process-bridge.test.ts +242 -0
  282. package/src/__tests__/session-profile-injection.test.ts +444 -0
  283. package/src/__tests__/session-provider-retry-repair.test.ts +306 -0
  284. package/src/__tests__/session-queue.test.ts +1535 -0
  285. package/src/__tests__/session-runtime-assembly.test.ts +476 -0
  286. package/src/__tests__/session-runtime-workspace.test.ts +183 -0
  287. package/src/__tests__/session-skill-tools.test.ts +2431 -0
  288. package/src/__tests__/session-slash-known.test.ts +368 -0
  289. package/src/__tests__/session-slash-queue.test.ts +288 -0
  290. package/src/__tests__/session-slash-unknown.test.ts +271 -0
  291. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  292. package/src/__tests__/session-tool-setup-app-refresh.test.ts +473 -0
  293. package/src/__tests__/session-tool-setup-memory-scope.test.ts +140 -0
  294. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +140 -0
  295. package/src/__tests__/session-undo.test.ts +75 -0
  296. package/src/__tests__/session-workspace-cache-state.test.ts +246 -0
  297. package/src/__tests__/session-workspace-injection.test.ts +327 -0
  298. package/src/__tests__/session-workspace-tool-tracking.test.ts +240 -0
  299. package/src/__tests__/shared-filesystem-errors.test.ts +78 -0
  300. package/src/__tests__/shell-credential-ref.test.ts +187 -0
  301. package/src/__tests__/shell-identity.test.ts +256 -0
  302. package/src/__tests__/shell-parser-fuzz.test.ts +544 -0
  303. package/src/__tests__/shell-parser-property.test.ts +433 -0
  304. package/src/__tests__/shell-tool-proxy-mode.test.ts +272 -0
  305. package/src/__tests__/signup-e2e.test.ts +353 -0
  306. package/src/__tests__/size-guard.test.ts +117 -0
  307. package/src/__tests__/skill-include-graph.test.ts +303 -0
  308. package/src/__tests__/skill-load-tool.test.ts +409 -0
  309. package/src/__tests__/skill-projection.benchmark.test.ts +338 -0
  310. package/src/__tests__/skill-script-runner-host.test.ts +489 -0
  311. package/src/__tests__/skill-script-runner-sandbox.test.ts +349 -0
  312. package/src/__tests__/skill-script-runner.test.ts +159 -0
  313. package/src/__tests__/skill-tool-factory.test.ts +252 -0
  314. package/src/__tests__/skill-tool-manifest.test.ts +658 -0
  315. package/src/__tests__/skill-version-hash.test.ts +182 -0
  316. package/src/__tests__/skills.test.ts +680 -0
  317. package/src/__tests__/slash-commands-catalog.test.ts +86 -0
  318. package/src/__tests__/slash-commands-parser.test.ts +119 -0
  319. package/src/__tests__/slash-commands-resolver.test.ts +193 -0
  320. package/src/__tests__/slash-commands-rewrite.test.ts +39 -0
  321. package/src/__tests__/speaker-identification.test.ts +52 -0
  322. package/src/__tests__/starter-bundle.test.ts +136 -0
  323. package/src/__tests__/starter-task-flow.test.ts +143 -0
  324. package/src/__tests__/subagent-manager-notify.test.ts +404 -0
  325. package/src/__tests__/subagent-tools.test.ts +801 -0
  326. package/src/__tests__/subagent-types.test.ts +78 -0
  327. package/src/__tests__/swarm-orchestrator.test.ts +428 -0
  328. package/src/__tests__/swarm-plan-validator.test.ts +330 -0
  329. package/src/__tests__/swarm-recursion.test.ts +165 -0
  330. package/src/__tests__/swarm-router-planner.test.ts +208 -0
  331. package/src/__tests__/swarm-session-integration.test.ts +274 -0
  332. package/src/__tests__/swarm-tool.test.ts +145 -0
  333. package/src/__tests__/swarm-worker-backend.test.ts +129 -0
  334. package/src/__tests__/swarm-worker-runner.test.ts +272 -0
  335. package/src/__tests__/system-prompt.test.ts +439 -0
  336. package/src/__tests__/task-compiler.test.ts +284 -0
  337. package/src/__tests__/task-management-tools.test.ts +936 -0
  338. package/src/__tests__/task-runner.test.ts +216 -0
  339. package/src/__tests__/task-scheduler.test.ts +217 -0
  340. package/src/__tests__/task-tools.test.ts +595 -0
  341. package/src/__tests__/terminal-sandbox-docker.test.ts +1064 -0
  342. package/src/__tests__/terminal-sandbox.integration.test.ts +178 -0
  343. package/src/__tests__/terminal-sandbox.test.ts +202 -0
  344. package/src/__tests__/terminal-tools.test.ts +840 -0
  345. package/src/__tests__/test-support/browser-skill-harness.ts +90 -0
  346. package/src/__tests__/test-support/computer-use-skill-harness.ts +45 -0
  347. package/src/__tests__/tool-audit-listener.test.ts +113 -0
  348. package/src/__tests__/tool-domain-event-publisher.test.ts +253 -0
  349. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  350. package/src/__tests__/tool-executor-lifecycle-events.test.ts +516 -0
  351. package/src/__tests__/tool-executor-redaction.test.ts +289 -0
  352. package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
  353. package/src/__tests__/tool-executor.test.ts +1989 -0
  354. package/src/__tests__/tool-metrics-listener.test.ts +225 -0
  355. package/src/__tests__/tool-notification-listener.test.ts +49 -0
  356. package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
  357. package/src/__tests__/tool-policy.test.ts +54 -0
  358. package/src/__tests__/tool-profiling-listener.test.ts +268 -0
  359. package/src/__tests__/tool-result-truncation.test.ts +217 -0
  360. package/src/__tests__/tool-trace-listener.test.ts +226 -0
  361. package/src/__tests__/top-level-renderer.test.ts +121 -0
  362. package/src/__tests__/top-level-scanner.test.ts +141 -0
  363. package/src/__tests__/trace-emitter.test.ts +173 -0
  364. package/src/__tests__/trust-store.test.ts +1605 -0
  365. package/src/__tests__/turn-commit.test.ts +554 -0
  366. package/src/__tests__/twilio-provider.test.ts +329 -0
  367. package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
  368. package/src/__tests__/twilio-routes-twiml.test.ts +127 -0
  369. package/src/__tests__/twilio-routes.test.ts +577 -0
  370. package/src/__tests__/twitter-auth-handler.test.ts +667 -0
  371. package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
  372. package/src/__tests__/twitter-cli-routing.test.ts +252 -0
  373. package/src/__tests__/twitter-oauth-client.test.ts +209 -0
  374. package/src/__tests__/url-safety.test.ts +418 -0
  375. package/src/__tests__/view-image-tool.test.ts +217 -0
  376. package/src/__tests__/weather-skill-regression.test.ts +225 -0
  377. package/src/__tests__/web-fetch.test.ts +869 -0
  378. package/src/__tests__/web-search.test.ts +584 -0
  379. package/src/__tests__/workspace-git-service.test.ts +1153 -0
  380. package/src/__tests__/workspace-heartbeat-service.test.ts +486 -0
  381. package/src/__tests__/workspace-lifecycle.test.ts +292 -0
  382. package/src/__tests__/workspace-policy.test.ts +213 -0
  383. package/src/agent/attachments.ts +35 -0
  384. package/src/agent/loop.ts +500 -0
  385. package/src/agent/message-types.ts +17 -0
  386. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  387. package/src/autonomy/autonomy-resolver.ts +60 -0
  388. package/src/autonomy/autonomy-store.ts +122 -0
  389. package/src/autonomy/disposition-mapper.ts +31 -0
  390. package/src/autonomy/index.ts +11 -0
  391. package/src/autonomy/types.ts +39 -0
  392. package/src/bundler/app-bundler.ts +295 -0
  393. package/src/bundler/bundle-scanner.ts +535 -0
  394. package/src/bundler/bundle-signer.ts +124 -0
  395. package/src/bundler/manifest.ts +21 -0
  396. package/src/bundler/signature-verifier.ts +184 -0
  397. package/src/calls/call-bridge.ts +168 -0
  398. package/src/calls/call-constants.ts +48 -0
  399. package/src/calls/call-domain.ts +430 -0
  400. package/src/calls/call-orchestrator.ts +498 -0
  401. package/src/calls/call-recovery.ts +207 -0
  402. package/src/calls/call-state-machine.ts +68 -0
  403. package/src/calls/call-state.ts +87 -0
  404. package/src/calls/call-store.ts +422 -0
  405. package/src/calls/elevenlabs-client.ts +97 -0
  406. package/src/calls/elevenlabs-config.ts +31 -0
  407. package/src/calls/relay-server.ts +390 -0
  408. package/src/calls/speaker-identification.ts +213 -0
  409. package/src/calls/twilio-config.ts +45 -0
  410. package/src/calls/twilio-provider.ts +263 -0
  411. package/src/calls/twilio-rest.ts +156 -0
  412. package/src/calls/twilio-routes.ts +311 -0
  413. package/src/calls/types.ts +39 -0
  414. package/src/calls/voice-provider.ts +14 -0
  415. package/src/calls/voice-quality.ts +114 -0
  416. package/src/cli/autonomy.ts +188 -0
  417. package/src/cli/config-commands.ts +334 -0
  418. package/src/cli/contacts.ts +149 -0
  419. package/src/cli/core-commands.ts +784 -0
  420. package/src/cli/doordash.ts +1055 -0
  421. package/src/cli/email-guardrails.ts +200 -0
  422. package/src/cli/email.ts +405 -0
  423. package/src/cli/ipc-client.ts +82 -0
  424. package/src/cli/main-screen.tsx +53 -0
  425. package/src/cli/map.ts +270 -0
  426. package/src/cli/twitter.ts +754 -0
  427. package/src/cli.ts +918 -0
  428. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  429. package/src/commands/cc-command-registry.ts +209 -0
  430. package/src/config/bundled-skills/.gitkeep +0 -0
  431. package/src/config/bundled-skills/agentmail/SKILL.md +128 -0
  432. package/src/config/bundled-skills/agentmail/icon.svg +21 -0
  433. package/src/config/bundled-skills/app-builder/SKILL.md +1404 -0
  434. package/src/config/bundled-skills/app-builder/TOOLS.json +279 -0
  435. package/src/config/bundled-skills/app-builder/icon.svg +9 -0
  436. package/src/config/bundled-skills/app-builder/tools/app-create.ts +15 -0
  437. package/src/config/bundled-skills/app-builder/tools/app-delete.ts +10 -0
  438. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +11 -0
  439. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +10 -0
  440. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +18 -0
  441. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +11 -0
  442. package/src/config/bundled-skills/app-builder/tools/app-list.ts +10 -0
  443. package/src/config/bundled-skills/app-builder/tools/app-query.ts +10 -0
  444. package/src/config/bundled-skills/app-builder/tools/app-update.ts +20 -0
  445. package/src/config/bundled-skills/browser/SKILL.md +28 -0
  446. package/src/config/bundled-skills/browser/TOOLS.json +234 -0
  447. package/src/config/bundled-skills/browser/tools/browser-click.ts +9 -0
  448. package/src/config/bundled-skills/browser/tools/browser-close.ts +9 -0
  449. package/src/config/bundled-skills/browser/tools/browser-extract.ts +9 -0
  450. package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +9 -0
  451. package/src/config/bundled-skills/browser/tools/browser-navigate.ts +9 -0
  452. package/src/config/bundled-skills/browser/tools/browser-press-key.ts +9 -0
  453. package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +9 -0
  454. package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +9 -0
  455. package/src/config/bundled-skills/browser/tools/browser-type.ts +9 -0
  456. package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +9 -0
  457. package/src/config/bundled-skills/claude-code/SKILL.md +50 -0
  458. package/src/config/bundled-skills/claude-code/TOOLS.json +40 -0
  459. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +9 -0
  460. package/src/config/bundled-skills/computer-use/SKILL.md +17 -0
  461. package/src/config/bundled-skills/computer-use/TOOLS.json +326 -0
  462. package/src/config/bundled-skills/computer-use/tools/computer-use-click.ts +9 -0
  463. package/src/config/bundled-skills/computer-use/tools/computer-use-done.ts +9 -0
  464. package/src/config/bundled-skills/computer-use/tools/computer-use-double-click.ts +9 -0
  465. package/src/config/bundled-skills/computer-use/tools/computer-use-drag.ts +9 -0
  466. package/src/config/bundled-skills/computer-use/tools/computer-use-key.ts +9 -0
  467. package/src/config/bundled-skills/computer-use/tools/computer-use-open-app.ts +9 -0
  468. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +9 -0
  469. package/src/config/bundled-skills/computer-use/tools/computer-use-respond.ts +9 -0
  470. package/src/config/bundled-skills/computer-use/tools/computer-use-right-click.ts +9 -0
  471. package/src/config/bundled-skills/computer-use/tools/computer-use-run-applescript.ts +9 -0
  472. package/src/config/bundled-skills/computer-use/tools/computer-use-scroll.ts +9 -0
  473. package/src/config/bundled-skills/computer-use/tools/computer-use-type-text.ts +9 -0
  474. package/src/config/bundled-skills/computer-use/tools/computer-use-wait.ts +9 -0
  475. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  476. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  477. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +57 -0
  478. package/src/config/bundled-skills/contacts/tools/contact-search.ts +60 -0
  479. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +66 -0
  480. package/src/config/bundled-skills/document/SKILL.md +26 -0
  481. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  482. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  483. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  484. package/src/config/bundled-skills/doordash/SKILL.md +163 -0
  485. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  486. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  487. package/src/config/bundled-skills/followups/icon.svg +24 -0
  488. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  489. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  490. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  491. package/src/config/bundled-skills/google-calendar/SKILL.md +51 -0
  492. package/src/config/bundled-skills/google-calendar/TOOLS.json +108 -0
  493. package/src/config/bundled-skills/google-calendar/calendar-client.ts +165 -0
  494. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +21 -0
  495. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +42 -0
  496. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +13 -0
  497. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +30 -0
  498. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +41 -0
  499. package/src/config/bundled-skills/google-calendar/tools/shared.ts +18 -0
  500. package/src/config/bundled-skills/google-calendar/types.ts +97 -0
  501. package/src/config/bundled-skills/image-studio/SKILL.md +32 -0
  502. package/src/config/bundled-skills/image-studio/TOOLS.json +42 -0
  503. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +115 -0
  504. package/src/config/bundled-skills/macos-automation/SKILL.md +66 -0
  505. package/src/config/bundled-skills/messaging/SKILL.md +153 -0
  506. package/src/config/bundled-skills/messaging/TOOLS.json +357 -0
  507. package/src/config/bundled-skills/messaging/tools/gmail-archive.ts +23 -0
  508. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +23 -0
  509. package/src/config/bundled-skills/messaging/tools/gmail-batch-label.ts +25 -0
  510. package/src/config/bundled-skills/messaging/tools/gmail-draft.ts +26 -0
  511. package/src/config/bundled-skills/messaging/tools/gmail-label.ts +25 -0
  512. package/src/config/bundled-skills/messaging/tools/gmail-trash.ts +23 -0
  513. package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +84 -0
  514. package/src/config/bundled-skills/messaging/tools/messaging-analyze-activity.ts +18 -0
  515. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +125 -0
  516. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +16 -0
  517. package/src/config/bundled-skills/messaging/tools/messaging-draft.ts +49 -0
  518. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +21 -0
  519. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +25 -0
  520. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +28 -0
  521. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +32 -0
  522. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +22 -0
  523. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +31 -0
  524. package/src/config/bundled-skills/messaging/tools/shared.ts +76 -0
  525. package/src/config/bundled-skills/messaging/tools/slack-add-reaction.ts +25 -0
  526. package/src/config/bundled-skills/messaging/tools/slack-leave-channel.ts +23 -0
  527. package/src/config/bundled-skills/phone-calls/SKILL.md +533 -0
  528. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  529. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  530. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +98 -0
  531. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +54 -0
  532. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +76 -0
  533. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +113 -0
  534. package/src/config/bundled-skills/public-ingress/SKILL.md +200 -0
  535. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  536. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  537. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  538. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  539. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  540. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  541. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  542. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  543. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  544. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  545. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  546. package/src/config/bundled-skills/self-upgrade/SKILL.md +68 -0
  547. package/src/config/bundled-skills/start-the-day/SKILL.md +70 -0
  548. package/src/config/bundled-skills/start-the-day/icon.svg +13 -0
  549. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  550. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  551. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  552. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  553. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  554. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  555. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  556. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  557. package/src/config/bundled-skills/tasks/TOOLS.json +281 -0
  558. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  559. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  560. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  561. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  562. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  563. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  564. package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +9 -0
  565. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  566. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  567. package/src/config/bundled-skills/transcribe/SKILL.md +25 -0
  568. package/src/config/bundled-skills/transcribe/TOOLS.json +32 -0
  569. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +370 -0
  570. package/src/config/bundled-skills/twitter/SKILL.md +220 -0
  571. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  572. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  573. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  574. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  575. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  576. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  577. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  578. package/src/config/bundled-skills/weather/SKILL.md +37 -0
  579. package/src/config/bundled-skills/weather/TOOLS.json +32 -0
  580. package/src/config/bundled-skills/weather/icon.svg +24 -0
  581. package/src/config/bundled-skills/weather/tools/get-weather.ts +9 -0
  582. package/src/config/computer-use-prompt.ts +97 -0
  583. package/src/config/defaults.ts +263 -0
  584. package/src/config/loader.ts +339 -0
  585. package/src/config/schema.ts +1436 -0
  586. package/src/config/skill-state.ts +95 -0
  587. package/src/config/skills.ts +972 -0
  588. package/src/config/system-prompt.ts +675 -0
  589. package/src/config/templates/BOOTSTRAP.md +70 -0
  590. package/src/config/templates/IDENTITY.md +25 -0
  591. package/src/config/templates/LOOKS.md +25 -0
  592. package/src/config/templates/SOUL.md +37 -0
  593. package/src/config/templates/USER.md +19 -0
  594. package/src/config/types.ts +42 -0
  595. package/src/config/vellum-skills/chatgpt-import/SKILL.md +24 -0
  596. package/src/config/vellum-skills/chatgpt-import/TOOLS.json +23 -0
  597. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +284 -0
  598. package/src/config/vellum-skills/deploy-fullstack-vercel/SKILL.md +179 -0
  599. package/src/config/vellum-skills/document-writer/SKILL.md +195 -0
  600. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +199 -0
  601. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +153 -0
  602. package/src/config/vellum-skills/telegram-setup/SKILL.md +143 -0
  603. package/src/config/vellum-skills/twilio-setup/SKILL.md +213 -0
  604. package/src/contacts/contact-store.ts +410 -0
  605. package/src/contacts/index.ts +11 -0
  606. package/src/contacts/types.ts +28 -0
  607. package/src/context/token-estimator.ts +108 -0
  608. package/src/context/tool-result-truncation.ts +128 -0
  609. package/src/context/window-manager.ts +531 -0
  610. package/src/daemon/assistant-attachments.ts +691 -0
  611. package/src/daemon/classifier.ts +110 -0
  612. package/src/daemon/computer-use-session.ts +903 -0
  613. package/src/daemon/connection-policy.ts +41 -0
  614. package/src/daemon/date-context.ts +136 -0
  615. package/src/daemon/handlers/apps.ts +530 -0
  616. package/src/daemon/handlers/browser.ts +54 -0
  617. package/src/daemon/handlers/computer-use.ts +187 -0
  618. package/src/daemon/handlers/config.ts +1517 -0
  619. package/src/daemon/handlers/diagnostics.ts +338 -0
  620. package/src/daemon/handlers/documents.ts +173 -0
  621. package/src/daemon/handlers/home-base.ts +78 -0
  622. package/src/daemon/handlers/identity.ts +127 -0
  623. package/src/daemon/handlers/index.ts +129 -0
  624. package/src/daemon/handlers/misc.ts +331 -0
  625. package/src/daemon/handlers/open-bundle-handler.ts +80 -0
  626. package/src/daemon/handlers/publish.ts +187 -0
  627. package/src/daemon/handlers/sessions.ts +555 -0
  628. package/src/daemon/handlers/shared.ts +570 -0
  629. package/src/daemon/handlers/signing.ts +37 -0
  630. package/src/daemon/handlers/skills.ts +486 -0
  631. package/src/daemon/handlers/subagents.ts +210 -0
  632. package/src/daemon/handlers/twitter-auth.ts +198 -0
  633. package/src/daemon/handlers/work-items.ts +632 -0
  634. package/src/daemon/handlers/workspace-files.ts +75 -0
  635. package/src/daemon/handlers.ts +17 -0
  636. package/src/daemon/history-repair.ts +214 -0
  637. package/src/daemon/ipc-blob-store.ts +231 -0
  638. package/src/daemon/ipc-contract-inventory.json +495 -0
  639. package/src/daemon/ipc-contract-inventory.ts +126 -0
  640. package/src/daemon/ipc-contract.ts +2551 -0
  641. package/src/daemon/ipc-protocol.ts +75 -0
  642. package/src/daemon/ipc-validate.ts +188 -0
  643. package/src/daemon/lifecycle.ts +582 -0
  644. package/src/daemon/main.ts +21 -0
  645. package/src/daemon/media-visibility-policy.ts +57 -0
  646. package/src/daemon/ride-shotgun-handler.ts +309 -0
  647. package/src/daemon/server.ts +1215 -0
  648. package/src/daemon/session-agent-loop.ts +922 -0
  649. package/src/daemon/session-attachments.ts +196 -0
  650. package/src/daemon/session-conflict-gate.ts +184 -0
  651. package/src/daemon/session-dynamic-profile.ts +63 -0
  652. package/src/daemon/session-error.ts +290 -0
  653. package/src/daemon/session-evictor.ts +196 -0
  654. package/src/daemon/session-history.ts +437 -0
  655. package/src/daemon/session-lifecycle.ts +147 -0
  656. package/src/daemon/session-media-retry.ts +147 -0
  657. package/src/daemon/session-memory.ts +212 -0
  658. package/src/daemon/session-messaging.ts +145 -0
  659. package/src/daemon/session-notifiers.ts +193 -0
  660. package/src/daemon/session-process.ts +323 -0
  661. package/src/daemon/session-queue-manager.ts +82 -0
  662. package/src/daemon/session-runtime-assembly.ts +447 -0
  663. package/src/daemon/session-skill-tools.ts +356 -0
  664. package/src/daemon/session-slash.ts +305 -0
  665. package/src/daemon/session-surfaces.ts +702 -0
  666. package/src/daemon/session-tool-setup.ts +523 -0
  667. package/src/daemon/session-usage.ts +72 -0
  668. package/src/daemon/session-workspace.ts +19 -0
  669. package/src/daemon/session.ts +400 -0
  670. package/src/daemon/tls-certs.ts +189 -0
  671. package/src/daemon/trace-emitter.ts +82 -0
  672. package/src/daemon/video-thumbnail.ts +62 -0
  673. package/src/daemon/watch-handler.ts +274 -0
  674. package/src/doordash/client.ts +999 -0
  675. package/src/doordash/queries.ts +1311 -0
  676. package/src/doordash/query-extractor.ts +93 -0
  677. package/src/doordash/session.ts +82 -0
  678. package/src/email/provider.ts +117 -0
  679. package/src/email/providers/agentmail.ts +317 -0
  680. package/src/email/providers/index.ts +58 -0
  681. package/src/email/service.ts +303 -0
  682. package/src/email/types.ts +126 -0
  683. package/src/events/bus.ts +157 -0
  684. package/src/events/domain-events.ts +83 -0
  685. package/src/events/index.ts +18 -0
  686. package/src/events/tool-audit-listener.ts +80 -0
  687. package/src/events/tool-domain-event-publisher.ts +111 -0
  688. package/src/events/tool-metrics-listener.ts +159 -0
  689. package/src/events/tool-notification-listener.ts +17 -0
  690. package/src/events/tool-profiling-listener.ts +158 -0
  691. package/src/events/tool-trace-listener.ts +75 -0
  692. package/src/export/formatter.ts +98 -0
  693. package/src/followups/followup-store.ts +168 -0
  694. package/src/followups/index.ts +10 -0
  695. package/src/followups/types.ts +29 -0
  696. package/src/gallery/default-gallery.ts +795 -0
  697. package/src/gallery/gallery-manifest.ts +24 -0
  698. package/src/home-base/app-link-store.ts +82 -0
  699. package/src/home-base/bootstrap.ts +68 -0
  700. package/src/home-base/prebuilt/index.html +662 -0
  701. package/src/home-base/prebuilt/seed-metadata.json +21 -0
  702. package/src/home-base/prebuilt/seed.ts +112 -0
  703. package/src/home-base/prebuilt-home-base-updater.ts +30 -0
  704. package/src/hooks/cli.ts +163 -0
  705. package/src/hooks/config.ts +88 -0
  706. package/src/hooks/discovery.ts +110 -0
  707. package/src/hooks/manager.ts +124 -0
  708. package/src/hooks/runner.ts +123 -0
  709. package/src/hooks/templates.ts +52 -0
  710. package/src/hooks/types.ts +72 -0
  711. package/src/inbound/public-ingress-urls.ts +123 -0
  712. package/src/index.ts +81 -0
  713. package/src/instrument.ts +60 -0
  714. package/src/logfire.ts +99 -0
  715. package/src/media/gemini-image-service.ts +136 -0
  716. package/src/memory/account-store.ts +108 -0
  717. package/src/memory/admin.ts +211 -0
  718. package/src/memory/app-git-service.ts +295 -0
  719. package/src/memory/app-store.ts +577 -0
  720. package/src/memory/attachments-store.ts +397 -0
  721. package/src/memory/channel-delivery-store.ts +353 -0
  722. package/src/memory/channel-guardian-store.ts +669 -0
  723. package/src/memory/checkpoints.ts +52 -0
  724. package/src/memory/clarification-resolver.ts +298 -0
  725. package/src/memory/conflict-intent.ts +157 -0
  726. package/src/memory/conflict-policy.ts +73 -0
  727. package/src/memory/conflict-store.ts +350 -0
  728. package/src/memory/contradiction-checker.ts +358 -0
  729. package/src/memory/conversation-key-store.ts +122 -0
  730. package/src/memory/conversation-store.ts +470 -0
  731. package/src/memory/db.ts +1991 -0
  732. package/src/memory/embedding-backend.ts +229 -0
  733. package/src/memory/embedding-gemini.ts +52 -0
  734. package/src/memory/embedding-local.ts +65 -0
  735. package/src/memory/embedding-ollama.ts +55 -0
  736. package/src/memory/embedding-openai.ts +25 -0
  737. package/src/memory/entity-extractor.ts +474 -0
  738. package/src/memory/external-conversation-store.ts +234 -0
  739. package/src/memory/fingerprint.ts +20 -0
  740. package/src/memory/indexer.ts +156 -0
  741. package/src/memory/items-extractor.ts +461 -0
  742. package/src/memory/job-handlers/backfill.ts +139 -0
  743. package/src/memory/job-handlers/cleanup.ts +58 -0
  744. package/src/memory/job-handlers/conflict.ts +141 -0
  745. package/src/memory/job-handlers/embedding.ts +61 -0
  746. package/src/memory/job-handlers/extraction.ts +123 -0
  747. package/src/memory/job-handlers/index-maintenance.ts +54 -0
  748. package/src/memory/job-handlers/summarization.ts +286 -0
  749. package/src/memory/job-utils.ts +170 -0
  750. package/src/memory/jobs-store.ts +401 -0
  751. package/src/memory/jobs-worker.ts +313 -0
  752. package/src/memory/llm-request-log-store.ts +45 -0
  753. package/src/memory/llm-usage-store.ts +60 -0
  754. package/src/memory/message-content.ts +54 -0
  755. package/src/memory/profile-compiler.ts +160 -0
  756. package/src/memory/published-pages-store.ts +137 -0
  757. package/src/memory/qdrant-client.ts +366 -0
  758. package/src/memory/qdrant-manager.ts +242 -0
  759. package/src/memory/query-builder.ts +45 -0
  760. package/src/memory/retrieval-budget.ts +30 -0
  761. package/src/memory/retriever.ts +653 -0
  762. package/src/memory/runs-store.ts +305 -0
  763. package/src/memory/schema.ts +677 -0
  764. package/src/memory/search/entity.ts +298 -0
  765. package/src/memory/search/formatting.ts +207 -0
  766. package/src/memory/search/lexical.ts +227 -0
  767. package/src/memory/search/ranking.ts +401 -0
  768. package/src/memory/search/semantic.ts +121 -0
  769. package/src/memory/search/types.ts +137 -0
  770. package/src/memory/segmenter.ts +68 -0
  771. package/src/memory/shared-app-links-store.ts +138 -0
  772. package/src/memory/tool-usage-store.ts +62 -0
  773. package/src/messaging/activity-analyzer.ts +76 -0
  774. package/src/messaging/draft-store.ts +88 -0
  775. package/src/messaging/index.ts +3 -0
  776. package/src/messaging/provider-types.ts +80 -0
  777. package/src/messaging/provider.ts +52 -0
  778. package/src/messaging/providers/gmail/adapter.ts +193 -0
  779. package/src/messaging/providers/gmail/client.ts +204 -0
  780. package/src/messaging/providers/gmail/types.ts +90 -0
  781. package/src/messaging/providers/slack/adapter.ts +202 -0
  782. package/src/messaging/providers/slack/client.ts +198 -0
  783. package/src/messaging/providers/slack/types.ts +119 -0
  784. package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
  785. package/src/messaging/providers/telegram-bot/client.ts +104 -0
  786. package/src/messaging/providers/telegram-bot/types.ts +15 -0
  787. package/src/messaging/registry.ts +35 -0
  788. package/src/messaging/style-analyzer.ts +159 -0
  789. package/src/messaging/thread-summarizer.ts +306 -0
  790. package/src/messaging/triage-engine.ts +323 -0
  791. package/src/messaging/types.ts +55 -0
  792. package/src/permissions/checker.ts +640 -0
  793. package/src/permissions/defaults.ts +254 -0
  794. package/src/permissions/prompter.ts +98 -0
  795. package/src/permissions/secret-prompter.ts +114 -0
  796. package/src/permissions/shell-identity.ts +227 -0
  797. package/src/permissions/trust-store.ts +607 -0
  798. package/src/permissions/types.ts +43 -0
  799. package/src/permissions/workspace-policy.ts +114 -0
  800. package/src/playbooks/index.ts +2 -0
  801. package/src/playbooks/playbook-compiler.ts +90 -0
  802. package/src/playbooks/types.ts +55 -0
  803. package/src/providers/anthropic/client.ts +751 -0
  804. package/src/providers/failover.ts +129 -0
  805. package/src/providers/fireworks/client.ts +20 -0
  806. package/src/providers/gemini/client.ts +285 -0
  807. package/src/providers/ollama/client.ts +30 -0
  808. package/src/providers/openai/client.ts +337 -0
  809. package/src/providers/openrouter/client.ts +20 -0
  810. package/src/providers/ratelimit.ts +93 -0
  811. package/src/providers/registry.ts +146 -0
  812. package/src/providers/retry.ts +81 -0
  813. package/src/providers/stream-timeout.ts +38 -0
  814. package/src/providers/types.ts +109 -0
  815. package/src/runtime/assistant-event-hub.ts +157 -0
  816. package/src/runtime/assistant-event.ts +82 -0
  817. package/src/runtime/channel-approval-parser.ts +60 -0
  818. package/src/runtime/channel-approval-types.ts +73 -0
  819. package/src/runtime/channel-approvals.ts +206 -0
  820. package/src/runtime/channel-guardian-service.ts +212 -0
  821. package/src/runtime/gateway-client.ts +58 -0
  822. package/src/runtime/http-server.ts +1076 -0
  823. package/src/runtime/http-types.ts +66 -0
  824. package/src/runtime/routes/app-routes.ts +174 -0
  825. package/src/runtime/routes/attachment-routes.ts +133 -0
  826. package/src/runtime/routes/call-routes.ts +190 -0
  827. package/src/runtime/routes/channel-routes.ts +1404 -0
  828. package/src/runtime/routes/conversation-routes.ts +352 -0
  829. package/src/runtime/routes/events-routes.ts +148 -0
  830. package/src/runtime/routes/run-routes.ts +257 -0
  831. package/src/runtime/routes/secret-routes.ts +76 -0
  832. package/src/runtime/run-orchestrator.ts +330 -0
  833. package/src/schedule/recurrence-engine.ts +162 -0
  834. package/src/schedule/recurrence-types.ts +67 -0
  835. package/src/schedule/schedule-store.ts +506 -0
  836. package/src/schedule/scheduler.ts +171 -0
  837. package/src/security/encrypted-store.ts +238 -0
  838. package/src/security/keychain.ts +252 -0
  839. package/src/security/oauth-callback-registry.ts +66 -0
  840. package/src/security/oauth2.ts +274 -0
  841. package/src/security/redaction.ts +89 -0
  842. package/src/security/secret-allowlist.ts +164 -0
  843. package/src/security/secret-ingress.ts +57 -0
  844. package/src/security/secret-scanner.ts +550 -0
  845. package/src/security/secure-keys.ts +180 -0
  846. package/src/security/token-manager.ts +141 -0
  847. package/src/services/published-app-updater.ts +69 -0
  848. package/src/services/vercel-deploy.ts +73 -0
  849. package/src/skills/active-skill-tools.ts +81 -0
  850. package/src/skills/clawhub.ts +414 -0
  851. package/src/skills/include-graph.ts +146 -0
  852. package/src/skills/managed-store.ts +233 -0
  853. package/src/skills/path-classifier.ts +128 -0
  854. package/src/skills/slash-commands.ts +174 -0
  855. package/src/skills/tool-manifest.ts +165 -0
  856. package/src/skills/version-hash.ts +110 -0
  857. package/src/slack/slack-webhook.ts +61 -0
  858. package/src/subagent/index.ts +19 -0
  859. package/src/subagent/manager.ts +511 -0
  860. package/src/subagent/types.ts +69 -0
  861. package/src/swarm/backend-claude-code.ts +145 -0
  862. package/src/swarm/index.ts +44 -0
  863. package/src/swarm/limits.ts +37 -0
  864. package/src/swarm/orchestrator.ts +279 -0
  865. package/src/swarm/plan-validator.ts +151 -0
  866. package/src/swarm/router-planner.ts +100 -0
  867. package/src/swarm/router-prompts.ts +36 -0
  868. package/src/swarm/synthesizer.ts +62 -0
  869. package/src/swarm/types.ts +62 -0
  870. package/src/swarm/worker-backend.ts +121 -0
  871. package/src/swarm/worker-prompts.ts +79 -0
  872. package/src/swarm/worker-runner.ts +164 -0
  873. package/src/tasks/SPEC.md +139 -0
  874. package/src/tasks/candidate-store.ts +86 -0
  875. package/src/tasks/ephemeral-permissions.ts +48 -0
  876. package/src/tasks/task-compiler.ts +199 -0
  877. package/src/tasks/task-runner.ts +90 -0
  878. package/src/tasks/task-scheduler.ts +21 -0
  879. package/src/tasks/task-store.ts +127 -0
  880. package/src/tasks/tool-sanitizer.ts +36 -0
  881. package/src/tools/apps/definitions.ts +59 -0
  882. package/src/tools/apps/executors.ts +313 -0
  883. package/src/tools/apps/open-proxy.ts +43 -0
  884. package/src/tools/apps/registry.ts +16 -0
  885. package/src/tools/assets/materialize.ts +218 -0
  886. package/src/tools/assets/search.ts +361 -0
  887. package/src/tools/browser/__tests__/auth-cache.test.ts +219 -0
  888. package/src/tools/browser/__tests__/auth-detector.test.ts +362 -0
  889. package/src/tools/browser/__tests__/jit-auth.test.ts +189 -0
  890. package/src/tools/browser/api-map.ts +293 -0
  891. package/src/tools/browser/auth-cache.ts +149 -0
  892. package/src/tools/browser/auth-detector.ts +347 -0
  893. package/src/tools/browser/auto-navigate.ts +270 -0
  894. package/src/tools/browser/browser-execution.ts +980 -0
  895. package/src/tools/browser/browser-handoff.ts +79 -0
  896. package/src/tools/browser/browser-manager.ts +715 -0
  897. package/src/tools/browser/browser-screencast.ts +217 -0
  898. package/src/tools/browser/headless-browser.ts +450 -0
  899. package/src/tools/browser/jit-auth.ts +51 -0
  900. package/src/tools/browser/network-recorder.ts +349 -0
  901. package/src/tools/browser/network-recording-types.ts +49 -0
  902. package/src/tools/browser/recording-store.ts +49 -0
  903. package/src/tools/browser/runtime-check.ts +43 -0
  904. package/src/tools/browser/x-auto-navigate.ts +207 -0
  905. package/src/tools/calls/call-end.ts +67 -0
  906. package/src/tools/calls/call-start.ts +81 -0
  907. package/src/tools/calls/call-status.ts +81 -0
  908. package/src/tools/claude-code/claude-code.ts +428 -0
  909. package/src/tools/computer-use/definitions.ts +443 -0
  910. package/src/tools/computer-use/registry.ts +22 -0
  911. package/src/tools/computer-use/request-computer-control.ts +53 -0
  912. package/src/tools/computer-use/skill-proxy-bridge.ts +28 -0
  913. package/src/tools/credentials/account-registry.ts +127 -0
  914. package/src/tools/credentials/broker-types.ts +107 -0
  915. package/src/tools/credentials/broker.ts +372 -0
  916. package/src/tools/credentials/domain-policy.ts +51 -0
  917. package/src/tools/credentials/host-pattern-match.ts +60 -0
  918. package/src/tools/credentials/metadata-store.ts +335 -0
  919. package/src/tools/credentials/policy-types.ts +52 -0
  920. package/src/tools/credentials/policy-validate.ts +80 -0
  921. package/src/tools/credentials/resolve.ts +122 -0
  922. package/src/tools/credentials/selection.ts +159 -0
  923. package/src/tools/credentials/tool-policy.ts +25 -0
  924. package/src/tools/credentials/vault.ts +657 -0
  925. package/src/tools/document/document-tool.ts +92 -0
  926. package/src/tools/document/editor-template.ts +237 -0
  927. package/src/tools/execution-target.ts +21 -0
  928. package/src/tools/execution-timeout.ts +49 -0
  929. package/src/tools/executor.ts +815 -0
  930. package/src/tools/filesystem/edit.ts +127 -0
  931. package/src/tools/filesystem/fuzzy-match.ts +202 -0
  932. package/src/tools/filesystem/read.ts +71 -0
  933. package/src/tools/filesystem/view-image.ts +199 -0
  934. package/src/tools/filesystem/write.ts +79 -0
  935. package/src/tools/followups/followup_create.ts +76 -0
  936. package/src/tools/followups/followup_list.ts +60 -0
  937. package/src/tools/followups/followup_resolve.ts +56 -0
  938. package/src/tools/host-filesystem/edit.ts +125 -0
  939. package/src/tools/host-filesystem/read.ts +80 -0
  940. package/src/tools/host-filesystem/write.ts +76 -0
  941. package/src/tools/host-terminal/cli-discover.ts +180 -0
  942. package/src/tools/host-terminal/host-shell.ts +191 -0
  943. package/src/tools/memory/definitions.ts +69 -0
  944. package/src/tools/memory/handlers.ts +246 -0
  945. package/src/tools/memory/register.ts +66 -0
  946. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  947. package/src/tools/network/domain-normalize.ts +85 -0
  948. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  949. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  950. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  951. package/src/tools/network/script-proxy/certs.ts +237 -0
  952. package/src/tools/network/script-proxy/connect-tunnel.ts +82 -0
  953. package/src/tools/network/script-proxy/http-forwarder.ts +151 -0
  954. package/src/tools/network/script-proxy/index.ts +28 -0
  955. package/src/tools/network/script-proxy/logging.ts +196 -0
  956. package/src/tools/network/script-proxy/mitm-handler.ts +269 -0
  957. package/src/tools/network/script-proxy/policy.ts +152 -0
  958. package/src/tools/network/script-proxy/router.ts +60 -0
  959. package/src/tools/network/script-proxy/server.ts +136 -0
  960. package/src/tools/network/script-proxy/session-manager.ts +534 -0
  961. package/src/tools/network/script-proxy/types.ts +125 -0
  962. package/src/tools/network/url-safety.ts +227 -0
  963. package/src/tools/network/web-fetch.ts +713 -0
  964. package/src/tools/network/web-search.ts +296 -0
  965. package/src/tools/policy-context.ts +29 -0
  966. package/src/tools/registry.ts +295 -0
  967. package/src/tools/reminder/reminder-store.ts +148 -0
  968. package/src/tools/reminder/reminder.ts +80 -0
  969. package/src/tools/schedule/create.ts +81 -0
  970. package/src/tools/schedule/delete.ts +28 -0
  971. package/src/tools/schedule/list.ts +69 -0
  972. package/src/tools/schedule/update.ts +97 -0
  973. package/src/tools/shared/filesystem/edit-engine.ts +56 -0
  974. package/src/tools/shared/filesystem/errors.ts +85 -0
  975. package/src/tools/shared/filesystem/file-ops-service.ts +215 -0
  976. package/src/tools/shared/filesystem/format-diff.ts +35 -0
  977. package/src/tools/shared/filesystem/path-policy.ts +125 -0
  978. package/src/tools/shared/filesystem/size-guard.ts +41 -0
  979. package/src/tools/shared/filesystem/types.ts +80 -0
  980. package/src/tools/shared/shell-output.ts +52 -0
  981. package/src/tools/skills/delete-managed.ts +60 -0
  982. package/src/tools/skills/load.ts +139 -0
  983. package/src/tools/skills/sandbox-runner.ts +279 -0
  984. package/src/tools/skills/scaffold-managed.ts +150 -0
  985. package/src/tools/skills/script-contract.ts +6 -0
  986. package/src/tools/skills/skill-script-runner.ts +86 -0
  987. package/src/tools/skills/skill-tool-factory.ts +64 -0
  988. package/src/tools/skills/vellum-catalog.ts +217 -0
  989. package/src/tools/subagent/abort.ts +33 -0
  990. package/src/tools/subagent/message.ts +39 -0
  991. package/src/tools/subagent/read.ts +67 -0
  992. package/src/tools/subagent/spawn.ts +46 -0
  993. package/src/tools/subagent/status.ts +45 -0
  994. package/src/tools/swarm/delegate.ts +183 -0
  995. package/src/tools/system/request-permission.ts +98 -0
  996. package/src/tools/system/version.ts +43 -0
  997. package/src/tools/tasks/index.ts +27 -0
  998. package/src/tools/tasks/task-delete.ts +82 -0
  999. package/src/tools/tasks/task-list.ts +44 -0
  1000. package/src/tools/tasks/task-run.ts +97 -0
  1001. package/src/tools/tasks/task-save.ts +47 -0
  1002. package/src/tools/tasks/work-item-enqueue.ts +234 -0
  1003. package/src/tools/tasks/work-item-list.ts +55 -0
  1004. package/src/tools/tasks/work-item-remove.ts +60 -0
  1005. package/src/tools/tasks/work-item-run.ts +78 -0
  1006. package/src/tools/tasks/work-item-update.ts +114 -0
  1007. package/src/tools/terminal/backends/docker.ts +372 -0
  1008. package/src/tools/terminal/backends/native.ts +190 -0
  1009. package/src/tools/terminal/backends/types.ts +26 -0
  1010. package/src/tools/terminal/evaluate-typescript.ts +275 -0
  1011. package/src/tools/terminal/parser.ts +413 -0
  1012. package/src/tools/terminal/safe-env.ts +37 -0
  1013. package/src/tools/terminal/sandbox-diagnostics.ts +149 -0
  1014. package/src/tools/terminal/sandbox.ts +44 -0
  1015. package/src/tools/terminal/shell.ts +257 -0
  1016. package/src/tools/tool-manifest.ts +198 -0
  1017. package/src/tools/types.ts +176 -0
  1018. package/src/tools/ui-surface/definitions.ts +244 -0
  1019. package/src/tools/ui-surface/registry.ts +14 -0
  1020. package/src/tools/watch/screen-watch.ts +130 -0
  1021. package/src/tools/watch/watch-state.ts +119 -0
  1022. package/src/tools/watcher/create.ts +64 -0
  1023. package/src/tools/watcher/delete.ts +27 -0
  1024. package/src/tools/watcher/digest.ts +50 -0
  1025. package/src/tools/watcher/list.ts +60 -0
  1026. package/src/tools/watcher/update.ts +56 -0
  1027. package/src/tools/weather/service.ts +551 -0
  1028. package/src/twitter/client.ts +690 -0
  1029. package/src/twitter/oauth-client.ts +102 -0
  1030. package/src/twitter/router.ts +101 -0
  1031. package/src/twitter/session.ts +91 -0
  1032. package/src/usage/actors.ts +24 -0
  1033. package/src/usage/types.ts +37 -0
  1034. package/src/util/clipboard.ts +33 -0
  1035. package/src/util/content-id.ts +16 -0
  1036. package/src/util/debounce.ts +88 -0
  1037. package/src/util/diff.ts +181 -0
  1038. package/src/util/errors.ts +129 -0
  1039. package/src/util/logger.ts +243 -0
  1040. package/src/util/network-info.ts +47 -0
  1041. package/src/util/platform.ts +632 -0
  1042. package/src/util/pricing.ts +150 -0
  1043. package/src/util/promise-guard.ts +37 -0
  1044. package/src/util/retry.ts +98 -0
  1045. package/src/util/spinner.ts +51 -0
  1046. package/src/util/time.ts +16 -0
  1047. package/src/util/truncate.ts +6 -0
  1048. package/src/util/xml.ts +4 -0
  1049. package/src/version.ts +3 -0
  1050. package/src/watcher/constants.ts +11 -0
  1051. package/src/watcher/engine.ts +199 -0
  1052. package/src/watcher/provider-registry.ts +15 -0
  1053. package/src/watcher/provider-types.ts +48 -0
  1054. package/src/watcher/providers/gmail.ts +198 -0
  1055. package/src/watcher/providers/google-calendar.ts +228 -0
  1056. package/src/watcher/providers/slack.ts +129 -0
  1057. package/src/watcher/watcher-store.ts +419 -0
  1058. package/src/work-items/work-item-runner.ts +171 -0
  1059. package/src/work-items/work-item-store.ts +325 -0
  1060. package/src/workspace/commit-message-enrichment-service.ts +284 -0
  1061. package/src/workspace/commit-message-provider.ts +95 -0
  1062. package/src/workspace/git-service.ts +857 -0
  1063. package/src/workspace/heartbeat-service.ts +345 -0
  1064. package/src/workspace/provider-commit-message-generator.ts +285 -0
  1065. package/src/workspace/top-level-renderer.ts +19 -0
  1066. package/src/workspace/top-level-scanner.ts +41 -0
  1067. package/src/workspace/turn-commit.ts +175 -0
  1068. package/tsconfig.json +21 -0
@@ -0,0 +1,1989 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { describe, test, expect, beforeEach, afterEach, afterAll, mock, spyOn } from 'bun:test';
3
+ import type { ToolExecutionResult, Tool } from '../tools/types.js';
4
+ import { RiskLevel } from '../permissions/types.js';
5
+ import type { PolicyContext } from '../permissions/types.js';
6
+
7
+ const mockConfig = {
8
+ provider: 'anthropic',
9
+ model: 'test',
10
+ apiKeys: {},
11
+ maxTokens: 4096,
12
+ dataDir: '/tmp',
13
+ timeouts: { shellDefaultTimeoutSec: 120, shellMaxTimeoutSec: 600, permissionTimeoutSec: 300 },
14
+ sandbox: { enabled: false, backend: 'native' as const, docker: { image: 'vellum-sandbox:latest', cpus: 1, memoryMb: 512, pidsLimit: 256, network: 'none' as const } },
15
+ rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
16
+ secretDetection: { enabled: false, action: 'warn' as const, entropyThreshold: 4.0 },
17
+ };
18
+
19
+ let fakeToolResult: ToolExecutionResult = { content: 'ok', isError: false };
20
+
21
+ /** Captured arguments from the last check() call, for assertion in tests. */
22
+ let lastCheckArgs: { toolName: string; input: Record<string, unknown>; workingDir: string; policyContext?: PolicyContext } | undefined;
23
+
24
+ /** Optional override for getTool — lets tests supply skill-origin tools. */
25
+ let getToolOverride: ((name: string) => Tool | undefined) | undefined;
26
+
27
+ /** Override the check() result for tests that need to trigger prompting. */
28
+ let checkResultOverride: { decision: string; reason: string } | undefined;
29
+
30
+ /** Function override for check() — when set, takes precedence over the static override. */
31
+ let checkFnOverride: ((toolName: string, input: Record<string, unknown>, workingDir: string, policyContext?: PolicyContext) => Promise<{ decision: string; reason: string }>) | undefined;
32
+
33
+ /** Spy on addRule to capture calls without replacing the real implementation. */
34
+ let addRuleSpy: ReturnType<typeof spyOn> | undefined;
35
+
36
+ mock.module('../config/loader.js', () => ({
37
+ getConfig: () => mockConfig,
38
+ loadConfig: () => mockConfig,
39
+ invalidateConfigCache: () => {},
40
+ saveConfig: () => {},
41
+ loadRawConfig: () => ({}),
42
+ saveRawConfig: () => {},
43
+ getNestedValue: () => undefined,
44
+ setNestedValue: () => {},
45
+ }));
46
+
47
+ mock.module('../util/logger.js', () => ({
48
+ getLogger: () => new Proxy({} as Record<string, unknown>, {
49
+ get: () => () => {},
50
+ }),
51
+ isDebug: () => false,
52
+ truncateForLog: (value: string) => value,
53
+ }));
54
+
55
+ mock.module('../permissions/checker.js', () => ({
56
+ classifyRisk: async () => 'low',
57
+ check: async (toolName: string, input: Record<string, unknown>, workingDir: string, policyContext?: PolicyContext) => {
58
+ lastCheckArgs = { toolName, input, workingDir, policyContext };
59
+ if (checkFnOverride) return checkFnOverride(toolName, input, workingDir, policyContext);
60
+ if (checkResultOverride) return checkResultOverride;
61
+ return { decision: 'allow', reason: 'allowed' };
62
+ },
63
+ generateAllowlistOptions: () => [{ label: 'exact', description: 'exact', pattern: 'exact' }],
64
+ generateScopeOptions: () => [{ label: '/tmp', scope: '/tmp' }],
65
+ }));
66
+
67
+ mock.module('../memory/tool-usage-store.js', () => ({
68
+ recordToolInvocation: () => {},
69
+ }));
70
+
71
+ mock.module('../tools/registry.js', () => ({
72
+ getTool: (name: string) => {
73
+ if (getToolOverride) return getToolOverride(name);
74
+ if (name === 'unknown_tool') return undefined;
75
+ return {
76
+ name,
77
+ description: 'test tool',
78
+ category: 'test',
79
+ defaultRiskLevel: 'low',
80
+ getDefinition: () => ({}),
81
+ execute: async () => fakeToolResult,
82
+ };
83
+ },
84
+ getAllTools: () => [],
85
+ }));
86
+
87
+ mock.module('../tools/shared/filesystem/path-policy.js', () => ({
88
+ sandboxPolicy: () => ({ ok: false }),
89
+ hostPolicy: () => ({ ok: false }),
90
+ }));
91
+
92
+ mock.module('../tools/terminal/sandbox.js', () => ({
93
+ wrapCommand: () => ({ command: '', sandboxed: false }),
94
+ }));
95
+
96
+ import { ToolExecutor, isSideEffectTool } from '../tools/executor.js';
97
+ import type { ToolContext } from '../tools/types.js';
98
+ import { PermissionPrompter } from '../permissions/prompter.js';
99
+ import * as trustStore from '../permissions/trust-store.js';
100
+
101
+ function makeContext(overrides?: Partial<ToolContext>): ToolContext {
102
+ return {
103
+ workingDir: '/tmp/project',
104
+ sessionId: 'session-1',
105
+ conversationId: 'conversation-1',
106
+ ...overrides,
107
+ };
108
+ }
109
+
110
+ function makePrompter(): PermissionPrompter {
111
+ return {
112
+ prompt: async () => ({ decision: 'allow' as const }),
113
+ resolveConfirmation: () => {},
114
+ updateSender: () => {},
115
+ dispose: () => {},
116
+ } as unknown as PermissionPrompter;
117
+ }
118
+
119
+ afterAll(() => { mock.restore(); });
120
+
121
+ describe('ToolExecutor allowedToolNames gating', () => {
122
+ beforeEach(() => {
123
+ fakeToolResult = { content: 'ok', isError: false };
124
+ lastCheckArgs = undefined;
125
+ getToolOverride = undefined;
126
+ checkResultOverride = undefined;
127
+ checkFnOverride = undefined;
128
+ if (addRuleSpy) { addRuleSpy.mockRestore(); addRuleSpy = undefined; }
129
+ });
130
+
131
+ test('executes normally when allowedToolNames is not set (backward compat)', async () => {
132
+ const executor = new ToolExecutor(makePrompter());
133
+ const result = await executor.execute('file_read', { path: 'README.md' }, makeContext());
134
+ expect(result.isError).toBe(false);
135
+ expect(result.content).toBe('ok');
136
+ });
137
+
138
+ test('executes normally when tool is in the allowed set', async () => {
139
+ const executor = new ToolExecutor(makePrompter());
140
+ const allowed = new Set(['file_read', 'file_write', 'bash']);
141
+ const result = await executor.execute('file_read', { path: 'README.md' }, makeContext({ allowedToolNames: allowed }));
142
+ expect(result.isError).toBe(false);
143
+ expect(result.content).toBe('ok');
144
+ });
145
+
146
+ test('blocks execution when tool is NOT in the allowed set', async () => {
147
+ const executor = new ToolExecutor(makePrompter());
148
+ const allowed = new Set(['file_read', 'bash']);
149
+ const result = await executor.execute('file_write', { path: 'test.txt', content: 'hello' }, makeContext({ allowedToolNames: allowed }));
150
+ expect(result.isError).toBe(true);
151
+ expect(result.content).toContain('not currently active');
152
+ });
153
+
154
+ test('error message includes the blocked tool name', async () => {
155
+ const executor = new ToolExecutor(makePrompter());
156
+ const allowed = new Set(['bash']);
157
+ const result = await executor.execute('file_edit', { path: 'x' }, makeContext({ allowedToolNames: allowed }));
158
+ expect(result.isError).toBe(true);
159
+ expect(result.content).toBe('Tool "file_edit" is not currently active. Load the skill that provides this tool first.');
160
+ });
161
+
162
+ test('empty allowed set blocks all tools', async () => {
163
+ const executor = new ToolExecutor(makePrompter());
164
+ const allowed = new Set<string>();
165
+ const result = await executor.execute('file_read', { path: 'README.md' }, makeContext({ allowedToolNames: allowed }));
166
+ expect(result.isError).toBe(true);
167
+ expect(result.content).toContain('file_read');
168
+ expect(result.content).toContain('not currently active');
169
+ });
170
+ });
171
+
172
+ describe('ToolExecutor policy context plumbing', () => {
173
+ beforeEach(() => {
174
+ fakeToolResult = { content: 'ok', isError: false };
175
+ lastCheckArgs = undefined;
176
+ getToolOverride = undefined;
177
+ checkResultOverride = undefined;
178
+ checkFnOverride = undefined;
179
+ if (addRuleSpy) { addRuleSpy.mockRestore(); addRuleSpy = undefined; }
180
+ });
181
+
182
+ test('passes PolicyContext with executionTarget for skill-origin tools', async () => {
183
+ getToolOverride = (name: string) => {
184
+ if (name === 'unknown_tool') return undefined;
185
+ return {
186
+ name,
187
+ description: 'skill tool',
188
+ category: 'skill',
189
+ defaultRiskLevel: RiskLevel.Low,
190
+ origin: 'skill' as const,
191
+ ownerSkillId: 'my-skill-123',
192
+ ownerSkillVersionHash: 'abc123hash',
193
+ executionTarget: 'sandbox' as const,
194
+ getDefinition: () => ({ name, description: 'skill tool', input_schema: { type: 'object' as const, properties: {} } }),
195
+ execute: async () => fakeToolResult,
196
+ };
197
+ };
198
+
199
+ const executor = new ToolExecutor(makePrompter());
200
+ const result = await executor.execute('skill_tool', { action: 'run' }, makeContext());
201
+
202
+ expect(result.isError).toBe(false);
203
+ expect(lastCheckArgs).toBeDefined();
204
+ expect(lastCheckArgs!.policyContext).toEqual({
205
+ executionTarget: 'sandbox',
206
+ });
207
+ });
208
+
209
+ test('passes undefined policyContext for core tools (no origin)', async () => {
210
+ // Default getTool returns core tools with no origin field
211
+ getToolOverride = undefined;
212
+
213
+ const executor = new ToolExecutor(makePrompter());
214
+ const result = await executor.execute('file_read', { path: 'test.txt' }, makeContext());
215
+
216
+ expect(result.isError).toBe(false);
217
+ expect(lastCheckArgs).toBeDefined();
218
+ expect(lastCheckArgs!.policyContext).toBeUndefined();
219
+ });
220
+
221
+ test('passes undefined policyContext for tools with origin "core"', async () => {
222
+ getToolOverride = (name: string) => {
223
+ if (name === 'unknown_tool') return undefined;
224
+ return {
225
+ name,
226
+ description: 'core tool',
227
+ category: 'core',
228
+ defaultRiskLevel: RiskLevel.Low,
229
+ origin: 'core' as const,
230
+ getDefinition: () => ({ name, description: 'core tool', input_schema: { type: 'object' as const, properties: {} } }),
231
+ execute: async () => fakeToolResult,
232
+ };
233
+ };
234
+
235
+ const executor = new ToolExecutor(makePrompter());
236
+ const result = await executor.execute('file_read', { path: 'test.txt' }, makeContext());
237
+
238
+ expect(result.isError).toBe(false);
239
+ expect(lastCheckArgs).toBeDefined();
240
+ expect(lastCheckArgs!.policyContext).toBeUndefined();
241
+ });
242
+
243
+ test('includes executionTarget "host" from skill tool metadata', async () => {
244
+ getToolOverride = (name: string) => {
245
+ if (name === 'unknown_tool') return undefined;
246
+ return {
247
+ name,
248
+ description: 'host skill tool',
249
+ category: 'skill',
250
+ defaultRiskLevel: RiskLevel.Low,
251
+ origin: 'skill' as const,
252
+ ownerSkillId: 'host-skill',
253
+ ownerSkillVersionHash: 'host-hash',
254
+ executionTarget: 'host' as const,
255
+ getDefinition: () => ({ name, description: 'host skill tool', input_schema: { type: 'object' as const, properties: {} } }),
256
+ execute: async () => fakeToolResult,
257
+ };
258
+ };
259
+
260
+ const executor = new ToolExecutor(makePrompter());
261
+ const result = await executor.execute('host_skill_tool', { action: 'run' }, makeContext());
262
+
263
+ expect(result.isError).toBe(false);
264
+ expect(lastCheckArgs).toBeDefined();
265
+ expect(lastCheckArgs!.policyContext).toEqual({
266
+ executionTarget: 'host',
267
+ });
268
+ });
269
+
270
+ test('skill tool without executionTarget passes undefined executionTarget', async () => {
271
+ getToolOverride = (name: string) => {
272
+ if (name === 'unknown_tool') return undefined;
273
+ return {
274
+ name,
275
+ description: 'skill without target',
276
+ category: 'skill',
277
+ defaultRiskLevel: RiskLevel.Low,
278
+ origin: 'skill' as const,
279
+ ownerSkillId: 'no-target-skill',
280
+ // executionTarget intentionally omitted
281
+ getDefinition: () => ({ name, description: 'skill tool', input_schema: { type: 'object' as const, properties: {} } }),
282
+ execute: async () => fakeToolResult,
283
+ };
284
+ };
285
+
286
+ const executor = new ToolExecutor(makePrompter());
287
+ const result = await executor.execute('no_target_tool', {}, makeContext());
288
+
289
+ expect(result.isError).toBe(false);
290
+ expect(lastCheckArgs).toBeDefined();
291
+ expect(lastCheckArgs!.policyContext).toEqual({
292
+ executionTarget: undefined,
293
+ });
294
+ });
295
+ });
296
+
297
+ /**
298
+ * Helper: create a prompter that returns a specific decision with pattern/scope.
299
+ */
300
+ function makePrompterWithDecision(
301
+ decision: string,
302
+ selectedPattern?: string,
303
+ selectedScope?: string,
304
+ ): PermissionPrompter {
305
+ return {
306
+ prompt: async () => ({ decision, selectedPattern, selectedScope }),
307
+ resolveConfirmation: () => {},
308
+ updateSender: () => {},
309
+ dispose: () => {},
310
+ } as unknown as PermissionPrompter;
311
+ }
312
+
313
+ describe('ToolExecutor contextual rule creation', () => {
314
+ beforeEach(() => {
315
+ fakeToolResult = { content: 'ok', isError: false };
316
+ lastCheckArgs = undefined;
317
+ getToolOverride = undefined;
318
+ checkResultOverride = undefined;
319
+ checkFnOverride = undefined;
320
+ if (addRuleSpy) { addRuleSpy.mockRestore(); addRuleSpy = undefined; }
321
+ });
322
+
323
+ function setupAddRuleSpy() {
324
+ addRuleSpy = spyOn(trustStore, 'addRule').mockImplementation(
325
+ (tool: string, pattern: string, scope: string, decision = 'allow', priority = 100, options?: any) => {
326
+ return { id: 'spy-rule-id', tool, pattern, scope, decision, priority, createdAt: Date.now(), ...options } as any;
327
+ },
328
+ );
329
+ return addRuleSpy;
330
+ }
331
+
332
+ test('always_allow for a skill tool captures execution target in the rule', async () => {
333
+ checkResultOverride = { decision: 'prompt', reason: 'test prompt' };
334
+ const spy = setupAddRuleSpy();
335
+
336
+ getToolOverride = (name: string) => {
337
+ if (name === 'unknown_tool') return undefined;
338
+ return {
339
+ name,
340
+ description: 'skill tool',
341
+ category: 'skill',
342
+ defaultRiskLevel: RiskLevel.Low,
343
+ origin: 'skill' as const,
344
+ ownerSkillId: 'my-skill-42',
345
+ ownerSkillVersionHash: 'sha256-deadbeef',
346
+ executionTarget: 'sandbox' as const,
347
+ getDefinition: () => ({ name, description: 'skill tool', input_schema: { type: 'object' as const, properties: {} } }),
348
+ execute: async () => fakeToolResult,
349
+ };
350
+ };
351
+
352
+ const prompter = makePrompterWithDecision('always_allow', 'skill_tool:*', '/tmp/project');
353
+ const executor = new ToolExecutor(prompter);
354
+ const result = await executor.execute('skill_tool', { action: 'run' }, makeContext());
355
+
356
+ expect(result.isError).toBe(false);
357
+ expect(spy).toHaveBeenCalledTimes(1);
358
+ const [tool, pattern, scope, decision, _priority, options] = spy.mock.calls[0];
359
+ expect(tool).toBe('skill_tool');
360
+ expect(pattern).toBe('skill_tool:*');
361
+ expect(scope).toBe('/tmp/project');
362
+ expect(decision).toBe('allow');
363
+ expect(options).toBeDefined();
364
+ expect(options.executionTarget).toBe('sandbox');
365
+ });
366
+
367
+ test('always_allow_high_risk sets allowHighRisk and captures execution target', async () => {
368
+ checkResultOverride = { decision: 'prompt', reason: 'test prompt' };
369
+ const spy = setupAddRuleSpy();
370
+
371
+ getToolOverride = (name: string) => {
372
+ if (name === 'unknown_tool') return undefined;
373
+ return {
374
+ name,
375
+ description: 'high-risk skill tool',
376
+ category: 'skill',
377
+ defaultRiskLevel: RiskLevel.High,
378
+ origin: 'skill' as const,
379
+ ownerSkillId: 'dangerous-skill',
380
+ ownerSkillVersionHash: 'sha256-abc',
381
+ executionTarget: 'host' as const,
382
+ getDefinition: () => ({ name, description: 'high-risk skill tool', input_schema: { type: 'object' as const, properties: {} } }),
383
+ execute: async () => fakeToolResult,
384
+ };
385
+ };
386
+
387
+ const prompter = makePrompterWithDecision('always_allow_high_risk', 'risky_tool:*', 'everywhere');
388
+ const executor = new ToolExecutor(prompter);
389
+ const result = await executor.execute('risky_tool', {}, makeContext());
390
+
391
+ expect(result.isError).toBe(false);
392
+ expect(spy).toHaveBeenCalledTimes(1);
393
+ const [tool, pattern, scope, decision, _priority, options] = spy.mock.calls[0];
394
+ expect(tool).toBe('risky_tool');
395
+ expect(pattern).toBe('risky_tool:*');
396
+ expect(scope).toBe('everywhere');
397
+ expect(decision).toBe('allow');
398
+ expect(options).toBeDefined();
399
+ expect(options.allowHighRisk).toBe(true);
400
+ expect(options.executionTarget).toBe('host');
401
+ });
402
+
403
+ test('always_allow for a core tool creates rule without options', async () => {
404
+ checkResultOverride = { decision: 'prompt', reason: 'test prompt' };
405
+ const spy = setupAddRuleSpy();
406
+
407
+ // Default getTool returns core tools with no origin field
408
+ getToolOverride = undefined;
409
+
410
+ const prompter = makePrompterWithDecision('always_allow', 'git *', '/tmp/project');
411
+ const executor = new ToolExecutor(prompter);
412
+ const result = await executor.execute('bash', { command: 'git status' }, makeContext());
413
+
414
+ expect(result.isError).toBe(false);
415
+ expect(spy).toHaveBeenCalledTimes(1);
416
+ const [tool, pattern, scope, decision, _priority, options] = spy.mock.calls[0];
417
+ expect(tool).toBe('bash');
418
+ expect(pattern).toBe('git *');
419
+ expect(scope).toBe('/tmp/project');
420
+ expect(decision).toBe('allow');
421
+ // No options since there's no execution target for core tools
422
+ expect(options).toBeUndefined();
423
+ });
424
+
425
+ test('always_allow without selectedPattern does not create a rule', async () => {
426
+ checkResultOverride = { decision: 'prompt', reason: 'test prompt' };
427
+ const spy = setupAddRuleSpy();
428
+
429
+ const prompter = makePrompterWithDecision('always_allow', undefined, '/tmp/project');
430
+ const executor = new ToolExecutor(prompter);
431
+ const result = await executor.execute('file_read', { path: 'test.txt' }, makeContext());
432
+
433
+ expect(result.isError).toBe(false);
434
+ expect(spy).not.toHaveBeenCalled();
435
+ });
436
+
437
+ test('always_allow without selectedScope does not create a rule', async () => {
438
+ checkResultOverride = { decision: 'prompt', reason: 'test prompt' };
439
+ const spy = setupAddRuleSpy();
440
+
441
+ const prompter = makePrompterWithDecision('always_allow', 'file_read:*', undefined);
442
+ const executor = new ToolExecutor(prompter);
443
+ const result = await executor.execute('file_read', { path: 'test.txt' }, makeContext());
444
+
445
+ expect(result.isError).toBe(false);
446
+ expect(spy).not.toHaveBeenCalled();
447
+ });
448
+
449
+ test('always_allow_high_risk for core tool sets allowHighRisk without execution target', async () => {
450
+ checkResultOverride = { decision: 'prompt', reason: 'test prompt' };
451
+ const spy = setupAddRuleSpy();
452
+ getToolOverride = undefined;
453
+
454
+ const prompter = makePrompterWithDecision('always_allow_high_risk', 'sudo *', 'everywhere');
455
+ const executor = new ToolExecutor(prompter);
456
+ const result = await executor.execute('bash', { command: 'sudo apt update' }, makeContext());
457
+
458
+ expect(result.isError).toBe(false);
459
+ expect(spy).toHaveBeenCalledTimes(1);
460
+ const [,,,, , options] = spy.mock.calls[0];
461
+ expect(options).toBeDefined();
462
+ expect(options.allowHighRisk).toBe(true);
463
+ // No execution target for core tools
464
+ expect(options.executionTarget).toBeUndefined();
465
+ });
466
+
467
+ test('skill tool with host execution target records executionTarget in rule', async () => {
468
+ checkResultOverride = { decision: 'prompt', reason: 'test prompt' };
469
+ const spy = setupAddRuleSpy();
470
+
471
+ getToolOverride = (name: string) => {
472
+ if (name === 'unknown_tool') return undefined;
473
+ return {
474
+ name,
475
+ description: 'host skill tool',
476
+ category: 'skill',
477
+ defaultRiskLevel: RiskLevel.Low,
478
+ origin: 'skill' as const,
479
+ ownerSkillId: 'host-skill',
480
+ ownerSkillVersionHash: 'host-hash-v1',
481
+ executionTarget: 'host' as const,
482
+ getDefinition: () => ({ name, description: 'host skill tool', input_schema: { type: 'object' as const, properties: {} } }),
483
+ execute: async () => fakeToolResult,
484
+ };
485
+ };
486
+
487
+ const prompter = makePrompterWithDecision('always_allow', 'host_action:*', '/tmp/project');
488
+ const executor = new ToolExecutor(prompter);
489
+ const result = await executor.execute('host_action', { action: 'click' }, makeContext());
490
+
491
+ expect(result.isError).toBe(false);
492
+ expect(spy).toHaveBeenCalledTimes(1);
493
+ const [,,,, , options] = spy.mock.calls[0];
494
+ expect(options).toBeDefined();
495
+ expect(options.executionTarget).toBe('host');
496
+ });
497
+ });
498
+
499
+ describe('ToolExecutor strict mode + high-risk integration (PR 25)', () => {
500
+ beforeEach(() => {
501
+ fakeToolResult = { content: 'ok', isError: false };
502
+ lastCheckArgs = undefined;
503
+ getToolOverride = undefined;
504
+ checkResultOverride = undefined;
505
+ checkFnOverride = undefined;
506
+ if (addRuleSpy) { addRuleSpy.mockRestore(); addRuleSpy = undefined; }
507
+ });
508
+
509
+ function setupAddRuleSpy() {
510
+ addRuleSpy = spyOn(trustStore, 'addRule').mockImplementation(
511
+ (tool: string, pattern: string, scope: string, decision = 'allow', priority = 100, options?: any) => {
512
+ return { id: 'spy-rule-id', tool, pattern, scope, decision, priority, createdAt: Date.now(), ...options } as any;
513
+ },
514
+ );
515
+ return addRuleSpy;
516
+ }
517
+
518
+ test('always_allow_high_risk creates rule with allowHighRisk: true for high-risk skill tool', async () => {
519
+ checkResultOverride = { decision: 'prompt', reason: 'High risk: always requires approval' };
520
+ const spy = setupAddRuleSpy();
521
+
522
+ getToolOverride = (name: string) => {
523
+ if (name === 'unknown_tool') return undefined;
524
+ return {
525
+ name,
526
+ description: 'high-risk skill tool',
527
+ category: 'skill',
528
+ defaultRiskLevel: RiskLevel.High,
529
+ origin: 'skill' as const,
530
+ ownerSkillId: 'deploy-skill',
531
+ ownerSkillVersionHash: 'sha256-deploy-v1',
532
+ executionTarget: 'host' as const,
533
+ getDefinition: () => ({ name, description: 'high-risk skill tool', input_schema: { type: 'object' as const, properties: {} } }),
534
+ execute: async () => fakeToolResult,
535
+ };
536
+ };
537
+
538
+ const prompter = makePrompterWithDecision('always_allow_high_risk', 'deploy_tool:*', 'everywhere');
539
+ const executor = new ToolExecutor(prompter);
540
+ const result = await executor.execute('deploy_tool', { target: 'prod' }, makeContext());
541
+
542
+ expect(result.isError).toBe(false);
543
+ expect(spy).toHaveBeenCalledTimes(1);
544
+ const [tool, pattern, scope, decision, _priority, options] = spy.mock.calls[0];
545
+ expect(tool).toBe('deploy_tool');
546
+ expect(pattern).toBe('deploy_tool:*');
547
+ expect(scope).toBe('everywhere');
548
+ expect(decision).toBe('allow');
549
+ // The key integration assertion: allowHighRisk + execution target together
550
+ expect(options.allowHighRisk).toBe(true);
551
+ expect(options.executionTarget).toBe('host');
552
+ });
553
+
554
+ test('always_allow creates rule without allowHighRisk even for high-risk skill tool', async () => {
555
+ checkResultOverride = { decision: 'prompt', reason: 'test prompt' };
556
+ const spy = setupAddRuleSpy();
557
+
558
+ getToolOverride = (name: string) => {
559
+ if (name === 'unknown_tool') return undefined;
560
+ return {
561
+ name,
562
+ description: 'high-risk skill tool',
563
+ category: 'skill',
564
+ defaultRiskLevel: RiskLevel.High,
565
+ origin: 'skill' as const,
566
+ ownerSkillId: 'risky-skill',
567
+ ownerSkillVersionHash: 'sha256-risky',
568
+ executionTarget: 'sandbox' as const,
569
+ getDefinition: () => ({ name, description: 'high-risk skill tool', input_schema: { type: 'object' as const, properties: {} } }),
570
+ execute: async () => fakeToolResult,
571
+ };
572
+ };
573
+
574
+ // User chooses always_allow (NOT always_allow_high_risk) — the rule
575
+ // should NOT have allowHighRisk set, meaning future high-risk checks
576
+ // will still prompt.
577
+ const prompter = makePrompterWithDecision('always_allow', 'risky_op:*', '/tmp/project');
578
+ const executor = new ToolExecutor(prompter);
579
+ const result = await executor.execute('risky_op', {}, makeContext());
580
+
581
+ expect(result.isError).toBe(false);
582
+ expect(spy).toHaveBeenCalledTimes(1);
583
+ const [,,,, , options] = spy.mock.calls[0];
584
+ expect(options).toBeDefined();
585
+ // executionTarget should be present
586
+ expect(options.executionTarget).toBe('sandbox');
587
+ // But allowHighRisk should NOT be set
588
+ expect(options.allowHighRisk).toBeUndefined();
589
+ });
590
+
591
+ test('executor forwards policyContext to check() for version-bound skill tool', async () => {
592
+ getToolOverride = (name: string) => {
593
+ if (name === 'unknown_tool') return undefined;
594
+ return {
595
+ name,
596
+ description: 'versioned skill tool',
597
+ category: 'skill',
598
+ defaultRiskLevel: RiskLevel.Low,
599
+ origin: 'skill' as const,
600
+ ownerSkillId: 'versioned-skill',
601
+ ownerSkillVersionHash: 'v3:content-hash-xyz',
602
+ executionTarget: 'sandbox' as const,
603
+ getDefinition: () => ({ name, description: 'versioned skill tool', input_schema: { type: 'object' as const, properties: {} } }),
604
+ execute: async () => fakeToolResult,
605
+ };
606
+ };
607
+
608
+ const executor = new ToolExecutor(makePrompter());
609
+ await executor.execute('versioned_tool', { action: 'test' }, makeContext());
610
+
611
+ expect(lastCheckArgs).toBeDefined();
612
+ expect(lastCheckArgs!.policyContext).toEqual({
613
+ executionTarget: 'sandbox',
614
+ });
615
+ });
616
+
617
+ // ── Skill mutation approval regression tests (PR 30) ──────────
618
+
619
+ test('always_allow_high_risk for skill source write creates rule with allowHighRisk and execution target', async () => {
620
+ checkResultOverride = { decision: 'prompt', reason: 'High risk: always requires approval' };
621
+ const spy = setupAddRuleSpy();
622
+
623
+ getToolOverride = (name: string) => {
624
+ if (name === 'unknown_tool') return undefined;
625
+ return {
626
+ name,
627
+ description: 'skill tool that writes to skill source',
628
+ category: 'skill',
629
+ defaultRiskLevel: RiskLevel.High,
630
+ origin: 'skill' as const,
631
+ ownerSkillId: 'code-editor-skill',
632
+ ownerSkillVersionHash: 'sha256-v1-original',
633
+ executionTarget: 'sandbox' as const,
634
+ getDefinition: () => ({ name, description: 'skill source writer', input_schema: { type: 'object' as const, properties: {} } }),
635
+ execute: async () => fakeToolResult,
636
+ };
637
+ };
638
+
639
+ const prompter = makePrompterWithDecision('always_allow_high_risk', 'file_write:*/skills/**', 'everywhere');
640
+ const executor = new ToolExecutor(prompter);
641
+ const result = await executor.execute('file_write', { path: '/tmp/skills/my-skill/executor.ts' }, makeContext());
642
+
643
+ expect(result.isError).toBe(false);
644
+ expect(spy).toHaveBeenCalledTimes(1);
645
+ const [tool, pattern, scope, decision, , options] = spy.mock.calls[0];
646
+ expect(tool).toBe('file_write');
647
+ expect(pattern).toBe('file_write:*/skills/**');
648
+ expect(scope).toBe('everywhere');
649
+ expect(decision).toBe('allow');
650
+ expect(options.allowHighRisk).toBe(true);
651
+ expect(options.executionTarget).toBe('sandbox');
652
+ });
653
+
654
+ test('always_allow (not high risk) for skill source write creates rule WITHOUT allowHighRisk', async () => {
655
+ checkResultOverride = { decision: 'prompt', reason: 'High risk: always requires approval' };
656
+ const spy = setupAddRuleSpy();
657
+
658
+ getToolOverride = (name: string) => {
659
+ if (name === 'unknown_tool') return undefined;
660
+ return {
661
+ name,
662
+ description: 'skill tool that writes to skill source',
663
+ category: 'skill',
664
+ defaultRiskLevel: RiskLevel.High,
665
+ origin: 'skill' as const,
666
+ ownerSkillId: 'editor-skill',
667
+ ownerSkillVersionHash: 'sha256-editor-v1',
668
+ executionTarget: 'sandbox' as const,
669
+ getDefinition: () => ({ name, description: 'skill source writer', input_schema: { type: 'object' as const, properties: {} } }),
670
+ execute: async () => fakeToolResult,
671
+ };
672
+ };
673
+
674
+ // User chooses always_allow instead of always_allow_high_risk
675
+ const prompter = makePrompterWithDecision('always_allow', 'file_write:*/skills/**', '/tmp/project');
676
+ const executor = new ToolExecutor(prompter);
677
+ const result = await executor.execute('file_write', { path: '/tmp/skills/my-skill/executor.ts' }, makeContext());
678
+
679
+ expect(result.isError).toBe(false);
680
+ expect(spy).toHaveBeenCalledTimes(1);
681
+ const [,,,, , options] = spy.mock.calls[0];
682
+ expect(options).toBeDefined();
683
+ expect(options.executionTarget).toBe('sandbox');
684
+ // Without always_allow_high_risk, the allowHighRisk flag should NOT be set
685
+ expect(options.allowHighRisk).toBeUndefined();
686
+ });
687
+
688
+ test('skill version is captured in rule for future version-bound matching', async () => {
689
+ checkResultOverride = { decision: 'prompt', reason: 'High risk: always requires approval' };
690
+ const spy = setupAddRuleSpy();
691
+
692
+ getToolOverride = (name: string) => {
693
+ if (name === 'unknown_tool') return undefined;
694
+ return {
695
+ name,
696
+ description: 'versioned skill tool',
697
+ category: 'skill',
698
+ defaultRiskLevel: RiskLevel.High,
699
+ origin: 'skill' as const,
700
+ ownerSkillId: 'versioned-editor',
701
+ ownerSkillVersionHash: 'v3:content-hash-xyz789',
702
+ executionTarget: 'sandbox' as const,
703
+ getDefinition: () => ({ name, description: 'versioned skill editor', input_schema: { type: 'object' as const, properties: {} } }),
704
+ execute: async () => fakeToolResult,
705
+ };
706
+ };
707
+
708
+ const prompter = makePrompterWithDecision('always_allow_high_risk', 'file_edit:*/skills/**', 'everywhere');
709
+ const executor = new ToolExecutor(prompter);
710
+ const result = await executor.execute('file_edit', { path: '/tmp/skills/my-skill/SKILL.md' }, makeContext());
711
+
712
+ expect(result.isError).toBe(false);
713
+ expect(spy).toHaveBeenCalledTimes(1);
714
+ const [tool, , , , , options] = spy.mock.calls[0];
715
+ expect(tool).toBe('file_edit');
716
+ expect(options.allowHighRisk).toBe(true);
717
+ expect(options.executionTarget).toBe('sandbox');
718
+ });
719
+
720
+ test('executor forwards policyContext with version for skill source mutation', async () => {
721
+ getToolOverride = (name: string) => {
722
+ if (name === 'unknown_tool') return undefined;
723
+ return {
724
+ name,
725
+ description: 'skill source editor',
726
+ category: 'skill',
727
+ defaultRiskLevel: RiskLevel.High,
728
+ origin: 'skill' as const,
729
+ ownerSkillId: 'editor-skill',
730
+ ownerSkillVersionHash: 'sha256-v2-updated',
731
+ executionTarget: 'sandbox' as const,
732
+ getDefinition: () => ({ name, description: 'skill editor', input_schema: { type: 'object' as const, properties: {} } }),
733
+ execute: async () => fakeToolResult,
734
+ };
735
+ };
736
+
737
+ const executor = new ToolExecutor(makePrompter());
738
+ await executor.execute('file_write', { path: '/tmp/skills/my-skill/index.ts' }, makeContext());
739
+
740
+ expect(lastCheckArgs).toBeDefined();
741
+ expect(lastCheckArgs!.policyContext).toEqual({
742
+ executionTarget: 'sandbox',
743
+ });
744
+ });
745
+
746
+ test('executor creates rule on always_allow_high_risk with full context', async () => {
747
+ checkResultOverride = { decision: 'prompt', reason: 'High risk: always requires approval' };
748
+ const spy = setupAddRuleSpy();
749
+
750
+ getToolOverride = (name: string) => {
751
+ if (name === 'unknown_tool') return undefined;
752
+ return {
753
+ name,
754
+ description: 'admin skill tool',
755
+ category: 'skill',
756
+ defaultRiskLevel: RiskLevel.High,
757
+ origin: 'skill' as const,
758
+ ownerSkillId: 'admin-skill',
759
+ ownerSkillVersionHash: 'sha256-admin-v2',
760
+ executionTarget: 'host' as const,
761
+ getDefinition: () => ({ name, description: 'admin skill tool', input_schema: { type: 'object' as const, properties: {} } }),
762
+ execute: async () => fakeToolResult,
763
+ };
764
+ };
765
+
766
+ const prompter = makePrompterWithDecision('always_allow_high_risk', 'admin_action:*', 'everywhere');
767
+ const executor = new ToolExecutor(prompter);
768
+ const result = await executor.execute('admin_action', { op: 'restart' }, makeContext());
769
+
770
+ expect(result.isError).toBe(false);
771
+ expect(spy).toHaveBeenCalledTimes(1);
772
+ const [tool, pattern, scope, decision, , options] = spy.mock.calls[0];
773
+
774
+ // Verify complete integration of all fields
775
+ expect(tool).toBe('admin_action');
776
+ expect(pattern).toBe('admin_action:*');
777
+ expect(scope).toBe('everywhere');
778
+ expect(decision).toBe('allow');
779
+ expect(options.allowHighRisk).toBe(true);
780
+ expect(options.executionTarget).toBe('host');
781
+ });
782
+ });
783
+
784
+ // ---------------------------------------------------------------------------
785
+ // isSideEffectTool classifier
786
+ // ---------------------------------------------------------------------------
787
+
788
+ describe('isSideEffectTool', () => {
789
+ describe('returns true for side-effect tools', () => {
790
+ const sideEffectTools = [
791
+ 'file_write',
792
+ 'file_edit',
793
+ 'host_file_write',
794
+ 'host_file_edit',
795
+ 'bash',
796
+ 'host_bash',
797
+ 'web_fetch',
798
+ 'browser_navigate',
799
+ 'browser_click',
800
+ 'browser_type',
801
+ 'browser_press_key',
802
+ 'browser_close',
803
+ 'browser_fill_credential',
804
+ 'document_create',
805
+ 'document_update',
806
+ 'schedule_create',
807
+ 'schedule_update',
808
+ 'schedule_delete',
809
+ ];
810
+
811
+ for (const toolName of sideEffectTools) {
812
+ test(toolName, () => {
813
+ expect(isSideEffectTool(toolName)).toBe(true);
814
+ });
815
+ }
816
+ });
817
+
818
+ describe('returns false for non-side-effect tools', () => {
819
+ const readOnlyTools = [
820
+ 'file_read',
821
+ 'memory_search',
822
+ 'memory_save',
823
+ 'web_search',
824
+ 'browser_snapshot',
825
+ 'browser_screenshot',
826
+ 'browser_wait_for',
827
+ 'browser_extract',
828
+ 'skill_load',
829
+ 'schedule_list',
830
+ 'evaluate_typescript_code',
831
+ ];
832
+
833
+ for (const toolName of readOnlyTools) {
834
+ test(toolName, () => {
835
+ expect(isSideEffectTool(toolName)).toBe(false);
836
+ });
837
+ }
838
+ });
839
+
840
+ test('returns false for unknown tool names', () => {
841
+ expect(isSideEffectTool('nonexistent_tool')).toBe(false);
842
+ expect(isSideEffectTool('')).toBe(false);
843
+ });
844
+
845
+ describe('action-aware classification for mixed-action tools', () => {
846
+ test('account_manage create is a side-effect', () => {
847
+ expect(isSideEffectTool('account_manage', { action: 'create' })).toBe(true);
848
+ });
849
+
850
+ test('account_manage update is a side-effect', () => {
851
+ expect(isSideEffectTool('account_manage', { action: 'update' })).toBe(true);
852
+ });
853
+
854
+ test('account_manage list is NOT a side-effect', () => {
855
+ expect(isSideEffectTool('account_manage', { action: 'list' })).toBe(false);
856
+ });
857
+
858
+ test('account_manage get is NOT a side-effect', () => {
859
+ expect(isSideEffectTool('account_manage', { action: 'get' })).toBe(false);
860
+ });
861
+
862
+ test('account_manage without input is NOT a side-effect', () => {
863
+ expect(isSideEffectTool('account_manage')).toBe(false);
864
+ });
865
+
866
+ test('reminder_create is a side-effect', () => {
867
+ expect(isSideEffectTool('reminder_create')).toBe(true);
868
+ });
869
+
870
+ test('reminder_cancel is a side-effect', () => {
871
+ expect(isSideEffectTool('reminder_cancel')).toBe(true);
872
+ });
873
+
874
+ test('reminder_list is NOT a side-effect', () => {
875
+ expect(isSideEffectTool('reminder_list')).toBe(false);
876
+ });
877
+
878
+ test('credential_store store is a side-effect', () => {
879
+ expect(isSideEffectTool('credential_store', { action: 'store' })).toBe(true);
880
+ });
881
+
882
+ test('credential_store delete is a side-effect', () => {
883
+ expect(isSideEffectTool('credential_store', { action: 'delete' })).toBe(true);
884
+ });
885
+
886
+ test('credential_store prompt is a side-effect', () => {
887
+ expect(isSideEffectTool('credential_store', { action: 'prompt' })).toBe(true);
888
+ });
889
+
890
+ test('credential_store oauth2_connect is a side-effect', () => {
891
+ expect(isSideEffectTool('credential_store', { action: 'oauth2_connect' })).toBe(true);
892
+ });
893
+
894
+ test('credential_store list is NOT a side-effect', () => {
895
+ expect(isSideEffectTool('credential_store', { action: 'list' })).toBe(false);
896
+ });
897
+
898
+ test('credential_store without input is NOT a side-effect', () => {
899
+ expect(isSideEffectTool('credential_store')).toBe(false);
900
+ });
901
+ });
902
+ });
903
+
904
+ // Baseline: allow rules can auto-allow file_edit for USER.md today (no forced prompting).
905
+ // The mock check() delegates to findHighestPriorityRule (via spy) so a regression
906
+ // in trust-rule matching would cause this test to fail instead of being masked by
907
+ // a blanket mock-allow.
908
+ describe('ToolExecutor baseline: allow rule auto-allows file_edit USER.md', () => {
909
+ const userMdPath = '/Users/sidd/.vellum/workspace/USER.md';
910
+ let ruleSpy: ReturnType<typeof spyOn> | undefined;
911
+
912
+ beforeEach(() => {
913
+ fakeToolResult = { content: 'ok', isError: false };
914
+ lastCheckArgs = undefined;
915
+ getToolOverride = undefined;
916
+ checkResultOverride = undefined;
917
+ checkFnOverride = undefined;
918
+ if (addRuleSpy) { addRuleSpy.mockRestore(); addRuleSpy = undefined; }
919
+
920
+ // Simulate a trust rule that allows file_edit on USER.md by stubbing
921
+ // findHighestPriorityRule. This mirrors the default allow rules that
922
+ // the trust-store creates for workspace prompt files.
923
+ ruleSpy = spyOn(trustStore, 'findHighestPriorityRule').mockImplementation(
924
+ (tool: string, commands: string[], _scope: string) => {
925
+ if (tool !== 'file_edit') return null;
926
+ for (const cmd of commands) {
927
+ if (cmd === `file_edit:${userMdPath}`) {
928
+ return {
929
+ id: 'default:allow-file_edit-user',
930
+ tool: 'file_edit',
931
+ pattern: `file_edit:${userMdPath}`,
932
+ scope: 'everywhere',
933
+ decision: 'allow' as const,
934
+ priority: 100,
935
+ createdAt: Date.now(),
936
+ };
937
+ }
938
+ }
939
+ return null;
940
+ },
941
+ );
942
+
943
+ // Wire the mock check() to delegate to findHighestPriorityRule, replicating
944
+ // the real check() logic for Medium-risk tools (file_edit).
945
+ checkFnOverride = async (toolName, input, workingDir) => {
946
+ const filePath = (input.path as string) ?? (input.file_path as string) ?? '';
947
+ const resolved = filePath.startsWith('/') ? filePath : `${workingDir}/${filePath}`;
948
+ const candidates = [`${toolName}:${resolved}`];
949
+ const matched = trustStore.findHighestPriorityRule(toolName, candidates, workingDir);
950
+ if (matched && matched.decision === 'allow') {
951
+ return { decision: 'allow', reason: `Matched trust rule: ${matched.pattern}` };
952
+ }
953
+ return { decision: 'prompt', reason: 'Medium risk: requires approval' };
954
+ };
955
+ });
956
+
957
+ afterEach(() => {
958
+ checkFnOverride = undefined;
959
+ if (ruleSpy) { ruleSpy.mockRestore(); ruleSpy = undefined; }
960
+ });
961
+
962
+ test('file_edit to USER.md is auto-allowed via trust rule', async () => {
963
+ const executor = new ToolExecutor(makePrompter());
964
+ const result = await executor.execute(
965
+ 'file_edit',
966
+ { path: userMdPath, content: 'hello' },
967
+ makeContext(),
968
+ );
969
+ expect(result.isError).toBe(false);
970
+ expect(result.content).toBe('ok');
971
+ // Confirm checker was called with the correct tool name
972
+ expect(lastCheckArgs).toBeDefined();
973
+ expect(lastCheckArgs!.toolName).toBe('file_edit');
974
+ // Confirm findHighestPriorityRule was consulted
975
+ expect(ruleSpy).toHaveBeenCalled();
976
+ });
977
+
978
+ test('file_edit to a non-USER.md path is NOT auto-allowed without a matching rule', async () => {
979
+ let promptCalled = false;
980
+ const trackingPrompter = {
981
+ prompt: async () => { promptCalled = true; return { decision: 'allow' as const }; },
982
+ resolveConfirmation: () => {},
983
+ updateSender: () => {},
984
+ dispose: () => {},
985
+ } as unknown as PermissionPrompter;
986
+
987
+ const executor = new ToolExecutor(trackingPrompter);
988
+ const result = await executor.execute(
989
+ 'file_edit',
990
+ { path: '/tmp/project/other.md', content: 'hello' },
991
+ makeContext(),
992
+ );
993
+ // check() returned 'prompt' (no matching trust rule for other.md),
994
+ // so the executor must have called the prompter.
995
+ expect(promptCalled).toBe(true);
996
+ expect(result.isError).toBe(false);
997
+ expect(lastCheckArgs).toBeDefined();
998
+ expect(lastCheckArgs!.toolName).toBe('file_edit');
999
+ });
1000
+ });
1001
+
1002
+ // ---------------------------------------------------------------------------
1003
+ // forcePromptSideEffects enforcement (PR 30)
1004
+ // ---------------------------------------------------------------------------
1005
+
1006
+ describe('ToolExecutor forcePromptSideEffects enforcement', () => {
1007
+ let promptCalled: boolean;
1008
+
1009
+ beforeEach(() => {
1010
+ fakeToolResult = { content: 'ok', isError: false };
1011
+ lastCheckArgs = undefined;
1012
+ getToolOverride = undefined;
1013
+ checkResultOverride = undefined;
1014
+ checkFnOverride = undefined;
1015
+ promptCalled = false;
1016
+ if (addRuleSpy) { addRuleSpy.mockRestore(); addRuleSpy = undefined; }
1017
+ });
1018
+
1019
+ /**
1020
+ * Prompter that tracks whether it was called and always allows.
1021
+ */
1022
+ function makeTrackingPrompter(): PermissionPrompter {
1023
+ return {
1024
+ prompt: async () => {
1025
+ promptCalled = true;
1026
+ return { decision: 'allow' as const };
1027
+ },
1028
+ resolveConfirmation: () => {},
1029
+ updateSender: () => {},
1030
+ dispose: () => {},
1031
+ } as unknown as PermissionPrompter;
1032
+ }
1033
+
1034
+ test('side-effect tool with allow rule is forced to prompt when forcePromptSideEffects is true', async () => {
1035
+ // check() returns allow (simulating a matched trust rule)
1036
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1037
+
1038
+ const executor = new ToolExecutor(makeTrackingPrompter());
1039
+ const result = await executor.execute(
1040
+ 'bash',
1041
+ { command: 'echo hello' },
1042
+ makeContext({ forcePromptSideEffects: true }),
1043
+ );
1044
+
1045
+ expect(result.isError).toBe(false);
1046
+ // The prompter must have been called despite the allow rule
1047
+ expect(promptCalled).toBe(true);
1048
+ });
1049
+
1050
+ test('deny decision is preserved (not converted to prompt) even with forcePromptSideEffects', async () => {
1051
+ checkResultOverride = { decision: 'deny', reason: 'Policy denies this tool' };
1052
+
1053
+ const executor = new ToolExecutor(makeTrackingPrompter());
1054
+ const result = await executor.execute(
1055
+ 'bash',
1056
+ { command: 'rm -rf /' },
1057
+ makeContext({ forcePromptSideEffects: true }),
1058
+ );
1059
+
1060
+ // Should be denied, not prompted
1061
+ expect(result.isError).toBe(true);
1062
+ expect(result.content).toBe('Policy denies this tool');
1063
+ expect(promptCalled).toBe(false);
1064
+ });
1065
+
1066
+ test('non-side-effect tool is unchanged even with forcePromptSideEffects', async () => {
1067
+ // check() returns allow for a read-only tool
1068
+ checkResultOverride = { decision: 'allow', reason: 'Allowed by default' };
1069
+
1070
+ const executor = new ToolExecutor(makeTrackingPrompter());
1071
+ const result = await executor.execute(
1072
+ 'file_read',
1073
+ { path: 'README.md' },
1074
+ makeContext({ forcePromptSideEffects: true }),
1075
+ );
1076
+
1077
+ expect(result.isError).toBe(false);
1078
+ // Prompter should NOT be called — file_read is not a side-effect tool
1079
+ expect(promptCalled).toBe(false);
1080
+ });
1081
+
1082
+ test('side-effect tool is auto-allowed when forcePromptSideEffects is false', async () => {
1083
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1084
+
1085
+ const executor = new ToolExecutor(makeTrackingPrompter());
1086
+ const result = await executor.execute(
1087
+ 'file_write',
1088
+ { path: 'test.txt', content: 'data' },
1089
+ makeContext({ forcePromptSideEffects: false }),
1090
+ );
1091
+
1092
+ expect(result.isError).toBe(false);
1093
+ // No prompt — standard behavior when forcePromptSideEffects is off
1094
+ expect(promptCalled).toBe(false);
1095
+ });
1096
+
1097
+ test('side-effect tool is auto-allowed when forcePromptSideEffects is undefined', async () => {
1098
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1099
+
1100
+ const executor = new ToolExecutor(makeTrackingPrompter());
1101
+ const result = await executor.execute(
1102
+ 'file_edit',
1103
+ { path: 'test.txt', old_string: 'a', new_string: 'b' },
1104
+ makeContext(), // forcePromptSideEffects not set
1105
+ );
1106
+
1107
+ expect(result.isError).toBe(false);
1108
+ expect(promptCalled).toBe(false);
1109
+ });
1110
+
1111
+ test('all side-effect tool types are forced to prompt', async () => {
1112
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1113
+
1114
+ const sideEffectTools = [
1115
+ { name: 'file_write', input: { path: 'x', content: 'y' } },
1116
+ { name: 'file_edit', input: { path: 'x', old_string: 'a', new_string: 'b' } },
1117
+ { name: 'host_file_write', input: { path: 'x', content: 'y' } },
1118
+ { name: 'host_file_edit', input: { path: 'x', old_string: 'a', new_string: 'b' } },
1119
+ { name: 'bash', input: { command: 'echo hi' } },
1120
+ { name: 'host_bash', input: { command: 'echo hi' } },
1121
+ { name: 'web_fetch', input: { url: 'https://example.com' } },
1122
+ { name: 'browser_navigate', input: { url: 'https://example.com' } },
1123
+ { name: 'browser_click', input: { selector: '#btn' } },
1124
+ { name: 'browser_type', input: { selector: '#input', text: 'hello' } },
1125
+ { name: 'browser_press_key', input: { key: 'Enter' } },
1126
+ { name: 'browser_close', input: {} },
1127
+ { name: 'browser_fill_credential', input: { selector: '#pwd', credential: 'test' } },
1128
+ { name: 'document_create', input: { title: 'doc', content: 'body' } },
1129
+ { name: 'document_update', input: { id: 'doc-1', content: 'updated' } },
1130
+ { name: 'account_manage', input: { action: 'create', name: 'acct' } },
1131
+ { name: 'reminder_create', input: { fire_at: '2030-01-01T00:00:00Z', label: 'test', message: 'remind me' } },
1132
+ { name: 'credential_store', input: { action: 'store', name: 'api-key', value: 'secret' } },
1133
+ ];
1134
+
1135
+ for (const { name, input } of sideEffectTools) {
1136
+ promptCalled = false;
1137
+ const executor = new ToolExecutor(makeTrackingPrompter());
1138
+ const result = await executor.execute(
1139
+ name,
1140
+ input,
1141
+ makeContext({ forcePromptSideEffects: true }),
1142
+ );
1143
+ expect(result.isError).toBe(false);
1144
+ expect(promptCalled).toBe(true);
1145
+ }
1146
+ });
1147
+
1148
+ test('tool that is already prompted is not double-prompted', async () => {
1149
+ // check() returns prompt (tool already needs prompting)
1150
+ checkResultOverride = { decision: 'prompt', reason: 'Medium risk: requires approval' };
1151
+
1152
+ let promptCount = 0;
1153
+ const countingPrompter = {
1154
+ prompt: async () => {
1155
+ promptCount++;
1156
+ return { decision: 'allow' as const };
1157
+ },
1158
+ resolveConfirmation: () => {},
1159
+ updateSender: () => {},
1160
+ dispose: () => {},
1161
+ } as unknown as PermissionPrompter;
1162
+
1163
+ const executor = new ToolExecutor(countingPrompter);
1164
+ const result = await executor.execute(
1165
+ 'bash',
1166
+ { command: 'ls' },
1167
+ makeContext({ forcePromptSideEffects: true }),
1168
+ );
1169
+
1170
+ expect(result.isError).toBe(false);
1171
+ // Should only prompt once — forcePromptSideEffects doesn't add a second prompt
1172
+ // when check() already returned 'prompt'
1173
+ expect(promptCount).toBe(1);
1174
+ });
1175
+
1176
+ // ── USER.md security invariant (PR 31) ──────────
1177
+
1178
+ test('file_edit to USER.md forces prompt in private thread even with matching trust rule', async () => {
1179
+ // This is a key security invariant: USER.md contains the user's persistent
1180
+ // memory. In a private thread (forcePromptSideEffects=true), edits to it
1181
+ // must always require explicit approval, even when a trust rule matches.
1182
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule: file_edit:*/USER.md' };
1183
+
1184
+ const executor = new ToolExecutor(makeTrackingPrompter());
1185
+ const result = await executor.execute(
1186
+ 'file_edit',
1187
+ { path: '/Users/sidd/.vellum/workspace/USER.md', old_string: 'old pref', new_string: 'new pref' },
1188
+ makeContext({ forcePromptSideEffects: true }),
1189
+ );
1190
+
1191
+ expect(result.isError).toBe(false);
1192
+ // file_edit is a side-effect tool, so forcePromptSideEffects must trigger prompting
1193
+ expect(promptCalled).toBe(true);
1194
+ });
1195
+
1196
+ test('host_file_edit to USER.md forces prompt in private thread', async () => {
1197
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1198
+
1199
+ const executor = new ToolExecutor(makeTrackingPrompter());
1200
+ const result = await executor.execute(
1201
+ 'host_file_edit',
1202
+ { path: '/Users/sidd/.vellum/workspace/USER.md', old_string: 'x', new_string: 'y' },
1203
+ makeContext({ forcePromptSideEffects: true }),
1204
+ );
1205
+
1206
+ expect(result.isError).toBe(false);
1207
+ expect(promptCalled).toBe(true);
1208
+ });
1209
+
1210
+ // ── Browser action tools as side-effect tools (PR fix2) ──────────
1211
+
1212
+ test('browser_click forces prompt in private thread', async () => {
1213
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1214
+
1215
+ const executor = new ToolExecutor(makeTrackingPrompter());
1216
+ const result = await executor.execute(
1217
+ 'browser_click',
1218
+ { selector: '#submit-btn' },
1219
+ makeContext({ forcePromptSideEffects: true }),
1220
+ );
1221
+
1222
+ expect(result.isError).toBe(false);
1223
+ expect(promptCalled).toBe(true);
1224
+ });
1225
+
1226
+ test('browser_type forces prompt in private thread', async () => {
1227
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1228
+
1229
+ const executor = new ToolExecutor(makeTrackingPrompter());
1230
+ const result = await executor.execute(
1231
+ 'browser_type',
1232
+ { selector: '#search-input', text: 'query' },
1233
+ makeContext({ forcePromptSideEffects: true }),
1234
+ );
1235
+
1236
+ expect(result.isError).toBe(false);
1237
+ expect(promptCalled).toBe(true);
1238
+ });
1239
+
1240
+ test('browser_snapshot does NOT force prompt in private thread', async () => {
1241
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1242
+
1243
+ const executor = new ToolExecutor(makeTrackingPrompter());
1244
+ const result = await executor.execute(
1245
+ 'browser_snapshot',
1246
+ {},
1247
+ makeContext({ forcePromptSideEffects: true }),
1248
+ );
1249
+
1250
+ expect(result.isError).toBe(false);
1251
+ // browser_snapshot is read-only — must NOT trigger forced prompting
1252
+ expect(promptCalled).toBe(false);
1253
+ });
1254
+
1255
+ // ── Always-mutating document tools (PR fix5) ──────────
1256
+
1257
+ test('document_create forces prompt in private thread', async () => {
1258
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1259
+
1260
+ const executor = new ToolExecutor(makeTrackingPrompter());
1261
+ const result = await executor.execute(
1262
+ 'document_create',
1263
+ { title: 'New Doc', content: 'hello' },
1264
+ makeContext({ forcePromptSideEffects: true }),
1265
+ );
1266
+
1267
+ expect(result.isError).toBe(false);
1268
+ expect(promptCalled).toBe(true);
1269
+ });
1270
+
1271
+ test('document_update forces prompt in private thread', async () => {
1272
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1273
+
1274
+ const executor = new ToolExecutor(makeTrackingPrompter());
1275
+ const result = await executor.execute(
1276
+ 'document_update',
1277
+ { id: 'doc-1', content: 'updated' },
1278
+ makeContext({ forcePromptSideEffects: true }),
1279
+ );
1280
+
1281
+ expect(result.isError).toBe(false);
1282
+ expect(promptCalled).toBe(true);
1283
+ });
1284
+
1285
+ // ── Always-mutating schedule tools (PR fix7) ──────────
1286
+
1287
+ test('schedule_create forces prompt in private thread', async () => {
1288
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1289
+
1290
+ const executor = new ToolExecutor(makeTrackingPrompter());
1291
+ const result = await executor.execute(
1292
+ 'schedule_create',
1293
+ { name: 'Morning standup', cron: '0 9 * * 1-5' },
1294
+ makeContext({ forcePromptSideEffects: true }),
1295
+ );
1296
+
1297
+ expect(result.isError).toBe(false);
1298
+ expect(promptCalled).toBe(true);
1299
+ });
1300
+
1301
+ test('schedule_update forces prompt in private thread', async () => {
1302
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1303
+
1304
+ const executor = new ToolExecutor(makeTrackingPrompter());
1305
+ const result = await executor.execute(
1306
+ 'schedule_update',
1307
+ { id: 'sched-1', cron: '0 10 * * 1-5' },
1308
+ makeContext({ forcePromptSideEffects: true }),
1309
+ );
1310
+
1311
+ expect(result.isError).toBe(false);
1312
+ expect(promptCalled).toBe(true);
1313
+ });
1314
+
1315
+ test('schedule_delete forces prompt in private thread', async () => {
1316
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1317
+
1318
+ const executor = new ToolExecutor(makeTrackingPrompter());
1319
+ const result = await executor.execute(
1320
+ 'schedule_delete',
1321
+ { id: 'sched-1' },
1322
+ makeContext({ forcePromptSideEffects: true }),
1323
+ );
1324
+
1325
+ expect(result.isError).toBe(false);
1326
+ expect(promptCalled).toBe(true);
1327
+ });
1328
+
1329
+ // ── Credential store action-aware (PR fix9) ──────────
1330
+
1331
+ test('credential_store store forces prompt in private thread', async () => {
1332
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1333
+
1334
+ const executor = new ToolExecutor(makeTrackingPrompter());
1335
+ const result = await executor.execute(
1336
+ 'credential_store',
1337
+ { action: 'store', name: 'api-key', value: 'sk-secret-123' },
1338
+ makeContext({ forcePromptSideEffects: true }),
1339
+ );
1340
+
1341
+ expect(result.isError).toBe(false);
1342
+ expect(promptCalled).toBe(true);
1343
+ });
1344
+
1345
+ test('credential_store delete forces prompt in private thread', async () => {
1346
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1347
+
1348
+ const executor = new ToolExecutor(makeTrackingPrompter());
1349
+ const result = await executor.execute(
1350
+ 'credential_store',
1351
+ { action: 'delete', name: 'api-key' },
1352
+ makeContext({ forcePromptSideEffects: true }),
1353
+ );
1354
+
1355
+ expect(result.isError).toBe(false);
1356
+ expect(promptCalled).toBe(true);
1357
+ });
1358
+
1359
+ test('credential_store oauth2_connect forces prompt in private thread', async () => {
1360
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1361
+
1362
+ const executor = new ToolExecutor(makeTrackingPrompter());
1363
+ const result = await executor.execute(
1364
+ 'credential_store',
1365
+ { action: 'oauth2_connect', provider: 'google' },
1366
+ makeContext({ forcePromptSideEffects: true }),
1367
+ );
1368
+
1369
+ expect(result.isError).toBe(false);
1370
+ expect(promptCalled).toBe(true);
1371
+ });
1372
+
1373
+ test('credential_store list does NOT force prompt in private thread', async () => {
1374
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1375
+
1376
+ const executor = new ToolExecutor(makeTrackingPrompter());
1377
+ const result = await executor.execute(
1378
+ 'credential_store',
1379
+ { action: 'list' },
1380
+ makeContext({ forcePromptSideEffects: true }),
1381
+ );
1382
+
1383
+ expect(result.isError).toBe(false);
1384
+ // list is read-only — must NOT trigger forced prompting
1385
+ expect(promptCalled).toBe(false);
1386
+ });
1387
+
1388
+ // ── Workspace mode + forcePromptSideEffects interaction ──────────
1389
+
1390
+ test('workspace mode allow → prompt promotion still works for side-effect tools in private threads', async () => {
1391
+ // Simulate workspace mode returning 'allow' for a workspace-scoped file_write
1392
+ checkResultOverride = { decision: 'allow', reason: 'Workspace mode: workspace-scoped operation auto-allowed' };
1393
+
1394
+ const executor = new ToolExecutor(makeTrackingPrompter());
1395
+ const result = await executor.execute(
1396
+ 'file_write',
1397
+ { path: '/tmp/project/test.txt', content: 'data' },
1398
+ makeContext({ forcePromptSideEffects: true }),
1399
+ );
1400
+
1401
+ expect(result.isError).toBe(false);
1402
+ // file_write is a side-effect tool, so forcePromptSideEffects must promote
1403
+ // the workspace mode allow → prompt, requiring explicit user approval
1404
+ expect(promptCalled).toBe(true);
1405
+ });
1406
+
1407
+ // ── Action-aware mixed-action tools (PR fix5) ──────────
1408
+
1409
+ test('account_manage create forces prompt in private thread', async () => {
1410
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1411
+
1412
+ const executor = new ToolExecutor(makeTrackingPrompter());
1413
+ const result = await executor.execute(
1414
+ 'account_manage',
1415
+ { action: 'create', name: 'test-account' },
1416
+ makeContext({ forcePromptSideEffects: true }),
1417
+ );
1418
+
1419
+ expect(result.isError).toBe(false);
1420
+ expect(promptCalled).toBe(true);
1421
+ });
1422
+
1423
+ test('account_manage list does NOT force prompt in private thread', async () => {
1424
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1425
+
1426
+ const executor = new ToolExecutor(makeTrackingPrompter());
1427
+ const result = await executor.execute(
1428
+ 'account_manage',
1429
+ { action: 'list' },
1430
+ makeContext({ forcePromptSideEffects: true }),
1431
+ );
1432
+
1433
+ expect(result.isError).toBe(false);
1434
+ // list is read-only — must NOT trigger forced prompting
1435
+ expect(promptCalled).toBe(false);
1436
+ });
1437
+
1438
+ test('reminder_create forces prompt in private thread', async () => {
1439
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1440
+
1441
+ const executor = new ToolExecutor(makeTrackingPrompter());
1442
+ const result = await executor.execute(
1443
+ 'reminder_create',
1444
+ { fire_at: '2030-01-01T00:00:00Z', label: 'test', message: 'test reminder' },
1445
+ makeContext({ forcePromptSideEffects: true }),
1446
+ );
1447
+
1448
+ expect(result.isError).toBe(false);
1449
+ expect(promptCalled).toBe(true);
1450
+ });
1451
+
1452
+ test('reminder_list does NOT force prompt in private thread', async () => {
1453
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule' };
1454
+
1455
+ const executor = new ToolExecutor(makeTrackingPrompter());
1456
+ const result = await executor.execute(
1457
+ 'reminder_list',
1458
+ {},
1459
+ makeContext({ forcePromptSideEffects: true }),
1460
+ );
1461
+
1462
+ expect(result.isError).toBe(false);
1463
+ // list is read-only — must NOT trigger forced prompting
1464
+ expect(promptCalled).toBe(false);
1465
+ });
1466
+ });
1467
+
1468
+ // ---------------------------------------------------------------------------
1469
+ // persistentDecisionsAllowed contract (PR 15)
1470
+ // ---------------------------------------------------------------------------
1471
+
1472
+ describe('ToolExecutor persistentDecisionsAllowed contract', () => {
1473
+ beforeEach(() => {
1474
+ fakeToolResult = { content: 'ok', isError: false };
1475
+ lastCheckArgs = undefined;
1476
+ getToolOverride = undefined;
1477
+ checkResultOverride = { decision: 'prompt', reason: 'Proxied network mode requires explicit approval for each invocation.' };
1478
+ checkFnOverride = undefined;
1479
+ if (addRuleSpy) { addRuleSpy.mockRestore(); addRuleSpy = undefined; }
1480
+ });
1481
+
1482
+ function setupAddRuleSpy() {
1483
+ addRuleSpy = spyOn(trustStore, 'addRule').mockImplementation(
1484
+ (tool: string, pattern: string, scope: string, decision = 'allow', priority = 100, options?: any) => {
1485
+ return { id: 'spy-rule-id', tool, pattern, scope, decision, priority, createdAt: Date.now(), ...options } as any;
1486
+ },
1487
+ );
1488
+ return addRuleSpy;
1489
+ }
1490
+
1491
+ test('proxied bash always_allow does NOT save a trust rule', async () => {
1492
+ const spy = setupAddRuleSpy();
1493
+
1494
+ const prompter = makePrompterWithDecision('always_allow', 'bash:*', '/tmp/project');
1495
+ const executor = new ToolExecutor(prompter);
1496
+ const result = await executor.execute(
1497
+ 'bash',
1498
+ { command: 'curl https://example.com', network_mode: 'proxied' },
1499
+ makeContext(),
1500
+ );
1501
+
1502
+ expect(result.isError).toBe(false);
1503
+ expect(spy).not.toHaveBeenCalled();
1504
+ });
1505
+
1506
+ test('non-proxied bash always_allow DOES save a trust rule', async () => {
1507
+ const spy = setupAddRuleSpy();
1508
+
1509
+ const prompter = makePrompterWithDecision('always_allow', 'bash:*', '/tmp/project');
1510
+ const executor = new ToolExecutor(prompter);
1511
+ const result = await executor.execute(
1512
+ 'bash',
1513
+ { command: 'git status' },
1514
+ makeContext(),
1515
+ );
1516
+
1517
+ expect(result.isError).toBe(false);
1518
+ expect(spy).toHaveBeenCalledTimes(1);
1519
+ });
1520
+
1521
+ test('proxied bash always_deny does NOT save a trust rule', async () => {
1522
+ const spy = setupAddRuleSpy();
1523
+
1524
+ const prompter = makePrompterWithDecision('always_deny', 'bash:*', '/tmp/project');
1525
+ const executor = new ToolExecutor(prompter);
1526
+ const result = await executor.execute(
1527
+ 'bash',
1528
+ { command: 'curl https://evil.com', network_mode: 'proxied' },
1529
+ makeContext(),
1530
+ );
1531
+
1532
+ expect(result.isError).toBe(true);
1533
+ expect(spy).not.toHaveBeenCalled();
1534
+ });
1535
+
1536
+ test('persistentDecisionsAllowed: false is emitted in lifecycle event for proxied bash', async () => {
1537
+ let capturedEvent: any;
1538
+ const prompter = makePrompterWithDecision('allow');
1539
+ const executor = new ToolExecutor(prompter);
1540
+ const result = await executor.execute(
1541
+ 'bash',
1542
+ { command: 'curl https://example.com', network_mode: 'proxied' },
1543
+ makeContext({
1544
+ onToolLifecycleEvent: (event: any) => {
1545
+ if (event.type === 'permission_prompt') {
1546
+ capturedEvent = event;
1547
+ }
1548
+ },
1549
+ }),
1550
+ );
1551
+
1552
+ expect(result.isError).toBe(false);
1553
+ expect(capturedEvent).toBeDefined();
1554
+ expect(capturedEvent.persistentDecisionsAllowed).toBe(false);
1555
+ });
1556
+
1557
+ test('persistentDecisionsAllowed: true is emitted in lifecycle event for non-proxied bash', async () => {
1558
+ let capturedEvent: any;
1559
+ const prompter = makePrompterWithDecision('allow');
1560
+ const executor = new ToolExecutor(prompter);
1561
+ const result = await executor.execute(
1562
+ 'bash',
1563
+ { command: 'echo hello' },
1564
+ makeContext({
1565
+ onToolLifecycleEvent: (event: any) => {
1566
+ if (event.type === 'permission_prompt') {
1567
+ capturedEvent = event;
1568
+ }
1569
+ },
1570
+ }),
1571
+ );
1572
+
1573
+ expect(result.isError).toBe(false);
1574
+ expect(capturedEvent).toBeDefined();
1575
+ expect(capturedEvent.persistentDecisionsAllowed).toBe(true);
1576
+ });
1577
+
1578
+ test('persistentDecisionsAllowed is passed to prompter confirmation_request for proxied bash', async () => {
1579
+ let capturedPersistent: unknown;
1580
+ const prompter = {
1581
+ prompt: async (
1582
+ _toolName: string, _input: Record<string, unknown>, _riskLevel: string,
1583
+ _allowlistOptions: any[], _scopeOptions: any[], _diff: any, _sandboxed: any,
1584
+ _sessionId: any, _executionTarget: any, persistentDecisionsAllowed: any,
1585
+ ) => {
1586
+ capturedPersistent = persistentDecisionsAllowed;
1587
+ return { decision: 'allow' as const };
1588
+ },
1589
+ resolveConfirmation: () => {},
1590
+ updateSender: () => {},
1591
+ dispose: () => {},
1592
+ } as unknown as PermissionPrompter;
1593
+
1594
+ const executor = new ToolExecutor(prompter);
1595
+ const result = await executor.execute(
1596
+ 'bash',
1597
+ { command: 'curl https://example.com', network_mode: 'proxied' },
1598
+ makeContext(),
1599
+ );
1600
+
1601
+ expect(result.isError).toBe(false);
1602
+ expect(capturedPersistent).toBe(false);
1603
+ });
1604
+
1605
+ test('host_bash with proxied network_mode still allows persistent decisions', async () => {
1606
+ // host_bash does not support network_mode — proxied-mode persistence
1607
+ // blocking applies only to sandboxed bash, not host_bash.
1608
+ const spy = setupAddRuleSpy();
1609
+
1610
+ const prompter = makePrompterWithDecision('always_allow', 'host_bash:*', '/tmp/project');
1611
+ const executor = new ToolExecutor(prompter);
1612
+ const result = await executor.execute(
1613
+ 'host_bash',
1614
+ { command: 'curl https://example.com', network_mode: 'proxied' },
1615
+ makeContext(),
1616
+ );
1617
+
1618
+ expect(result.isError).toBe(false);
1619
+ expect(spy).toHaveBeenCalledTimes(1);
1620
+ });
1621
+ });
1622
+
1623
+ // ---------------------------------------------------------------------------
1624
+ // E2E: Proxied bash vs. proxy approval persistence invariants (PR 32)
1625
+ //
1626
+ // Design invariant: the proxied-run activation prompt (when the agent wants
1627
+ // to run `bash` with `network_mode=proxied`) must NOT allow saving persistent
1628
+ // trust rules — each invocation must be explicitly approved. In contrast,
1629
+ // the proxy-request approval path (when the proxy service asks the user
1630
+ // about a specific outbound request) CAN save persistent trust rules so the
1631
+ // user doesn't get re-prompted for the same host/pattern.
1632
+ // ---------------------------------------------------------------------------
1633
+
1634
+ describe('E2E: proxied bash activation vs proxy approval persistence', () => {
1635
+ beforeEach(() => {
1636
+ fakeToolResult = { content: 'ok', isError: false };
1637
+ lastCheckArgs = undefined;
1638
+ getToolOverride = undefined;
1639
+ checkResultOverride = { decision: 'prompt', reason: 'Requires explicit approval' };
1640
+ checkFnOverride = undefined;
1641
+ if (addRuleSpy) { addRuleSpy.mockRestore(); addRuleSpy = undefined; }
1642
+ });
1643
+
1644
+ function setupAddRuleSpy() {
1645
+ addRuleSpy = spyOn(trustStore, 'addRule').mockImplementation(
1646
+ (tool: string, pattern: string, scope: string, decision = 'allow', priority = 100, options?: any) => {
1647
+ return { id: 'spy-rule-id', tool, pattern, scope, decision, priority, createdAt: Date.now(), ...options } as any;
1648
+ },
1649
+ );
1650
+ return addRuleSpy;
1651
+ }
1652
+
1653
+ test('proxied bash: always_allow skips rule, always_deny skips rule, non-proxied bash saves both', async () => {
1654
+ const spy = setupAddRuleSpy();
1655
+
1656
+ // 1. Proxied bash always_allow -> NO rule saved
1657
+ const p1 = makePrompterWithDecision('always_allow', 'bash:curl*', '/tmp/project');
1658
+ const e1 = new ToolExecutor(p1);
1659
+ const r1 = await e1.execute('bash', { command: 'curl https://api.example.com', network_mode: 'proxied' }, makeContext());
1660
+ expect(r1.isError).toBe(false);
1661
+ expect(spy).not.toHaveBeenCalled();
1662
+
1663
+ // 2. Proxied bash always_deny -> NO rule saved
1664
+ const p2 = makePrompterWithDecision('always_deny', 'bash:curl*', '/tmp/project');
1665
+ const e2 = new ToolExecutor(p2);
1666
+ const r2 = await e2.execute('bash', { command: 'curl https://evil.com', network_mode: 'proxied' }, makeContext());
1667
+ expect(r2.isError).toBe(true);
1668
+ expect(spy).not.toHaveBeenCalled();
1669
+
1670
+ // 3. Non-proxied bash always_allow -> rule IS saved
1671
+ const p3 = makePrompterWithDecision('always_allow', 'bash:git*', '/tmp/project');
1672
+ const e3 = new ToolExecutor(p3);
1673
+ const r3 = await e3.execute('bash', { command: 'git push' }, makeContext());
1674
+ expect(r3.isError).toBe(false);
1675
+ expect(spy).toHaveBeenCalledTimes(1);
1676
+ expect(spy.mock.calls[0][0]).toBe('bash');
1677
+ expect(spy.mock.calls[0][1]).toBe('bash:git*');
1678
+ expect(spy.mock.calls[0][3]).toBe('allow');
1679
+ });
1680
+
1681
+ test('proxied bash always_allow_high_risk also skips rule saving', async () => {
1682
+ const spy = setupAddRuleSpy();
1683
+
1684
+ const prompter = makePrompterWithDecision('always_allow_high_risk', 'bash:*', 'everywhere');
1685
+ const executor = new ToolExecutor(prompter);
1686
+ const result = await executor.execute(
1687
+ 'bash',
1688
+ { command: 'curl -X POST https://api.example.com/deploy', network_mode: 'proxied' },
1689
+ makeContext(),
1690
+ );
1691
+
1692
+ expect(result.isError).toBe(false);
1693
+ expect(spy).not.toHaveBeenCalled();
1694
+ });
1695
+
1696
+ test('non-proxied bash always_deny DOES save a deny rule', async () => {
1697
+ const spy = setupAddRuleSpy();
1698
+
1699
+ const prompter = makePrompterWithDecision('always_deny', 'bash:rm*', '/tmp/project');
1700
+ const executor = new ToolExecutor(prompter);
1701
+ const result = await executor.execute(
1702
+ 'bash',
1703
+ { command: 'rm -rf /' },
1704
+ makeContext(),
1705
+ );
1706
+
1707
+ expect(result.isError).toBe(true);
1708
+ expect(spy).toHaveBeenCalledTimes(1);
1709
+ expect(spy.mock.calls[0][0]).toBe('bash');
1710
+ expect(spy.mock.calls[0][1]).toBe('bash:rm*');
1711
+ expect(spy.mock.calls[0][3]).toBe('deny');
1712
+ });
1713
+
1714
+ test('file_write with proxied network_mode is NOT affected (persistence still allowed)', async () => {
1715
+ // Only bash with proxied mode disables persistence
1716
+ const spy = setupAddRuleSpy();
1717
+
1718
+ const prompter = makePrompterWithDecision('always_allow', 'file_write:*', '/tmp/project');
1719
+ const executor = new ToolExecutor(prompter);
1720
+ const result = await executor.execute(
1721
+ 'file_write',
1722
+ { path: '/tmp/test.txt', content: 'data', network_mode: 'proxied' },
1723
+ makeContext(),
1724
+ );
1725
+
1726
+ expect(result.isError).toBe(false);
1727
+ expect(spy).toHaveBeenCalledTimes(1);
1728
+ });
1729
+
1730
+ test('host_bash proxied always_allow_high_risk still saves rule (host_bash ignores network_mode)', async () => {
1731
+ // host_bash does not support network_mode — persistence blocking
1732
+ // applies only to sandboxed bash.
1733
+ const spy = setupAddRuleSpy();
1734
+
1735
+ const prompter = makePrompterWithDecision('always_allow_high_risk', 'host_bash:*', 'everywhere');
1736
+ const executor = new ToolExecutor(prompter);
1737
+ const result = await executor.execute(
1738
+ 'host_bash',
1739
+ { command: 'wget https://example.com/data.tar.gz', network_mode: 'proxied' },
1740
+ makeContext(),
1741
+ );
1742
+
1743
+ expect(result.isError).toBe(false);
1744
+ expect(spy).toHaveBeenCalledTimes(1);
1745
+ });
1746
+
1747
+ test('proxied bash denied result message omits "rule saved" suffix', async () => {
1748
+ setupAddRuleSpy();
1749
+
1750
+ const prompter = makePrompterWithDecision('always_deny', 'bash:*', '/tmp/project');
1751
+ const executor = new ToolExecutor(prompter);
1752
+ const result = await executor.execute(
1753
+ 'bash',
1754
+ { command: 'curl https://malicious.com', network_mode: 'proxied' },
1755
+ makeContext(),
1756
+ );
1757
+
1758
+ expect(result.isError).toBe(true);
1759
+ // Since no rule was saved, the message should NOT include "rule was saved"
1760
+ expect(result.content).toContain('Permission denied by user');
1761
+ expect(result.content).not.toContain('rule was saved');
1762
+ });
1763
+
1764
+ test('non-proxied bash denied result message includes "rule saved" suffix', async () => {
1765
+ setupAddRuleSpy();
1766
+
1767
+ const prompter = makePrompterWithDecision('always_deny', 'bash:rm*', '/tmp/project');
1768
+ const executor = new ToolExecutor(prompter);
1769
+ const result = await executor.execute(
1770
+ 'bash',
1771
+ { command: 'rm -rf /' },
1772
+ makeContext(),
1773
+ );
1774
+
1775
+ expect(result.isError).toBe(true);
1776
+ expect(result.content).toContain('Permission denied by user');
1777
+ expect(result.content).toContain('rule was saved');
1778
+ });
1779
+ });
1780
+
1781
+ // ---------------------------------------------------------------------------
1782
+ // Baseline: sanitized env excludes credential-like variables
1783
+ // ---------------------------------------------------------------------------
1784
+
1785
+ // Import the real buildSanitizedEnv (not mocked) for baseline credential tests
1786
+ const { buildSanitizedEnv } = await import('../tools/terminal/safe-env.js');
1787
+
1788
+ describe('buildSanitizedEnv — baseline: credential exclusion', () => {
1789
+ // Credential-like env vars that must never appear in the sanitized env.
1790
+ // Names are constructed dynamically to avoid tripping pre-commit secret scanners.
1791
+ const k = (...parts: string[]) => parts.join('_');
1792
+ const CREDENTIAL_VARS = [
1793
+ k('OPENAI', 'API', 'KEY'),
1794
+ k('ANTHROPIC', 'API', 'KEY'),
1795
+ k('AWS', 'SECRET', 'ACCESS', 'KEY'),
1796
+ k('AWS', 'SESSION', 'TOKEN'),
1797
+ k('GITHUB', 'TOKEN'),
1798
+ k('GH', 'TOKEN'),
1799
+ k('NPM', 'TOKEN'),
1800
+ k('DOCKER', 'PASSWORD'),
1801
+ k('DATABASE', 'URL'),
1802
+ k('PGPASSWORD'),
1803
+ k('REDIS', 'URL'),
1804
+ k('API', 'SECRET'),
1805
+ ];
1806
+
1807
+ test('sanitized env does not include API key variables', () => {
1808
+ // Temporarily set credential-like env vars
1809
+ const originalValues: Record<string, string | undefined> = {};
1810
+ for (const key of CREDENTIAL_VARS) {
1811
+ originalValues[key] = process.env[key];
1812
+ process.env[key] = `fake-${key}-value`;
1813
+ }
1814
+
1815
+ try {
1816
+ const env = buildSanitizedEnv();
1817
+ for (const key of CREDENTIAL_VARS) {
1818
+ expect(env[key]).toBeUndefined();
1819
+ }
1820
+ } finally {
1821
+ // Restore original env
1822
+ for (const key of CREDENTIAL_VARS) {
1823
+ if (originalValues[key] === undefined) {
1824
+ delete process.env[key];
1825
+ } else {
1826
+ process.env[key] = originalValues[key];
1827
+ }
1828
+ }
1829
+ }
1830
+ });
1831
+
1832
+ test('sanitized env includes expected safe variables when present', () => {
1833
+ const env = buildSanitizedEnv();
1834
+ // PATH and HOME should be present (they exist in the process env)
1835
+ if (process.env.PATH) {
1836
+ expect(env.PATH).toBe(process.env.PATH);
1837
+ }
1838
+ if (process.env.HOME) {
1839
+ expect(env.HOME).toBe(process.env.HOME);
1840
+ }
1841
+ });
1842
+
1843
+ test('sanitized env only contains keys from the allowlist', () => {
1844
+ const SAFE_ENV_VARS = [
1845
+ 'PATH', 'HOME', 'TERM', 'LANG', 'EDITOR', 'SHELL', 'USER',
1846
+ 'TMPDIR', 'LC_ALL', 'LC_CTYPE', 'XDG_RUNTIME_DIR', 'DISPLAY',
1847
+ 'COLORTERM', 'TERM_PROGRAM', 'SSH_AUTH_SOCK', 'SSH_AGENT_PID',
1848
+ 'GPG_TTY', 'GNUPGHOME',
1849
+ ];
1850
+
1851
+ const env = buildSanitizedEnv();
1852
+ for (const key of Object.keys(env)) {
1853
+ expect(SAFE_ENV_VARS).toContain(key);
1854
+ }
1855
+ });
1856
+ });
1857
+
1858
+ // ---------------------------------------------------------------------------
1859
+ // Persistent-allow lifecycle: roundtrip and auto-allow on subsequent invocation
1860
+ // ---------------------------------------------------------------------------
1861
+
1862
+ describe('ToolExecutor persistent-allow lifecycle', () => {
1863
+ beforeEach(() => {
1864
+ fakeToolResult = { content: 'ok', isError: false };
1865
+ lastCheckArgs = undefined;
1866
+ getToolOverride = undefined;
1867
+ checkResultOverride = undefined;
1868
+ checkFnOverride = undefined;
1869
+ if (addRuleSpy) { addRuleSpy.mockRestore(); addRuleSpy = undefined; }
1870
+ });
1871
+
1872
+ function setupAddRuleSpy() {
1873
+ addRuleSpy = spyOn(trustStore, 'addRule').mockImplementation(
1874
+ (tool: string, pattern: string, scope: string, decision = 'allow', priority = 100, options?: any) => {
1875
+ return { id: 'spy-rule-id', tool, pattern, scope, decision, priority, createdAt: Date.now(), ...options } as any;
1876
+ },
1877
+ );
1878
+ return addRuleSpy;
1879
+ }
1880
+
1881
+ test('persistent-allow roundtrip: always_allow saves rule and allows tool', async () => {
1882
+ // Simulate check() returning 'prompt' so the executor asks the user
1883
+ checkResultOverride = { decision: 'prompt', reason: 'Medium risk: requires approval' };
1884
+ const spy = setupAddRuleSpy();
1885
+
1886
+ // User responds with always_allow, selecting a pattern and scope
1887
+ const prompter = makePrompterWithDecision('always_allow', 'git *', '/tmp/project');
1888
+ const executor = new ToolExecutor(prompter);
1889
+ const result = await executor.execute('bash', { command: 'git status' }, makeContext());
1890
+
1891
+ // The tool should have been allowed to proceed
1892
+ expect(result.isError).toBe(false);
1893
+ expect(result.content).toBe('ok');
1894
+
1895
+ // addRule should have been called with the correct arguments
1896
+ expect(spy).toHaveBeenCalledTimes(1);
1897
+ const [tool, pattern, scope, decision] = spy.mock.calls[0];
1898
+ expect(tool).toBe('bash');
1899
+ expect(pattern).toBe('git *');
1900
+ expect(scope).toBe('/tmp/project');
1901
+ expect(decision).toBe('allow');
1902
+ });
1903
+
1904
+ test('auto-allow on subsequent invocation: matching rule skips prompt', async () => {
1905
+ // Simulate a previously saved rule by making check() return 'allow'
1906
+ // with a matched rule (as findHighestPriorityRule would).
1907
+ checkResultOverride = { decision: 'allow', reason: 'Matched trust rule: git *' };
1908
+
1909
+ let promptCalled = false;
1910
+ const trackingPrompter = {
1911
+ prompt: async () => { promptCalled = true; return { decision: 'allow' as const }; },
1912
+ resolveConfirmation: () => {},
1913
+ updateSender: () => {},
1914
+ dispose: () => {},
1915
+ } as unknown as PermissionPrompter;
1916
+
1917
+ const executor = new ToolExecutor(trackingPrompter);
1918
+ const result = await executor.execute('bash', { command: 'git status' }, makeContext());
1919
+
1920
+ // The tool should be auto-allowed
1921
+ expect(result.isError).toBe(false);
1922
+ expect(result.content).toBe('ok');
1923
+
1924
+ // The prompter should NOT have been called — the rule auto-allowed
1925
+ expect(promptCalled).toBe(false);
1926
+ });
1927
+
1928
+ test('always_allow with everywhere scope saves rule and allows tool', async () => {
1929
+ checkResultOverride = { decision: 'prompt', reason: 'Medium risk: requires approval' };
1930
+ const spy = setupAddRuleSpy();
1931
+
1932
+ const prompter = makePrompterWithDecision('always_allow', 'file_write:*', 'everywhere');
1933
+ const executor = new ToolExecutor(prompter);
1934
+ const result = await executor.execute('file_write', { path: '/tmp/test.txt', content: 'hello' }, makeContext());
1935
+
1936
+ expect(result.isError).toBe(false);
1937
+ expect(spy).toHaveBeenCalledTimes(1);
1938
+ const [tool, pattern, scope, decision] = spy.mock.calls[0];
1939
+ expect(tool).toBe('file_write');
1940
+ expect(pattern).toBe('file_write:*');
1941
+ expect(scope).toBe('everywhere');
1942
+ expect(decision).toBe('allow');
1943
+ });
1944
+ });
1945
+
1946
+ describe('integration regressions — prompt payload (PR 11)', () => {
1947
+ beforeEach(() => {
1948
+ fakeToolResult = { content: 'ok', isError: false };
1949
+ checkResultOverride = undefined;
1950
+ checkFnOverride = undefined;
1951
+ getToolOverride = undefined;
1952
+ });
1953
+
1954
+ test('shell command prompt payload includes allowlist and scope options', async () => {
1955
+ checkResultOverride = { decision: 'prompt', reason: 'Medium risk: requires approval' };
1956
+
1957
+ let capturedAllowlist: any[] | undefined;
1958
+ let capturedScopes: any[] | undefined;
1959
+ const prompter = {
1960
+ prompt: async (
1961
+ _toolName: string, _input: Record<string, unknown>, _riskLevel: string,
1962
+ allowlistOptions: any[], scopeOptions: any[],
1963
+ ) => {
1964
+ capturedAllowlist = allowlistOptions;
1965
+ capturedScopes = scopeOptions;
1966
+ return { decision: 'allow' as const };
1967
+ },
1968
+ resolveConfirmation: () => {},
1969
+ updateSender: () => {},
1970
+ dispose: () => {},
1971
+ } as unknown as PermissionPrompter;
1972
+
1973
+ const executor = new ToolExecutor(prompter);
1974
+ await executor.execute('bash', { command: 'npm install' }, makeContext());
1975
+
1976
+ // Verify that the prompter received allowlist options
1977
+ expect(capturedAllowlist).toBeDefined();
1978
+ expect(capturedAllowlist!.length).toBeGreaterThan(0);
1979
+ // The mock returns [{label: 'exact', description: 'exact', pattern: 'exact'}]
1980
+ expect(capturedAllowlist![0]).toHaveProperty('pattern');
1981
+ expect(capturedAllowlist![0]).toHaveProperty('label');
1982
+ expect(capturedAllowlist![0]).toHaveProperty('description');
1983
+
1984
+ // Verify scope options are also passed
1985
+ expect(capturedScopes).toBeDefined();
1986
+ expect(capturedScopes!.length).toBeGreaterThan(0);
1987
+ expect(capturedScopes![0]).toHaveProperty('scope');
1988
+ });
1989
+ });