@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,1605 @@
1
+ import { describe, test, expect, beforeEach, mock, spyOn } from 'bun:test';
2
+ import * as fs from 'node:fs';
3
+ import { mkdtempSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join, dirname } from 'node:path';
6
+
7
+ // Create a temp directory for the trust file
8
+ const testDir = mkdtempSync(join(tmpdir(), 'trust-store-test-'));
9
+
10
+ // Mock platform module so trust-store writes to temp dir instead of ~/.vellum
11
+ mock.module('../util/platform.js', () => ({
12
+ getRootDir: () => testDir,
13
+ getDataDir: () => testDir,
14
+ isMacOS: () => process.platform === 'darwin',
15
+ isLinux: () => process.platform === 'linux',
16
+ isWindows: () => process.platform === 'win32',
17
+ getSocketPath: () => join(testDir, 'test.sock'),
18
+ getPidPath: () => join(testDir, 'test.pid'),
19
+ getDbPath: () => join(testDir, 'test.db'),
20
+ getLogPath: () => join(testDir, 'test.log'),
21
+ ensureDataDir: () => {},
22
+ }));
23
+
24
+ // Mock logger to suppress output during tests
25
+ mock.module('../util/logger.js', () => ({
26
+ getLogger: () => ({
27
+ info: () => {},
28
+ warn: () => {},
29
+ error: () => {},
30
+ debug: () => {},
31
+ trace: () => {},
32
+ fatal: () => {},
33
+ child: () => ({
34
+ info: () => {},
35
+ warn: () => {},
36
+ error: () => {},
37
+ debug: () => {},
38
+ }),
39
+ }),
40
+ }));
41
+
42
+ import { addRule, removeRule, updateRule, findMatchingRule, findDenyRule, findHighestPriorityRule, getAllRules, clearAllRules, clearCache } from '../permissions/trust-store.js';
43
+ import { getDefaultRuleTemplates } from '../permissions/defaults.js';
44
+
45
+ const trustPath = join(testDir, 'protected', 'trust.json');
46
+ const DEFAULT_TEMPLATES = getDefaultRuleTemplates();
47
+ const NUM_DEFAULTS = DEFAULT_TEMPLATES.length;
48
+ const DEFAULT_PRIORITY_BY_ID = new Map(DEFAULT_TEMPLATES.map((t) => [t.id, t.priority]));
49
+
50
+ describe('Trust Store', () => {
51
+ beforeEach(() => {
52
+ // Clear cached rules and remove the trust file between tests
53
+ clearCache();
54
+ try { rmSync(trustPath); } catch { /* may not exist */ }
55
+ });
56
+
57
+ // Intentionally do not remove `testDir` in afterAll.
58
+ // A late async log flush can still attempt to open `test.log` under this dir,
59
+ // which intermittently causes an unhandled ENOENT in CI if the dir is removed.
60
+ // ── addRule ─────────────────────────────────────────────────────
61
+
62
+ describe('addRule', () => {
63
+ test('adds a rule and returns it', () => {
64
+ const rule = addRule('bash', 'git *', '/home/user/project');
65
+ expect(rule.id).toBeDefined();
66
+ expect(rule.tool).toBe('bash');
67
+ expect(rule.pattern).toBe('git *');
68
+ expect(rule.scope).toBe('/home/user/project');
69
+ expect(rule.decision).toBe('allow');
70
+ expect(rule.priority).toBe(100);
71
+ expect(rule.createdAt).toBeGreaterThan(0);
72
+ });
73
+
74
+ test('assigns unique IDs to each rule', () => {
75
+ const rule1 = addRule('bash', 'npm *', '/tmp');
76
+ const rule2 = addRule('bash', 'bun *', '/tmp');
77
+ expect(rule1.id).not.toBe(rule2.id);
78
+ });
79
+
80
+ test('persists rule to disk', () => {
81
+ addRule('bash', 'git push', '/home/user');
82
+ const raw = readFileSync(trustPath, 'utf-8');
83
+ const data = JSON.parse(raw);
84
+ expect(data.version).toBe(3);
85
+ expect(data.rules).toHaveLength(1 + NUM_DEFAULTS);
86
+ const userRule = data.rules.find((r: { pattern: string }) => r.pattern === 'git push');
87
+ expect(userRule).toBeDefined();
88
+ expect(userRule.priority).toBe(100);
89
+ });
90
+
91
+ test('multiple rules accumulate', () => {
92
+ addRule('bash', 'git *', '/tmp');
93
+ addRule('file_write', '/tmp/*', '/tmp');
94
+ addRule('bash', 'npm *', '/tmp');
95
+ expect(getAllRules()).toHaveLength(3 + NUM_DEFAULTS);
96
+ });
97
+
98
+ test('default priority is 100', () => {
99
+ const rule = addRule('bash', 'git *', '/tmp');
100
+ expect(rule.priority).toBe(100);
101
+ });
102
+
103
+ test('custom priority is respected', () => {
104
+ const rule = addRule('bash', 'git *', '/tmp', 'allow', 5);
105
+ expect(rule.priority).toBe(5);
106
+ });
107
+
108
+ test('rules are sorted by priority descending in getAllRules', () => {
109
+ addRule('bash', 'low *', '/tmp', 'allow', 0);
110
+ addRule('bash', 'high *', '/tmp', 'allow', 2);
111
+ addRule('bash', 'med *', '/tmp', 'allow', 1);
112
+ const rules = getAllRules();
113
+ // Default ask rules have higher priority than user rules
114
+ const maxDefaultPriority = Math.max(...DEFAULT_TEMPLATES.map((t) => t.priority));
115
+ expect(rules[0].priority).toBe(maxDefaultPriority);
116
+ const userRules = rules.filter((r) => !r.id.startsWith('default:'));
117
+ expect(userRules[0].priority).toBe(2);
118
+ expect(userRules[1].priority).toBe(1);
119
+ expect(userRules[2].priority).toBe(0);
120
+ });
121
+
122
+ test('accepts allowHighRisk option and persists it', () => {
123
+ const rule = addRule('bash', 'sudo *', 'everywhere', 'allow', 100, { allowHighRisk: true });
124
+ expect(rule.allowHighRisk).toBe(true);
125
+ // Verify it persists to disk
126
+ clearCache();
127
+ const rules = getAllRules();
128
+ const found = rules.find((r) => r.id === rule.id);
129
+ expect(found).toBeDefined();
130
+ expect(found!.allowHighRisk).toBe(true);
131
+ });
132
+
133
+ test('addRule without allowHighRisk option does not set the field', () => {
134
+ const rule = addRule('bash', 'git *', '/tmp');
135
+ expect(rule.allowHighRisk).toBeUndefined();
136
+ // Verify on disk
137
+ const raw = JSON.parse(readFileSync(trustPath, 'utf-8'));
138
+ const diskRule = raw.rules.find((r: { id: string }) => r.id === rule.id);
139
+ expect(diskRule).toBeDefined();
140
+ expect(diskRule).not.toHaveProperty('allowHighRisk');
141
+ });
142
+
143
+ test('at same priority deny rules sort before allow rules', () => {
144
+ addRule('bash', 'allow *', '/tmp', 'allow', 100);
145
+ addRule('bash', 'deny *', '/tmp', 'deny', 100);
146
+ const userRules = getAllRules().filter((r) => !r.id.startsWith('default:'));
147
+ expect(userRules[0].decision).toBe('deny');
148
+ expect(userRules[1].decision).toBe('allow');
149
+ });
150
+
151
+ test('accepts executionTarget option and persists it', () => {
152
+ const rule = addRule('skill_tool', 'skill_tool:*', '/tmp', 'allow', 100, {
153
+ executionTarget: 'sandbox',
154
+ });
155
+ expect(rule.executionTarget).toBe('sandbox');
156
+
157
+ // Verify persistence to disk
158
+ clearCache();
159
+ const rules = getAllRules();
160
+ const found = rules.find((r) => r.id === rule.id);
161
+ expect(found).toBeDefined();
162
+ expect(found!.executionTarget).toBe('sandbox');
163
+ });
164
+
165
+ test('accepts all contextual options together (target, allowHighRisk)', () => {
166
+ const rule = addRule('risky_tool', 'risky_tool:*', 'everywhere', 'allow', 100, {
167
+ allowHighRisk: true,
168
+ executionTarget: 'host',
169
+ });
170
+ expect(rule.allowHighRisk).toBe(true);
171
+ expect(rule.executionTarget).toBe('host');
172
+
173
+ // Verify on disk
174
+ const raw = JSON.parse(readFileSync(trustPath, 'utf-8'));
175
+ const diskRule = raw.rules.find((r: { id: string }) => r.id === rule.id);
176
+ expect(diskRule).toBeDefined();
177
+ expect(diskRule.allowHighRisk).toBe(true);
178
+ expect(diskRule.executionTarget).toBe('host');
179
+ });
180
+
181
+ test('addRule without options does not set optional fields', () => {
182
+ const rule = addRule('bash', 'echo *', '/tmp');
183
+ expect(rule.executionTarget).toBeUndefined();
184
+
185
+ // Verify on disk
186
+ const raw = JSON.parse(readFileSync(trustPath, 'utf-8'));
187
+ const diskRule = raw.rules.find((r: { id: string }) => r.id === rule.id);
188
+ expect(diskRule).toBeDefined();
189
+ expect(diskRule).not.toHaveProperty('executionTarget');
190
+ });
191
+ });
192
+
193
+ // ── removeRule ──────────────────────────────────────────────────
194
+
195
+ describe('removeRule', () => {
196
+ test('removes an existing rule', () => {
197
+ const rule = addRule('bash', 'git *', '/tmp');
198
+ expect(removeRule(rule.id)).toBe(true);
199
+ expect(getAllRules()).toHaveLength(NUM_DEFAULTS);
200
+ });
201
+
202
+ test('returns false for non-existent ID', () => {
203
+ expect(removeRule('non-existent-id')).toBe(false);
204
+ });
205
+
206
+ test('persists removal to disk', () => {
207
+ const rule = addRule('bash', 'npm *', '/tmp');
208
+ removeRule(rule.id);
209
+ // Reload from disk to verify
210
+ clearCache();
211
+ expect(getAllRules()).toHaveLength(NUM_DEFAULTS);
212
+ });
213
+
214
+ test('only removes the targeted rule', () => {
215
+ const rule1 = addRule('bash', 'git *', '/tmp');
216
+ const rule2 = addRule('bash', 'npm *', '/tmp');
217
+ removeRule(rule1.id);
218
+ const remaining = getAllRules();
219
+ expect(remaining).toHaveLength(1 + NUM_DEFAULTS);
220
+ expect(remaining.find((r) => r.id === rule2.id)).toBeDefined();
221
+ });
222
+ });
223
+
224
+ // ── updateRule ─────────────────────────────────────────────────
225
+
226
+ describe('updateRule', () => {
227
+ test('updates pattern on an existing rule', () => {
228
+ const rule = addRule('bash', 'git *', '/tmp');
229
+ const updated = updateRule(rule.id, { pattern: 'git push *' });
230
+ expect(updated.pattern).toBe('git push *');
231
+ expect(updated.id).toBe(rule.id);
232
+ expect(updated.tool).toBe('bash');
233
+ });
234
+
235
+ test('updates multiple fields at once', () => {
236
+ const rule = addRule('bash', 'npm *', '/tmp');
237
+ const updated = updateRule(rule.id, { tool: 'file_write', scope: '/home', decision: 'deny', priority: 50 });
238
+ expect(updated.tool).toBe('file_write');
239
+ expect(updated.scope).toBe('/home');
240
+ expect(updated.decision).toBe('deny');
241
+ expect(updated.priority).toBe(50);
242
+ });
243
+
244
+ test('throws for non-existent rule ID', () => {
245
+ expect(() => updateRule('non-existent-id', { pattern: 'test' })).toThrow('Trust rule not found: non-existent-id');
246
+ });
247
+
248
+ test('persists update to disk', () => {
249
+ const rule = addRule('bash', 'git *', '/tmp');
250
+ updateRule(rule.id, { pattern: 'git status' });
251
+ clearCache();
252
+ const rules = getAllRules();
253
+ const found = rules.find((r) => r.id === rule.id);
254
+ expect(found).toBeDefined();
255
+ expect(found!.pattern).toBe('git status');
256
+ });
257
+
258
+ test('re-sorts rules after priority change', () => {
259
+ const rule1 = addRule('bash', 'low *', '/tmp', 'allow', 10);
260
+ const rule2 = addRule('bash', 'high *', '/tmp', 'allow', 200);
261
+ // rule2 should be first (higher priority)
262
+ let userRules = getAllRules().filter((r) => !r.id.startsWith('default:'));
263
+ expect(userRules[0].id).toBe(rule2.id);
264
+ // Update rule1 to have higher priority
265
+ updateRule(rule1.id, { priority: 300 });
266
+ userRules = getAllRules().filter((r) => !r.id.startsWith('default:'));
267
+ expect(userRules[0].id).toBe(rule1.id);
268
+ });
269
+
270
+ test('leaves unchanged fields intact', () => {
271
+ const rule = addRule('bash', 'git *', '/home/user', 'allow', 100);
272
+ updateRule(rule.id, { pattern: 'git push *' });
273
+ const updated = getAllRules().find((r) => r.id === rule.id)!;
274
+ expect(updated.tool).toBe('bash');
275
+ expect(updated.scope).toBe('/home/user');
276
+ expect(updated.decision).toBe('allow');
277
+ expect(updated.priority).toBe(100);
278
+ expect(updated.createdAt).toBe(rule.createdAt);
279
+ });
280
+ });
281
+
282
+ // ── findMatchingRule ────────────────────────────────────────────
283
+
284
+ describe('findMatchingRule', () => {
285
+ test('finds exact match', () => {
286
+ addRule('bash', 'git push', '/tmp');
287
+ const match = findMatchingRule('bash', 'git push', '/tmp');
288
+ expect(match).not.toBeNull();
289
+ expect(match!.pattern).toBe('git push');
290
+ });
291
+
292
+ test('finds glob wildcard match', () => {
293
+ addRule('bash', 'git *', '/tmp');
294
+ const match = findMatchingRule('bash', 'git push origin main', '/tmp');
295
+ expect(match).not.toBeNull();
296
+ });
297
+
298
+ test('returns null when tool does not match', () => {
299
+ addRule('file_write', 'git *', '/tmp');
300
+ // host_bash default is 'ask' so findMatchingRule (allow-only) won't find it
301
+ const match = findMatchingRule('host_bash', 'git push', '/tmp');
302
+ expect(match).toBeNull();
303
+ });
304
+
305
+ test('returns null when pattern does not match', () => {
306
+ addRule('host_bash', 'git *', '/tmp');
307
+ const match = findMatchingRule('host_bash', 'npm install', '/tmp');
308
+ expect(match).toBeNull();
309
+ });
310
+
311
+ // Scope matching
312
+ describe('scope matching', () => {
313
+ test('matches when scope equals rule scope', () => {
314
+ addRule('bash', 'npm *', '/home/user/project');
315
+ const match = findMatchingRule('bash', 'npm install', '/home/user/project');
316
+ expect(match).not.toBeNull();
317
+ });
318
+
319
+ test('matches when scope is under rule scope (prefix)', () => {
320
+ addRule('bash', 'npm *', '/home/user');
321
+ const match = findMatchingRule('bash', 'npm install', '/home/user/project/sub');
322
+ expect(match).not.toBeNull();
323
+ });
324
+
325
+ test('does not match when scope is outside rule scope', () => {
326
+ addRule('host_bash', 'npm *', '/home/user/project');
327
+ const match = findMatchingRule('host_bash', 'npm install', '/home/other');
328
+ expect(match).toBeNull();
329
+ });
330
+
331
+ test('everywhere scope matches any directory', () => {
332
+ addRule('bash', 'git *', 'everywhere');
333
+ const match = findMatchingRule('bash', 'git status', '/any/random/path');
334
+ expect(match).not.toBeNull();
335
+ });
336
+
337
+ test('everywhere scope matches root', () => {
338
+ addRule('bash', 'ls', 'everywhere');
339
+ const match = findMatchingRule('bash', 'ls', '/');
340
+ expect(match).not.toBeNull();
341
+ });
342
+
343
+ test('does not match sibling path with shared prefix', () => {
344
+ addRule('host_bash', 'npm *', '/home/user/project');
345
+ const match = findMatchingRule('host_bash', 'npm install', '/home/user/project-evil');
346
+ expect(match).toBeNull();
347
+ });
348
+
349
+ test('matches exact scope with trailing slash on working dir', () => {
350
+ addRule('bash', 'npm *', '/home/user/project');
351
+ const match = findMatchingRule('bash', 'npm install', '/home/user/project/');
352
+ expect(match).not.toBeNull();
353
+ });
354
+
355
+ test('matches when rule scope has trailing slash', () => {
356
+ addRule('bash', 'npm *', '/home/user/project/');
357
+ const match = findMatchingRule('bash', 'npm install', '/home/user/project');
358
+ expect(match).not.toBeNull();
359
+ });
360
+
361
+ test('does not match sibling with glob-suffixed scope', () => {
362
+ addRule('host_bash', 'npm *', '/home/user/project*');
363
+ const match = findMatchingRule('host_bash', 'npm install', '/home/user/project-evil');
364
+ expect(match).toBeNull();
365
+ });
366
+ });
367
+
368
+ // Pattern matching with minimatch
369
+ describe('pattern matching', () => {
370
+ test('matches * wildcard', () => {
371
+ addRule('bash', 'npm *', '/tmp');
372
+ expect(findMatchingRule('bash', 'npm install', '/tmp')).not.toBeNull();
373
+ expect(findMatchingRule('bash', 'npm test', '/tmp')).not.toBeNull();
374
+ });
375
+
376
+ test('matches exact string', () => {
377
+ addRule('host_bash', 'git status', '/tmp');
378
+ expect(findMatchingRule('host_bash', 'git status', '/tmp')).not.toBeNull();
379
+ expect(findMatchingRule('host_bash', 'git push', '/tmp')).toBeNull();
380
+ });
381
+
382
+ test('matches file path pattern', () => {
383
+ addRule('file_write', '/tmp/*', '/tmp');
384
+ expect(findMatchingRule('file_write', '/tmp/file.txt', '/tmp')).not.toBeNull();
385
+ });
386
+
387
+ test('star pattern matches single-segment strings', () => {
388
+ addRule('file_write', '*', '/tmp');
389
+ // minimatch '*' matches strings without path separators
390
+ expect(findMatchingRule('file_write', 'file.txt', '/tmp')).not.toBeNull();
391
+ });
392
+
393
+ test('star pattern does not match paths with slashes', () => {
394
+ addRule('file_write', '*', '/tmp');
395
+ // minimatch '*' does not cross '/' boundaries
396
+ expect(findMatchingRule('file_write', '/any/path/file.txt', '/tmp')).toBeNull();
397
+ });
398
+ });
399
+ });
400
+
401
+ // ── findHighestPriorityRule ──────────────────────────────────────
402
+
403
+ describe('findHighestPriorityRule', () => {
404
+ test('returns highest priority matching rule', () => {
405
+ addRule('bash', 'rm *', '/tmp', 'allow', 0);
406
+ addRule('bash', 'rm *', '/tmp', 'deny', 100);
407
+ const match = findHighestPriorityRule('bash', ['rm file.txt'], '/tmp');
408
+ expect(match).not.toBeNull();
409
+ expect(match!.decision).toBe('deny');
410
+ expect(match!.priority).toBe(100);
411
+ });
412
+
413
+ test('higher priority allow beats lower priority deny', () => {
414
+ addRule('bash', 'rm *', '/tmp', 'deny', 0);
415
+ addRule('bash', 'rm *', '/tmp', 'allow', 100);
416
+ const match = findHighestPriorityRule('bash', ['rm file.txt'], '/tmp');
417
+ expect(match).not.toBeNull();
418
+ expect(match!.decision).toBe('allow');
419
+ });
420
+
421
+ test('same priority: deny beats allow', () => {
422
+ addRule('bash', 'rm *', '/tmp', 'allow', 100);
423
+ addRule('bash', 'rm *', '/tmp', 'deny', 100);
424
+ const match = findHighestPriorityRule('bash', ['rm file.txt'], '/tmp');
425
+ expect(match).not.toBeNull();
426
+ expect(match!.decision).toBe('deny');
427
+ });
428
+
429
+ test('checks multiple command candidates', () => {
430
+ addRule('web_fetch', 'web_fetch:https://example.com/*', '/tmp', 'allow');
431
+ const match = findHighestPriorityRule(
432
+ 'web_fetch',
433
+ ['web_fetch:https://example.com/page', 'web_fetch:https://example.com/*'],
434
+ '/tmp',
435
+ );
436
+ expect(match).not.toBeNull();
437
+ });
438
+
439
+ test('returns null when no rule matches', () => {
440
+ // Use file_read with a non-workspace path — file_read defaults only
441
+ // cover specific workspace files, so /tmp paths won't match any default.
442
+ addRule('file_read', 'file_read:/specific/*', '/tmp', 'allow');
443
+ const match = findHighestPriorityRule('file_read', ['file_read:/other/path'], '/tmp');
444
+ expect(match).toBeNull();
445
+ });
446
+
447
+ test('respects scope matching', () => {
448
+ // Use file_read — bash has a global default allow rule that matches everywhere.
449
+ addRule('file_read', 'file_read:/home/user/project/*', '/home/user/project', 'deny');
450
+ expect(findHighestPriorityRule('file_read', ['file_read:/home/user/project/file.txt'], '/home/user/project/sub')).not.toBeNull();
451
+ expect(findHighestPriorityRule('file_read', ['file_read:/home/user/project/file.txt'], '/home/other')).toBeNull();
452
+ });
453
+
454
+ test('everywhere scope matches any directory', () => {
455
+ addRule('bash', 'git *', 'everywhere', 'allow');
456
+ const match = findHighestPriorityRule('bash', ['git status'], '/any/random/path');
457
+ expect(match).not.toBeNull();
458
+ });
459
+ });
460
+
461
+ // ── getAllRules ─────────────────────────────────────────────────
462
+
463
+ describe('getAllRules', () => {
464
+ test('returns default rules when no user rules exist', () => {
465
+ const rules = getAllRules();
466
+ expect(rules).toHaveLength(NUM_DEFAULTS);
467
+ expect(rules.every((r) => r.id.startsWith('default:'))).toBe(true);
468
+ });
469
+
470
+ test('returns a copy (not the internal array)', () => {
471
+ addRule('bash', 'git *', '/tmp');
472
+ const rules1 = getAllRules();
473
+ const rules2 = getAllRules();
474
+ expect(rules1).toEqual(rules2);
475
+ expect(rules1).not.toBe(rules2); // different references
476
+ });
477
+ });
478
+
479
+ // ── clearCache ─────────────────────────────────────────────────
480
+
481
+ describe('clearCache', () => {
482
+ test('forces reload from disk on next access', () => {
483
+ addRule('bash', 'git *', '/tmp');
484
+ expect(getAllRules()).toHaveLength(1 + NUM_DEFAULTS);
485
+ clearCache();
486
+ // After clearing cache, rules are reloaded from disk
487
+ expect(getAllRules()).toHaveLength(1 + NUM_DEFAULTS);
488
+ });
489
+ });
490
+
491
+ // ── persistence ─────────────────────────────────────────────────
492
+
493
+ describe('persistence', () => {
494
+ test('rules survive cache clear (loaded from disk)', () => {
495
+ const rule = addRule('bash', 'npm *', '/tmp');
496
+ clearCache();
497
+ const rules = getAllRules();
498
+ expect(rules).toHaveLength(1 + NUM_DEFAULTS);
499
+ expect(rules.find((r) => r.id === rule.id)).toBeDefined();
500
+ });
501
+
502
+ test('trust file has correct structure', () => {
503
+ addRule('bash', 'git *', '/tmp');
504
+ const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
505
+ expect(data).toHaveProperty('version', 3);
506
+ expect(data).toHaveProperty('rules');
507
+ expect(Array.isArray(data.rules)).toBe(true);
508
+ const userRule = data.rules.find((r: { pattern: string }) => r.pattern === 'git *');
509
+ expect(userRule).toHaveProperty('priority', 100);
510
+ });
511
+ });
512
+
513
+ // ── deny rules ─────────────────────────────────────────────────
514
+
515
+ describe('deny rules', () => {
516
+ test('addRule with deny decision creates a deny rule', () => {
517
+ const rule = addRule('bash', 'rm -rf *', '/tmp', 'deny');
518
+ expect(rule.decision).toBe('deny');
519
+ expect(rule.tool).toBe('bash');
520
+ expect(rule.pattern).toBe('rm -rf *');
521
+ });
522
+
523
+ test('deny rule persists to disk', () => {
524
+ addRule('bash', 'rm *', '/tmp', 'deny');
525
+ clearCache();
526
+ const rules = getAllRules();
527
+ expect(rules).toHaveLength(1 + NUM_DEFAULTS);
528
+ const userRule = rules.find((r) => r.pattern === 'rm *');
529
+ expect(userRule).toBeDefined();
530
+ expect(userRule!.decision).toBe('deny');
531
+ });
532
+
533
+ test('findDenyRule finds deny rules', () => {
534
+ addRule('bash', 'rm *', '/tmp', 'deny');
535
+ const match = findDenyRule('bash', 'rm file.txt', '/tmp');
536
+ expect(match).not.toBeNull();
537
+ expect(match!.decision).toBe('deny');
538
+ });
539
+
540
+ test('findDenyRule ignores allow rules', () => {
541
+ addRule('bash', 'rm *', '/tmp', 'allow');
542
+ const match = findDenyRule('bash', 'rm file.txt', '/tmp');
543
+ expect(match).toBeNull();
544
+ });
545
+
546
+ test('findMatchingRule ignores deny rules', () => {
547
+ // Use host_bash — bash has a default allow rule that would match.
548
+ addRule('host_bash', 'rm *', '/tmp', 'deny');
549
+ const match = findMatchingRule('host_bash', 'rm file.txt', '/tmp');
550
+ expect(match).toBeNull();
551
+ });
552
+
553
+ test('deny and allow rules coexist', () => {
554
+ addRule('bash', 'git *', '/tmp', 'allow');
555
+ addRule('bash', 'git push --force *', '/tmp', 'deny');
556
+ expect(findMatchingRule('bash', 'git status', '/tmp')).not.toBeNull();
557
+ expect(findDenyRule('bash', 'git push --force origin', '/tmp')).not.toBeNull();
558
+ });
559
+
560
+ test('deny rule with scope matching', () => {
561
+ addRule('bash', 'rm *', '/home/user/project', 'deny');
562
+ expect(findDenyRule('bash', 'rm file.txt', '/home/user/project/sub')).not.toBeNull();
563
+ expect(findDenyRule('bash', 'rm file.txt', '/home/other')).toBeNull();
564
+ });
565
+
566
+ test('deny rule with everywhere scope', () => {
567
+ addRule('bash', 'rm -rf *', 'everywhere', 'deny');
568
+ expect(findDenyRule('bash', 'rm -rf /', '/any/path')).not.toBeNull();
569
+ });
570
+
571
+ test('removeRule works for deny rules', () => {
572
+ const rule = addRule('bash', 'rm *', '/tmp', 'deny');
573
+ expect(removeRule(rule.id)).toBe(true);
574
+ expect(findDenyRule('bash', 'rm file.txt', '/tmp')).toBeNull();
575
+ });
576
+ });
577
+
578
+ // ── v1 migration ───────────────────────────────────────────────
579
+
580
+ describe('v1 migration', () => {
581
+ test('v1 rules get priority 100 on load', () => {
582
+ mkdirSync(dirname(trustPath), { recursive: true });
583
+ writeFileSync(trustPath, JSON.stringify({
584
+ version: 1,
585
+ rules: [{
586
+ id: 'test-v1-id',
587
+ tool: 'bash',
588
+ pattern: 'git *',
589
+ scope: '/tmp',
590
+ decision: 'allow',
591
+ createdAt: 1000,
592
+ }],
593
+ }));
594
+ clearCache();
595
+ const rules = getAllRules();
596
+ expect(rules).toHaveLength(1 + NUM_DEFAULTS);
597
+ const migratedRule = rules.find((r) => r.id === 'test-v1-id');
598
+ expect(migratedRule).toBeDefined();
599
+ expect(migratedRule!.priority).toBe(100);
600
+ });
601
+
602
+ test('v1 file is upgraded to v3 on disk', () => {
603
+ mkdirSync(dirname(trustPath), { recursive: true });
604
+ writeFileSync(trustPath, JSON.stringify({
605
+ version: 1,
606
+ rules: [{
607
+ id: 'migrate-me',
608
+ tool: 'bash',
609
+ pattern: 'npm *',
610
+ scope: 'everywhere',
611
+ decision: 'allow',
612
+ createdAt: 2000,
613
+ }],
614
+ }));
615
+ clearCache();
616
+ getAllRules(); // triggers load + migration
617
+ const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
618
+ expect(data.version).toBe(3);
619
+ const migratedRule = data.rules.find((r: { id: string }) => r.id === 'migrate-me');
620
+ expect(migratedRule.priority).toBe(100);
621
+ });
622
+ });
623
+
624
+ // ── loadFromDisk resilience ─────────────────────────────────────
625
+
626
+ describe('loadFromDisk resilience', () => {
627
+ test('returns in-memory rules when saveToDisk fails during migration', () => {
628
+ // Write a v1 trust file that triggers needsSave on load
629
+ mkdirSync(dirname(trustPath), { recursive: true });
630
+ writeFileSync(trustPath, JSON.stringify({
631
+ version: 1,
632
+ rules: [{
633
+ id: 'v1-readonly',
634
+ tool: 'bash',
635
+ pattern: 'git *',
636
+ scope: '/tmp',
637
+ decision: 'allow' as const,
638
+ createdAt: 1000,
639
+ }],
640
+ }));
641
+
642
+ // Spy on writeFileSync to throw when saveToDisk is called during migration.
643
+ // This is deterministic regardless of user privileges (unlike chmod 0o555).
644
+ const spy = spyOn(fs, 'writeFileSync').mockImplementation(() => {
645
+ throw new Error('Simulated write failure');
646
+ });
647
+
648
+ try {
649
+ clearCache();
650
+ const rules = getAllRules();
651
+ // Should still return the migrated rules + defaults in-memory
652
+ expect(rules).toHaveLength(1 + NUM_DEFAULTS);
653
+ const migratedRule = rules.find((r) => r.id === 'v1-readonly');
654
+ expect(migratedRule).toBeDefined();
655
+ expect(migratedRule!.priority).toBe(100);
656
+ // Verify that saveToDisk was attempted (writeFileSync was called)
657
+ expect(spy).toHaveBeenCalled();
658
+ } finally {
659
+ spy.mockRestore();
660
+ }
661
+ });
662
+ });
663
+
664
+ // ── default rules ─────────────────────────────────────────────
665
+
666
+ describe('default rules', () => {
667
+ test('backfills default rules on first load', () => {
668
+ const rules = getAllRules();
669
+ const defaults = rules.filter((r) => r.id.startsWith('default:'));
670
+ expect(defaults).toHaveLength(NUM_DEFAULTS);
671
+ for (const rule of defaults) {
672
+ expect(rule.priority).toBe(DEFAULT_PRIORITY_BY_ID.get(rule.id)!);
673
+ if (rule.id === 'default:allow-bash-rm-bootstrap') {
674
+ expect(rule.scope).toBe(join(testDir, 'workspace'));
675
+ } else {
676
+ expect(rule.scope).toBe('everywhere');
677
+ }
678
+ }
679
+
680
+ });
681
+
682
+ test('default rules cover file, host file, host shell, and workspace prompt tools', () => {
683
+ const rules = getAllRules();
684
+ const defaultTools = [...new Set(
685
+ rules
686
+ .filter((r) => r.id.startsWith('default:'))
687
+ .map((r) => r.tool),
688
+ )].sort();
689
+ expect(defaultTools).toEqual([
690
+ 'bash',
691
+ 'browser_click',
692
+ 'browser_close',
693
+ 'browser_extract',
694
+ 'browser_fill_credential',
695
+ 'browser_navigate',
696
+ 'browser_press_key',
697
+ 'browser_screenshot',
698
+ 'browser_snapshot',
699
+ 'browser_type',
700
+ 'browser_wait_for',
701
+ 'computer_use_click',
702
+ 'computer_use_double_click',
703
+ 'computer_use_drag',
704
+ 'computer_use_key',
705
+ 'computer_use_open_app',
706
+ 'computer_use_request_control',
707
+ 'computer_use_right_click',
708
+ 'computer_use_run_applescript',
709
+ 'computer_use_scroll',
710
+ 'computer_use_type_text',
711
+ 'computer_use_wait',
712
+ 'delete_managed_skill',
713
+ 'file_edit',
714
+ 'file_read',
715
+ 'file_write',
716
+ 'host_bash',
717
+ 'host_file_edit',
718
+ 'host_file_read',
719
+ 'host_file_write',
720
+ 'memory_search',
721
+ 'scaffold_managed_skill',
722
+ 'skill_load',
723
+ 'ui_dismiss',
724
+ 'ui_update',
725
+ 'view_image',
726
+ ]);
727
+ });
728
+
729
+ test('default rules are not duplicated on reload', () => {
730
+ getAllRules(); // first load
731
+ clearCache();
732
+ const rules = getAllRules(); // second load
733
+ const defaults = rules.filter((r) => r.id.startsWith('default:'));
734
+ expect(defaults).toHaveLength(NUM_DEFAULTS);
735
+ });
736
+
737
+ test('default rules persist to disk', () => {
738
+ getAllRules(); // triggers backfill + save
739
+ const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
740
+ const defaults = data.rules.filter((r: { id: string }) => r.id.startsWith('default:'));
741
+ expect(defaults).toHaveLength(NUM_DEFAULTS);
742
+ });
743
+
744
+ test('default rules are backfilled alongside v1 migration', () => {
745
+ mkdirSync(dirname(trustPath), { recursive: true });
746
+ writeFileSync(trustPath, JSON.stringify({
747
+ version: 1,
748
+ rules: [{
749
+ id: 'v1-user-rule',
750
+ tool: 'bash',
751
+ pattern: 'git *',
752
+ scope: '/tmp',
753
+ decision: 'allow',
754
+ createdAt: 1000,
755
+ }],
756
+ }));
757
+ clearCache();
758
+ const rules = getAllRules();
759
+ expect(rules).toHaveLength(1 + NUM_DEFAULTS);
760
+ expect(rules.find((r) => r.id === 'v1-user-rule')!.priority).toBe(100);
761
+ const defaults = rules.filter((r) => r.id.startsWith('default:'));
762
+ expect(defaults).toHaveLength(NUM_DEFAULTS);
763
+ expect(defaults.every((r) => r.priority === DEFAULT_PRIORITY_BY_ID.get(r.id))).toBe(true);
764
+ });
765
+
766
+ test('removed default rule is re-backfilled on next load', () => {
767
+ // First load backfills defaults
768
+ getAllRules();
769
+ // Remove one default rule by editing trust.json directly on disk
770
+ // (removeRule() throws for default rules, so we simulate external editing)
771
+ const raw = JSON.parse(readFileSync(trustPath, 'utf-8'));
772
+ raw.rules = raw.rules.filter((r: { id: string }) => r.id !== 'default:ask-host_file_read-global');
773
+ writeFileSync(trustPath, JSON.stringify(raw, null, 2));
774
+ // After reload, the rule is re-backfilled (defaults are always present)
775
+ clearCache();
776
+ const rules = getAllRules();
777
+ expect(rules.find((r) => r.id === 'default:ask-host_file_read-global')).toBeDefined();
778
+ });
779
+
780
+ test('findHighestPriorityRule matches default ask for host_file_read', () => {
781
+ const match = findHighestPriorityRule('host_file_read', ['host_file_read:/etc/hosts'], '/tmp');
782
+ expect(match).not.toBeNull();
783
+ expect(match!.id).toBe('default:ask-host_file_read-global');
784
+ expect(match!.decision).toBe('ask');
785
+ expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-host_file_read-global')!);
786
+ });
787
+
788
+ test('findHighestPriorityRule matches default ask for host_file_write', () => {
789
+ const match = findHighestPriorityRule('host_file_write', ['host_file_write:/etc/hosts'], '/tmp');
790
+ expect(match).not.toBeNull();
791
+ expect(match!.id).toBe('default:ask-host_file_write-global');
792
+ expect(match!.decision).toBe('ask');
793
+ expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-host_file_write-global')!);
794
+ });
795
+
796
+ test('findHighestPriorityRule matches default ask for host_file_edit', () => {
797
+ const match = findHighestPriorityRule('host_file_edit', ['host_file_edit:/etc/hosts'], '/tmp');
798
+ expect(match).not.toBeNull();
799
+ expect(match!.id).toBe('default:ask-host_file_edit-global');
800
+ expect(match!.decision).toBe('ask');
801
+ expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-host_file_edit-global')!);
802
+ });
803
+
804
+ test('findHighestPriorityRule matches default ask for host_bash', () => {
805
+ const match = findHighestPriorityRule('host_bash', ['ls'], '/tmp');
806
+ expect(match).not.toBeNull();
807
+ expect(match!.id).toBe('default:ask-host_bash-global');
808
+ expect(match!.decision).toBe('ask');
809
+ expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-host_bash-global')!);
810
+ });
811
+
812
+ test('findHighestPriorityRule matches default ask for computer_use_click', () => {
813
+ const match = findHighestPriorityRule('computer_use_click', ['computer_use_click:'], '/tmp');
814
+ expect(match).not.toBeNull();
815
+ expect(match!.id).toBe('default:ask-computer_use_click-global');
816
+ expect(match!.decision).toBe('ask');
817
+ expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-computer_use_click-global')!);
818
+ });
819
+
820
+ test('findHighestPriorityRule matches default ask for computer_use_request_control', () => {
821
+ const match = findHighestPriorityRule('computer_use_request_control', ['computer_use_request_control:'], '/tmp');
822
+ expect(match).not.toBeNull();
823
+ expect(match!.id).toBe('default:ask-computer_use_request_control-global');
824
+ expect(match!.decision).toBe('ask');
825
+ expect(match!.priority).toBe(DEFAULT_PRIORITY_BY_ID.get('default:ask-computer_use_request_control-global')!);
826
+ });
827
+
828
+ test('bootstrap delete rule matches only when workingDir is the workspace dir', () => {
829
+ const workspaceDir = join(testDir, 'workspace');
830
+ // Should match when workingDir is the workspace directory — the bootstrap
831
+ // rule (priority 100) outranks the global default allow (priority 50).
832
+ const match = findHighestPriorityRule('bash', ['rm BOOTSTRAP.md'], workspaceDir);
833
+ expect(match).not.toBeNull();
834
+ expect(match!.id).toBe('default:allow-bash-rm-bootstrap');
835
+ expect(match!.decision).toBe('allow');
836
+ // Outside workspace, the bootstrap rule doesn't match — the global
837
+ // default:allow-bash-global rule matches instead (not the bootstrap rule).
838
+ const other = findHighestPriorityRule('bash', ['rm BOOTSTRAP.md'], '/tmp/other-project');
839
+ expect(other).not.toBeNull();
840
+ expect(other!.id).not.toBe('default:allow-bash-rm-bootstrap');
841
+ expect(other!.id).toBe('default:allow-bash-global');
842
+ });
843
+
844
+ test('default ask does not affect files outside protected directory', () => {
845
+ const safePath = join(testDir, 'data', 'assistant.db');
846
+ const match = findHighestPriorityRule('file_read', [`file_read:${safePath}`], '/tmp');
847
+ // Should not match a default deny rule
848
+ expect(match === null || !match.id.startsWith('default:')).toBe(true);
849
+ });
850
+
851
+ test('default rules are backfilled after malformed JSON in trust file', () => {
852
+ mkdirSync(dirname(trustPath), { recursive: true });
853
+ writeFileSync(trustPath, 'NOT VALID JSON {{{');
854
+ clearCache();
855
+ const rules = getAllRules();
856
+ const defaults = rules.filter((r) => r.id.startsWith('default:'));
857
+ expect(defaults).toHaveLength(NUM_DEFAULTS);
858
+ });
859
+
860
+ test('default rules are backfilled in-memory after unknown file version without overwriting disk', () => {
861
+ mkdirSync(dirname(trustPath), { recursive: true });
862
+ const originalContent = JSON.stringify({ version: 9999, rules: [{ id: 'future-rule', tool: 'bash', pattern: 'future *', scope: 'everywhere', decision: 'allow', priority: 50, createdAt: 1000 }] });
863
+ writeFileSync(trustPath, originalContent);
864
+ clearCache();
865
+ const rules = getAllRules();
866
+ // Defaults should be present in-memory
867
+ const defaults = rules.filter((r) => r.id.startsWith('default:'));
868
+ expect(defaults).toHaveLength(NUM_DEFAULTS);
869
+ // The on-disk file must NOT be overwritten — it preserves the unknown format
870
+ const diskContent = readFileSync(trustPath, 'utf-8');
871
+ expect(diskContent).toBe(originalContent);
872
+ });
873
+
874
+ test('clearAllRules preserves default rules', () => {
875
+ addRule('bash', 'git *', '/tmp');
876
+ clearAllRules();
877
+ const rules = getAllRules();
878
+ // User rules should be gone, but defaults should remain
879
+ expect(rules.filter((r) => !r.id.startsWith('default:'))).toHaveLength(0);
880
+ const defaults = rules.filter((r) => r.id.startsWith('default:'));
881
+ expect(defaults).toHaveLength(NUM_DEFAULTS);
882
+ });
883
+
884
+ // ── skill source mutation rules ────────────────────────────────
885
+
886
+ test('default rules include ask rules for file_write on skill source paths', () => {
887
+ const rules = getAllRules();
888
+ const managed = rules.find((r) => r.id === 'default:ask-file_write-managed-skills');
889
+ expect(managed).toBeDefined();
890
+ expect(managed!.tool).toBe('file_write');
891
+ expect(managed!.decision).toBe('ask');
892
+ expect(managed!.priority).toBe(50);
893
+ expect(managed!.pattern).toContain('workspace/skills/**');
894
+
895
+ const bundled = rules.find((r) => r.id === 'default:ask-file_write-bundled-skills');
896
+ expect(bundled).toBeDefined();
897
+ expect(bundled!.tool).toBe('file_write');
898
+ expect(bundled!.decision).toBe('ask');
899
+ expect(bundled!.priority).toBe(50);
900
+ });
901
+
902
+ test('default rules include ask rules for file_edit on skill source paths', () => {
903
+ const rules = getAllRules();
904
+ const managed = rules.find((r) => r.id === 'default:ask-file_edit-managed-skills');
905
+ expect(managed).toBeDefined();
906
+ expect(managed!.tool).toBe('file_edit');
907
+ expect(managed!.decision).toBe('ask');
908
+ expect(managed!.priority).toBe(50);
909
+ expect(managed!.pattern).toContain('workspace/skills/**');
910
+
911
+ const bundled = rules.find((r) => r.id === 'default:ask-file_edit-bundled-skills');
912
+ expect(bundled).toBeDefined();
913
+ expect(bundled!.tool).toBe('file_edit');
914
+ expect(bundled!.decision).toBe('ask');
915
+ expect(bundled!.priority).toBe(50);
916
+ });
917
+
918
+ // ── default allow: skill_load ────────────────────────────────
919
+
920
+ test('skill_load default allow rule exists in templates', () => {
921
+ const templates = getDefaultRuleTemplates();
922
+ const skillLoadRule = templates.find(t => t.id === 'default:allow-skill_load-global');
923
+ expect(skillLoadRule).toBeDefined();
924
+ expect(skillLoadRule!.tool).toBe('skill_load');
925
+ expect(skillLoadRule!.pattern).toBe('skill_load:*');
926
+ expect(skillLoadRule!.decision).toBe('allow');
927
+ expect(skillLoadRule!.scope).toBe('everywhere');
928
+ });
929
+
930
+ test('findHighestPriorityRule matches default allow for skill_load', () => {
931
+ const match = findHighestPriorityRule('skill_load', ['skill_load:browser'], '/tmp');
932
+ expect(match).not.toBeNull();
933
+ expect(match!.id).toBe('default:allow-skill_load-global');
934
+ expect(match!.decision).toBe('allow');
935
+ expect(match!.priority).toBe(100);
936
+ });
937
+
938
+ test('findHighestPriorityRule matches default allow for skill_load with any skill name', () => {
939
+ const match = findHighestPriorityRule('skill_load', ['skill_load:some-random-skill'], '/tmp');
940
+ expect(match).not.toBeNull();
941
+ expect(match!.id).toBe('default:allow-skill_load-global');
942
+ expect(match!.decision).toBe('allow');
943
+ });
944
+
945
+ // ── default allow: browser tools ────────────────────────────
946
+
947
+ test('all 10 browser tools have default allow rules', () => {
948
+ const templates = getDefaultRuleTemplates();
949
+ const browserTools = [
950
+ 'browser_navigate', 'browser_snapshot', 'browser_screenshot', 'browser_close',
951
+ 'browser_click', 'browser_type', 'browser_press_key', 'browser_wait_for',
952
+ 'browser_extract', 'browser_fill_credential',
953
+ ];
954
+
955
+ for (const tool of browserTools) {
956
+ const rule = templates.find(t => t.id === `default:allow-${tool}-global`);
957
+ expect(rule).toBeDefined();
958
+ expect(rule!.tool).toBe(tool);
959
+ // browser_navigate uses standalone "**" because its candidates
960
+ // contain URLs with "/" that single "*" cannot match.
961
+ const expectedPattern = tool === 'browser_navigate' ? '**' : `${tool}:*`;
962
+ expect(rule!.pattern).toBe(expectedPattern);
963
+ expect(rule!.decision).toBe('allow');
964
+ expect(rule!.scope).toBe('everywhere');
965
+ }
966
+ });
967
+
968
+ test('browser tool default rules match via findHighestPriorityRule', () => {
969
+ // Use a candidate without slashes so the `browser_snapshot:*` pattern
970
+ // matches (minimatch `*` does not cross `/` boundaries).
971
+ const result = findHighestPriorityRule('browser_snapshot', ['browser_snapshot:'], '/tmp');
972
+ expect(result).toBeDefined();
973
+ expect(result!.decision).toBe('allow');
974
+ });
975
+
976
+ test('no default ask rules exist for file_read on skill source paths', () => {
977
+ const rules = getAllRules();
978
+ // There should be no default rules with IDs matching file_read for skill sources
979
+ const readManagedSkill = rules.find((r) => r.id === 'default:ask-file_read-managed-skills');
980
+ const readBundledSkill = rules.find((r) => r.id === 'default:ask-file_read-bundled-skills');
981
+ expect(readManagedSkill).toBeUndefined();
982
+ expect(readBundledSkill).toBeUndefined();
983
+ });
984
+
985
+ test('findHighestPriorityRule matches default ask for file_write on managed skill path', () => {
986
+ const skillFile = join(testDir, 'workspace', 'skills', 'my-skill', 'SKILL.md');
987
+ const match = findHighestPriorityRule('file_write', [`file_write:${skillFile}`], '/tmp');
988
+ expect(match).not.toBeNull();
989
+ expect(match!.id).toBe('default:ask-file_write-managed-skills');
990
+ expect(match!.decision).toBe('ask');
991
+ });
992
+
993
+ test('findHighestPriorityRule matches default ask for file_edit on managed skill path', () => {
994
+ const skillFile = join(testDir, 'workspace', 'skills', 'my-skill', 'tools.ts');
995
+ const match = findHighestPriorityRule('file_edit', [`file_edit:${skillFile}`], '/tmp');
996
+ expect(match).not.toBeNull();
997
+ expect(match!.id).toBe('default:ask-file_edit-managed-skills');
998
+ expect(match!.decision).toBe('ask');
999
+ });
1000
+ });
1001
+
1002
+ // ── trust rule schema v3 (PR 14) ──────────────────────────────
1003
+
1004
+ describe('trust rule schema v3 (PR 14)', () => {
1005
+ test('new rules can include v3 optional fields', () => {
1006
+ const rule = addRule('bash', 'git *', '/tmp');
1007
+ // Manually set v3 optional fields on the rule and persist
1008
+ rule.executionTarget = '/usr/local/bin/node';
1009
+ rule.allowHighRisk = true;
1010
+ // Re-persist the updated rules
1011
+ const rules = getAllRules().map((r) =>
1012
+ r.id === rule.id ? rule : r,
1013
+ );
1014
+ // Write directly to verify round-trip
1015
+ const trustData = { version: 3, rules };
1016
+ writeFileSync(trustPath, JSON.stringify(trustData, null, 2));
1017
+ clearCache();
1018
+ const reloaded = getAllRules();
1019
+ const found = reloaded.find((r) => r.id === rule.id);
1020
+ expect(found).toBeDefined();
1021
+ expect(found!.executionTarget).toBe('/usr/local/bin/node');
1022
+ expect(found!.allowHighRisk).toBe(true);
1023
+ });
1024
+
1025
+ test('v2 file is upgraded to v3 on disk', () => {
1026
+ mkdirSync(dirname(trustPath), { recursive: true });
1027
+ writeFileSync(trustPath, JSON.stringify({
1028
+ version: 2,
1029
+ rules: [{
1030
+ id: 'v2-rule',
1031
+ tool: 'bash',
1032
+ pattern: 'npm *',
1033
+ scope: 'everywhere',
1034
+ decision: 'allow',
1035
+ priority: 100,
1036
+ createdAt: 3000,
1037
+ }],
1038
+ }));
1039
+ clearCache();
1040
+ getAllRules(); // triggers load + migration
1041
+ const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
1042
+ expect(data.version).toBe(3);
1043
+ });
1044
+
1045
+ test('v2 rules survive v3 migration with no v3-only fields', () => {
1046
+ mkdirSync(dirname(trustPath), { recursive: true });
1047
+ writeFileSync(trustPath, JSON.stringify({
1048
+ version: 2,
1049
+ rules: [
1050
+ {
1051
+ id: 'user-v2-a',
1052
+ tool: 'bash',
1053
+ pattern: 'git *',
1054
+ scope: '/tmp',
1055
+ decision: 'allow',
1056
+ priority: 100,
1057
+ createdAt: 4000,
1058
+ },
1059
+ {
1060
+ id: 'user-v2-b',
1061
+ tool: 'file_write',
1062
+ pattern: '/tmp/*',
1063
+ scope: '/tmp',
1064
+ decision: 'deny',
1065
+ priority: 50,
1066
+ createdAt: 4001,
1067
+ },
1068
+ ],
1069
+ }));
1070
+ clearCache();
1071
+ const rules = getAllRules();
1072
+ const ruleA = rules.find((r) => r.id === 'user-v2-a');
1073
+ const ruleB = rules.find((r) => r.id === 'user-v2-b');
1074
+ expect(ruleA).toBeDefined();
1075
+ expect(ruleB).toBeDefined();
1076
+ expect(ruleA!.pattern).toBe('git *');
1077
+ expect(ruleB!.decision).toBe('deny');
1078
+ // No v3-only fields should be present
1079
+ expect(ruleA).not.toHaveProperty('executionTarget');
1080
+ expect(ruleA).not.toHaveProperty('allowHighRisk');
1081
+ });
1082
+
1083
+ test('trust file persists with version 3', () => {
1084
+ addRule('bash', 'echo *', '/tmp');
1085
+ const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
1086
+ expect(data.version).toBe(3);
1087
+ });
1088
+ });
1089
+
1090
+ // ── v2 → v3 migration hardening (PR 15) ────────────────────────
1091
+
1092
+ describe('v2 → v3 migration hardening (PR 15)', () => {
1093
+ test('v2 rules with extra unknown fields survive migration cleanly', () => {
1094
+ mkdirSync(dirname(trustPath), { recursive: true });
1095
+ writeFileSync(trustPath, JSON.stringify({
1096
+ version: 2,
1097
+ rules: [{
1098
+ id: 'v2-extra-fields',
1099
+ tool: 'bash',
1100
+ pattern: 'git *',
1101
+ scope: '/tmp',
1102
+ decision: 'allow',
1103
+ priority: 100,
1104
+ createdAt: 5000,
1105
+ customField: 'should-survive',
1106
+ nested: { deep: true },
1107
+ }],
1108
+ }));
1109
+ clearCache();
1110
+ const rules = getAllRules();
1111
+ const rule = rules.find((r) => r.id === 'v2-extra-fields');
1112
+ expect(rule).toBeDefined();
1113
+ expect(rule!.tool).toBe('bash');
1114
+ expect(rule!.pattern).toBe('git *');
1115
+ // Extra fields pass through because the migration does not strip them
1116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- asserting extra fields pass through migration
1117
+ expect((rule as any).customField).toBe('should-survive');
1118
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- asserting extra fields pass through migration
1119
+ expect((rule as any).nested).toEqual({ deep: true });
1120
+ });
1121
+
1122
+ test('v2 file with empty rules array migrates correctly', () => {
1123
+ mkdirSync(dirname(trustPath), { recursive: true });
1124
+ writeFileSync(trustPath, JSON.stringify({
1125
+ version: 2,
1126
+ rules: [],
1127
+ }));
1128
+ clearCache();
1129
+ const rules = getAllRules();
1130
+ // Should only have default rules, no user rules
1131
+ expect(rules).toHaveLength(NUM_DEFAULTS);
1132
+ expect(rules.every((r) => r.id.startsWith('default:'))).toBe(true);
1133
+ // File should be upgraded to v3 on disk
1134
+ const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
1135
+ expect(data.version).toBe(3);
1136
+ });
1137
+
1138
+ test('v2 file with no rules field at all migrates correctly', () => {
1139
+ mkdirSync(dirname(trustPath), { recursive: true });
1140
+ writeFileSync(trustPath, JSON.stringify({
1141
+ version: 2,
1142
+ }));
1143
+ clearCache();
1144
+ const rules = getAllRules();
1145
+ // rules defaults to [] so only defaults should appear
1146
+ expect(rules).toHaveLength(NUM_DEFAULTS);
1147
+ expect(rules.every((r) => r.id.startsWith('default:'))).toBe(true);
1148
+ // File should be upgraded to v3 on disk
1149
+ const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
1150
+ expect(data.version).toBe(3);
1151
+ });
1152
+
1153
+ test('malformed v2 file (rules is a string instead of array) is handled gracefully', () => {
1154
+ mkdirSync(dirname(trustPath), { recursive: true });
1155
+ writeFileSync(trustPath, JSON.stringify({
1156
+ version: 2,
1157
+ rules: 'not-an-array',
1158
+ }));
1159
+ clearCache();
1160
+ const rules = getAllRules();
1161
+ // Should fall back to empty rules and backfill defaults
1162
+ expect(rules).toHaveLength(NUM_DEFAULTS);
1163
+ expect(rules.every((r) => r.id.startsWith('default:'))).toBe(true);
1164
+ });
1165
+
1166
+ test('malformed v2 file (rules is an object instead of array) is handled gracefully', () => {
1167
+ mkdirSync(dirname(trustPath), { recursive: true });
1168
+ writeFileSync(trustPath, JSON.stringify({
1169
+ version: 2,
1170
+ rules: { notAnArray: true },
1171
+ }));
1172
+ clearCache();
1173
+ const rules = getAllRules();
1174
+ expect(rules).toHaveLength(NUM_DEFAULTS);
1175
+ expect(rules.every((r) => r.id.startsWith('default:'))).toBe(true);
1176
+ });
1177
+
1178
+ test('malformed file (valid JSON but null) is handled gracefully', () => {
1179
+ mkdirSync(dirname(trustPath), { recursive: true });
1180
+ writeFileSync(trustPath, 'null');
1181
+ clearCache();
1182
+ const rules = getAllRules();
1183
+ // Accessing null.version throws TypeError, caught by try/catch,
1184
+ // falls through to backfill defaults
1185
+ expect(rules).toHaveLength(NUM_DEFAULTS);
1186
+ });
1187
+
1188
+ test('concurrent v2 → v3 migration (loading twice in sequence) is idempotent', () => {
1189
+ mkdirSync(dirname(trustPath), { recursive: true });
1190
+ writeFileSync(trustPath, JSON.stringify({
1191
+ version: 2,
1192
+ rules: [{
1193
+ id: 'idempotent-rule',
1194
+ tool: 'bash',
1195
+ pattern: 'npm *',
1196
+ scope: 'everywhere',
1197
+ decision: 'allow',
1198
+ priority: 100,
1199
+ createdAt: 6000,
1200
+ }],
1201
+ }));
1202
+ // First load — triggers v2 → v3 migration
1203
+ clearCache();
1204
+ const rules1 = getAllRules();
1205
+ const rule1 = rules1.find((r) => r.id === 'idempotent-rule');
1206
+ expect(rule1).toBeDefined();
1207
+ expect(rule1!.pattern).toBe('npm *');
1208
+
1209
+ // Second load — should load the already-migrated v3 file without re-migrating
1210
+ clearCache();
1211
+ const rules2 = getAllRules();
1212
+ const rule2 = rules2.find((r) => r.id === 'idempotent-rule');
1213
+ expect(rule2).toBeDefined();
1214
+ expect(rule2!.pattern).toBe('npm *');
1215
+ expect(rule2!.priority).toBe(100);
1216
+
1217
+ // Verify file is still v3 and rule count is stable
1218
+ const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
1219
+ expect(data.version).toBe(3);
1220
+ const userRules = data.rules.filter((r: { id: string }) => !r.id.startsWith('default:'));
1221
+ expect(userRules).toHaveLength(1);
1222
+ });
1223
+
1224
+ test('v3 file with optional fields is loaded correctly without re-migration', () => {
1225
+ mkdirSync(dirname(trustPath), { recursive: true });
1226
+ const v3Rules = [
1227
+ {
1228
+ id: 'v3-with-options',
1229
+ tool: 'bash',
1230
+ pattern: 'skill-cmd *',
1231
+ scope: '/tmp',
1232
+ decision: 'allow',
1233
+ priority: 100,
1234
+ createdAt: 7000,
1235
+ executionTarget: '/usr/bin/node',
1236
+ allowHighRisk: false,
1237
+ },
1238
+ {
1239
+ id: 'v3-without-options',
1240
+ tool: 'bash',
1241
+ pattern: 'git *',
1242
+ scope: '/tmp',
1243
+ decision: 'allow',
1244
+ priority: 100,
1245
+ createdAt: 7001,
1246
+ },
1247
+ ];
1248
+ writeFileSync(trustPath, JSON.stringify({ version: 3, rules: v3Rules }));
1249
+ clearCache();
1250
+ const rules = getAllRules();
1251
+
1252
+ // Rule with optional fields should have them preserved
1253
+ const withOptions = rules.find((r) => r.id === 'v3-with-options');
1254
+ expect(withOptions).toBeDefined();
1255
+ expect(withOptions!.executionTarget).toBe('/usr/bin/node');
1256
+ expect(withOptions!.allowHighRisk).toBe(false);
1257
+
1258
+ // Rule without optional fields should remain without them
1259
+ const withoutOptions = rules.find((r) => r.id === 'v3-without-options');
1260
+ expect(withoutOptions).toBeDefined();
1261
+ expect(withoutOptions).not.toHaveProperty('executionTarget');
1262
+ });
1263
+
1264
+ test('v2 migration preserves rule meaning exactly — no extra fields added', () => {
1265
+ mkdirSync(dirname(trustPath), { recursive: true });
1266
+ const originalRules = [
1267
+ {
1268
+ id: 'preserve-a',
1269
+ tool: 'bash',
1270
+ pattern: 'git *',
1271
+ scope: '/home/user',
1272
+ decision: 'allow' as const,
1273
+ priority: 100,
1274
+ createdAt: 8000,
1275
+ },
1276
+ {
1277
+ id: 'preserve-b',
1278
+ tool: 'file_write',
1279
+ pattern: '/tmp/**',
1280
+ scope: 'everywhere',
1281
+ decision: 'deny' as const,
1282
+ priority: 50,
1283
+ createdAt: 8001,
1284
+ },
1285
+ ];
1286
+ writeFileSync(trustPath, JSON.stringify({ version: 2, rules: originalRules }));
1287
+ clearCache();
1288
+ const rules = getAllRules();
1289
+
1290
+ for (const original of originalRules) {
1291
+ const migrated = rules.find((r) => r.id === original.id);
1292
+ expect(migrated).toBeDefined();
1293
+ // Every original field is preserved exactly
1294
+ expect(migrated!.tool).toBe(original.tool);
1295
+ expect(migrated!.pattern).toBe(original.pattern);
1296
+ expect(migrated!.scope).toBe(original.scope);
1297
+ expect(migrated!.decision).toBe(original.decision);
1298
+ expect(migrated!.priority).toBe(original.priority);
1299
+ expect(migrated!.createdAt).toBe(original.createdAt);
1300
+ // No extra fields were injected by migration
1301
+ expect(migrated).not.toHaveProperty('executionTarget');
1302
+ expect(migrated).not.toHaveProperty('allowHighRisk');
1303
+ }
1304
+ });
1305
+
1306
+ test('v1 → v3 full migration preserves rules and adds priority', () => {
1307
+ mkdirSync(dirname(trustPath), { recursive: true });
1308
+ writeFileSync(trustPath, JSON.stringify({
1309
+ version: 1,
1310
+ rules: [{
1311
+ id: 'v1-full-migration',
1312
+ tool: 'bash',
1313
+ pattern: 'docker *',
1314
+ scope: '/srv',
1315
+ decision: 'allow',
1316
+ createdAt: 9000,
1317
+ }],
1318
+ }));
1319
+ clearCache();
1320
+ const rules = getAllRules();
1321
+ const rule = rules.find((r) => r.id === 'v1-full-migration');
1322
+ expect(rule).toBeDefined();
1323
+ // v1 → v2 adds priority 100
1324
+ expect(rule!.priority).toBe(100);
1325
+ // File should be v3 on disk
1326
+ const data = JSON.parse(readFileSync(trustPath, 'utf-8'));
1327
+ expect(data.version).toBe(3);
1328
+ });
1329
+ });
1330
+
1331
+ // ── executionTarget-aware rule matching ──────────────────────
1332
+
1333
+ describe('executionTarget-aware rule matching', () => {
1334
+ /**
1335
+ * Helper: write a v3 trust file with the given rules directly to disk,
1336
+ * then clear the cache so the next getRules() call picks them up.
1337
+ */
1338
+ function seedRules(rules: Array<Record<string, unknown>>): void {
1339
+ mkdirSync(dirname(trustPath), { recursive: true });
1340
+ writeFileSync(trustPath, JSON.stringify({ version: 3, rules }));
1341
+ clearCache();
1342
+ }
1343
+
1344
+ // ── wildcard semantics (no executionTarget on rule) ──────────
1345
+
1346
+ describe('wildcard semantics — rules without executionTarget', () => {
1347
+ test('rule with no executionTarget matches when no context is provided', () => {
1348
+ addRule('bash', 'git *', '/tmp', 'allow', 200);
1349
+ const match = findHighestPriorityRule('bash', ['git status'], '/tmp');
1350
+ expect(match).not.toBeNull();
1351
+ expect(match!.decision).toBe('allow');
1352
+ });
1353
+
1354
+ test('rule with no executionTarget matches any execution target', () => {
1355
+ addRule('bash', 'git *', '/tmp', 'allow', 200);
1356
+ const match = findHighestPriorityRule('bash', ['git status'], '/tmp', {
1357
+ executionTarget: '/usr/bin/node',
1358
+ });
1359
+ expect(match).not.toBeNull();
1360
+ expect(match!.decision).toBe('allow');
1361
+ });
1362
+ });
1363
+
1364
+ // ── executionTarget matching ──────────────────────────────────
1365
+
1366
+ describe('executionTarget matching', () => {
1367
+ test('rule with executionTarget matches exact target', () => {
1368
+ seedRules([{
1369
+ id: 'et-exact',
1370
+ tool: 'bash',
1371
+ pattern: 'run *',
1372
+ scope: 'everywhere',
1373
+ decision: 'allow',
1374
+ priority: 200,
1375
+ createdAt: Date.now(),
1376
+ executionTarget: '/usr/local/bin/node',
1377
+ }]);
1378
+ const match = findHighestPriorityRule('bash', ['run script.js'], '/tmp', {
1379
+ executionTarget: '/usr/local/bin/node',
1380
+ });
1381
+ expect(match).not.toBeNull();
1382
+ expect(match!.id).toBe('et-exact');
1383
+ });
1384
+
1385
+ test('rule with executionTarget does NOT match different target', () => {
1386
+ seedRules([{
1387
+ id: 'et-diff',
1388
+ tool: 'bash',
1389
+ pattern: 'run *',
1390
+ scope: 'everywhere',
1391
+ decision: 'allow',
1392
+ priority: 200,
1393
+ createdAt: Date.now(),
1394
+ executionTarget: '/usr/local/bin/node',
1395
+ }]);
1396
+ const match = findHighestPriorityRule('bash', ['run script.js'], '/tmp', {
1397
+ executionTarget: '/usr/local/bin/bun',
1398
+ });
1399
+ expect(match === null || match.id !== 'et-diff').toBe(true);
1400
+ });
1401
+
1402
+ test('rule with executionTarget does NOT match when no target in context', () => {
1403
+ seedRules([{
1404
+ id: 'et-no-ctx',
1405
+ tool: 'bash',
1406
+ pattern: 'run *',
1407
+ scope: 'everywhere',
1408
+ decision: 'allow',
1409
+ priority: 200,
1410
+ createdAt: Date.now(),
1411
+ executionTarget: '/usr/local/bin/node',
1412
+ }]);
1413
+ const match = findHighestPriorityRule('bash', ['run script.js'], '/tmp', {});
1414
+ expect(match === null || match.id !== 'et-no-ctx').toBe(true);
1415
+ });
1416
+
1417
+ test('rule WITHOUT executionTarget matches any target (wildcard)', () => {
1418
+ addRule('bash', 'run *', '/tmp', 'allow', 200);
1419
+ const match = findHighestPriorityRule('bash', ['run script.js'], '/tmp', {
1420
+ executionTarget: '/any/path/to/runtime',
1421
+ });
1422
+ expect(match).not.toBeNull();
1423
+ expect(match!.pattern).toBe('run *');
1424
+ });
1425
+ });
1426
+
1427
+ // ── backward compatibility ────────────────────────────────────
1428
+
1429
+ describe('backward compatibility', () => {
1430
+ test('existing callers without ctx parameter still work', () => {
1431
+ addRule('bash', 'git *', '/tmp', 'allow', 200);
1432
+ // Calling without the 4th argument — must still match
1433
+ const match = findHighestPriorityRule('bash', ['git status'], '/tmp');
1434
+ expect(match).not.toBeNull();
1435
+ expect(match!.pattern).toBe('git *');
1436
+ });
1437
+
1438
+ test('empty PolicyContext object behaves the same as no context', () => {
1439
+ addRule('bash', 'ls *', '/tmp', 'allow', 200);
1440
+ const matchNoCtx = findHighestPriorityRule('bash', ['ls -la'], '/tmp');
1441
+ const matchEmptyCtx = findHighestPriorityRule('bash', ['ls -la'], '/tmp', {});
1442
+ expect(matchNoCtx).not.toBeNull();
1443
+ expect(matchEmptyCtx).not.toBeNull();
1444
+ expect(matchNoCtx!.id).toBe(matchEmptyCtx!.id);
1445
+ });
1446
+ });
1447
+ });
1448
+
1449
+ // ── network_request trust rule matching ────────────────────────
1450
+
1451
+ describe('network_request trust rules', () => {
1452
+ test('exact origin rule matches network_request candidates', () => {
1453
+ addRule('network_request', 'network_request:https://api.example.com/*', 'everywhere');
1454
+ const rule = findHighestPriorityRule(
1455
+ 'network_request',
1456
+ ['network_request:https://api.example.com/v1/data', 'network_request:https://api.example.com/*'],
1457
+ '/tmp',
1458
+ );
1459
+ expect(rule).not.toBeNull();
1460
+ expect(rule!.decision).toBe('allow');
1461
+ });
1462
+
1463
+ test('exact url rule matches only that url candidate', () => {
1464
+ addRule('network_request', 'network_request:https://api.example.com/v1/data', 'everywhere');
1465
+ const match = findHighestPriorityRule(
1466
+ 'network_request',
1467
+ ['network_request:https://api.example.com/v1/data', 'network_request:https://api.example.com/*'],
1468
+ '/tmp',
1469
+ );
1470
+ expect(match).not.toBeNull();
1471
+
1472
+ const noMatch = findHighestPriorityRule(
1473
+ 'network_request',
1474
+ ['network_request:https://api.example.com/v2/other'],
1475
+ '/tmp',
1476
+ );
1477
+ expect(noMatch).toBeNull();
1478
+ });
1479
+
1480
+ test('globstar rule matches any network_request candidate', () => {
1481
+ // minimatch treats standalone "**" as globstar (matching "/"), but
1482
+ // "network_request:*" uses single "*" which doesn't cross slashes.
1483
+ // The tool field is already filtered by findHighestPriorityRule, so
1484
+ // "**" is the correct catch-all pattern.
1485
+ addRule('network_request', '**', 'everywhere');
1486
+ const rule = findHighestPriorityRule(
1487
+ 'network_request',
1488
+ ['network_request:https://any-host.example.org/path'],
1489
+ '/tmp',
1490
+ );
1491
+ expect(rule).not.toBeNull();
1492
+ });
1493
+
1494
+ test('single-star wildcard matches flat candidates only', () => {
1495
+ // "network_request:*" won't match URLs with slashes — consistent
1496
+ // with the behavior of web_fetch:* and browser_navigate:* patterns.
1497
+ addRule('network_request', 'network_request:*', 'everywhere');
1498
+ const noSlashMatch = findHighestPriorityRule(
1499
+ 'network_request',
1500
+ ['network_request:flat-target'],
1501
+ '/tmp',
1502
+ );
1503
+ expect(noSlashMatch).not.toBeNull();
1504
+
1505
+ const slashNoMatch = findHighestPriorityRule(
1506
+ 'network_request',
1507
+ ['network_request:https://example.com/path'],
1508
+ '/tmp',
1509
+ );
1510
+ // Single "*" does not match "/" so this URL candidate won't match.
1511
+ expect(slashNoMatch).toBeNull();
1512
+ });
1513
+
1514
+ test('network_request rule does not match web_fetch tool', () => {
1515
+ addRule('network_request', 'network_request:https://api.example.com/*', 'everywhere');
1516
+ const rule = findHighestPriorityRule(
1517
+ 'web_fetch',
1518
+ ['web_fetch:https://api.example.com/v1/data', 'web_fetch:https://api.example.com/*'],
1519
+ '/tmp',
1520
+ );
1521
+ expect(rule).toBeNull();
1522
+ });
1523
+
1524
+ test('web_fetch rule does not match network_request tool', () => {
1525
+ addRule('web_fetch', 'web_fetch:https://api.example.com/*', 'everywhere');
1526
+ const rule = findHighestPriorityRule(
1527
+ 'network_request',
1528
+ ['network_request:https://api.example.com/v1/data', 'network_request:https://api.example.com/*'],
1529
+ '/tmp',
1530
+ );
1531
+ expect(rule).toBeNull();
1532
+ });
1533
+
1534
+ test('deny rule takes precedence over allow at same priority', () => {
1535
+ addRule('network_request', 'network_request:https://api.example.com/*', 'everywhere', 'allow', 100);
1536
+ addRule('network_request', 'network_request:https://api.example.com/*', 'everywhere', 'deny', 100);
1537
+ const rule = findHighestPriorityRule(
1538
+ 'network_request',
1539
+ ['network_request:https://api.example.com/v1/data', 'network_request:https://api.example.com/*'],
1540
+ '/tmp',
1541
+ );
1542
+ expect(rule).not.toBeNull();
1543
+ expect(rule!.decision).toBe('deny');
1544
+ });
1545
+
1546
+ test('higher-priority allow overrides lower-priority deny', () => {
1547
+ addRule('network_request', 'network_request:https://api.example.com/*', 'everywhere', 'deny', 50);
1548
+ addRule('network_request', 'network_request:https://api.example.com/*', 'everywhere', 'allow', 100);
1549
+ const rule = findHighestPriorityRule(
1550
+ 'network_request',
1551
+ ['network_request:https://api.example.com/v1/data', 'network_request:https://api.example.com/*'],
1552
+ '/tmp',
1553
+ );
1554
+ expect(rule).not.toBeNull();
1555
+ expect(rule!.decision).toBe('allow');
1556
+ });
1557
+
1558
+ test('scope restricts network_request rule matching', () => {
1559
+ addRule('network_request', 'network_request:https://api.example.com/*', '/home/user/project');
1560
+ const inScope = findHighestPriorityRule(
1561
+ 'network_request',
1562
+ ['network_request:https://api.example.com/*'],
1563
+ '/home/user/project',
1564
+ );
1565
+ expect(inScope).not.toBeNull();
1566
+
1567
+ const outOfScope = findHighestPriorityRule(
1568
+ 'network_request',
1569
+ ['network_request:https://api.example.com/*'],
1570
+ '/tmp/other',
1571
+ );
1572
+ expect(outOfScope).toBeNull();
1573
+ });
1574
+ });
1575
+ });
1576
+
1577
+ describe('computer-use tool trust rule matching', () => {
1578
+ test('actionable CU tools have default ask trust rules', () => {
1579
+ // Actionable CU tools (those that perform screen interactions) should
1580
+ // have default "ask" rules so strict mode prompts before use.
1581
+ const actionableCuTools = [
1582
+ 'computer_use_click',
1583
+ 'computer_use_type_text',
1584
+ 'computer_use_request_control',
1585
+ ];
1586
+
1587
+ for (const name of actionableCuTools) {
1588
+ const rule = findHighestPriorityRule(name, [name], '/tmp/test');
1589
+ expect(rule).not.toBeNull();
1590
+ expect(rule!.decision).toBe('ask');
1591
+ }
1592
+ });
1593
+
1594
+ test('terminal CU tools (done/respond) have no default trust rules', () => {
1595
+ // computer_use_done and computer_use_respond are terminal signal tools
1596
+ // with RiskLevel.Low — they should not have ask rules since they don't
1597
+ // perform any screen action.
1598
+ const terminalCuTools = ['computer_use_done', 'computer_use_respond'];
1599
+
1600
+ for (const name of terminalCuTools) {
1601
+ const defaultRule = DEFAULT_TEMPLATES.find((t) => t.tool === name);
1602
+ expect(defaultRule).toBeUndefined();
1603
+ }
1604
+ });
1605
+ });