vellum 0.2.1 → 0.2.2

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 (349) hide show
  1. package/README.md +15 -2
  2. package/bun.lock +5 -2
  3. package/package.json +4 -2
  4. package/scripts/capture-x-graphql.ts +562 -0
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
  6. package/scripts/test.sh +5 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
  8. package/src/__tests__/account-registry.test.ts +2 -1
  9. package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
  10. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  11. package/src/__tests__/asset-search-tool.test.ts +23 -22
  12. package/src/__tests__/attachments-store.test.ts +56 -127
  13. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  14. package/src/__tests__/browser-skill-endstate.test.ts +4 -3
  15. package/src/__tests__/call-bridge.test.ts +385 -0
  16. package/src/__tests__/call-constants.test.ts +40 -0
  17. package/src/__tests__/call-orchestrator.test.ts +130 -4
  18. package/src/__tests__/call-recovery.test.ts +518 -0
  19. package/src/__tests__/call-routes-http.test.ts +459 -0
  20. package/src/__tests__/call-state-machine.test.ts +143 -0
  21. package/src/__tests__/call-store.test.ts +216 -1
  22. package/src/__tests__/cli-discover.test.ts +1 -1
  23. package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
  24. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  25. package/src/__tests__/computer-use-tools.test.ts +250 -0
  26. package/src/__tests__/config-schema.test.ts +299 -3
  27. package/src/__tests__/conflict-store.test.ts +2 -1
  28. package/src/__tests__/contacts-tools.test.ts +331 -0
  29. package/src/__tests__/conversation-store.test.ts +30 -32
  30. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  31. package/src/__tests__/date-context.test.ts +373 -0
  32. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  33. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  34. package/src/__tests__/followup-tools.test.ts +303 -0
  35. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  36. package/src/__tests__/intent-routing.test.ts +64 -57
  37. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  38. package/src/__tests__/ipc-snapshot.test.ts +62 -28
  39. package/src/__tests__/llm-usage-store.test.ts +3 -8
  40. package/src/__tests__/media-generate-image.test.ts +1 -1
  41. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  42. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  43. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  44. package/src/__tests__/playbook-tools.test.ts +342 -0
  45. package/src/__tests__/profile-compiler.test.ts +2 -1
  46. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  47. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  48. package/src/__tests__/recurrence-engine.test.ts +69 -0
  49. package/src/__tests__/recurrence-types.test.ts +71 -0
  50. package/src/__tests__/registry.test.ts +5 -3
  51. package/src/__tests__/relay-server.test.ts +633 -0
  52. package/src/__tests__/reminder-store.test.ts +6 -3
  53. package/src/__tests__/reminder.test.ts +43 -77
  54. package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
  55. package/src/__tests__/run-orchestrator.test.ts +4 -4
  56. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
  57. package/src/__tests__/runtime-runs-http.test.ts +4 -4
  58. package/src/__tests__/runtime-runs.test.ts +4 -4
  59. package/src/__tests__/schedule-store.test.ts +482 -0
  60. package/src/__tests__/schedule-tools.test.ts +700 -0
  61. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  62. package/src/__tests__/server-history-render.test.ts +14 -13
  63. package/src/__tests__/session-error.test.ts +28 -0
  64. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  65. package/src/__tests__/session-queue.test.ts +71 -48
  66. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  67. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  68. package/src/__tests__/signup-e2e.test.ts +2 -1
  69. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  70. package/src/__tests__/skill-script-runner.test.ts +159 -0
  71. package/src/__tests__/speaker-identification.test.ts +52 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  73. package/src/__tests__/subagent-tools.test.ts +141 -41
  74. package/src/__tests__/task-compiler.test.ts +2 -1
  75. package/src/__tests__/task-runner.test.ts +2 -1
  76. package/src/__tests__/task-scheduler.test.ts +2 -1
  77. package/src/__tests__/task-tools.test.ts +49 -56
  78. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  79. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  80. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  81. package/src/__tests__/tool-executor.test.ts +13 -17
  82. package/src/__tests__/turn-commit.test.ts +218 -3
  83. package/src/__tests__/twilio-provider.test.ts +143 -0
  84. package/src/__tests__/twilio-routes.test.ts +789 -0
  85. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  86. package/src/__tests__/view-image-tool.test.ts +217 -0
  87. package/src/__tests__/workspace-git-service.test.ts +186 -0
  88. package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
  89. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  90. package/src/bundler/app-bundler.ts +12 -8
  91. package/src/calls/call-bridge.ts +95 -0
  92. package/src/calls/call-constants.ts +43 -5
  93. package/src/calls/call-domain.ts +276 -0
  94. package/src/calls/call-orchestrator.ts +43 -17
  95. package/src/calls/call-recovery.ts +207 -0
  96. package/src/calls/call-state-machine.ts +68 -0
  97. package/src/calls/call-store.ts +192 -5
  98. package/src/calls/relay-server.ts +41 -4
  99. package/src/calls/speaker-identification.ts +213 -0
  100. package/src/calls/twilio-provider.ts +10 -6
  101. package/src/calls/twilio-routes.ts +90 -76
  102. package/src/calls/types.ts +1 -1
  103. package/src/cli/config-commands.ts +334 -0
  104. package/src/cli/core-commands.ts +776 -0
  105. package/src/cli/doordash.ts +251 -1
  106. package/src/cli/ipc-client.ts +82 -0
  107. package/src/cli/map.ts +246 -0
  108. package/src/cli/twitter.ts +575 -0
  109. package/src/cli.ts +7 -5
  110. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  111. package/src/commands/cc-command-registry.ts +209 -0
  112. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  113. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  114. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  115. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  116. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  117. package/src/config/bundled-skills/document/SKILL.md +18 -0
  118. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  119. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  120. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  121. package/src/config/bundled-skills/doordash/SKILL.md +82 -23
  122. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  123. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  124. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  125. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  126. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  127. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
  128. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  129. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  130. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  131. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  132. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  133. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  134. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  135. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  136. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  137. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  138. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  139. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  141. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  142. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  143. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  144. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  145. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  146. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  147. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  148. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  149. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  150. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  151. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  152. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  153. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  154. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  155. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  156. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  157. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  158. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  159. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  160. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  161. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  162. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  163. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  164. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  165. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  166. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  167. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  168. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  169. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  170. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  171. package/src/config/defaults.ts +33 -0
  172. package/src/config/loader.ts +4 -1
  173. package/src/config/schema.ts +161 -1
  174. package/src/config/system-prompt.ts +61 -16
  175. package/src/config/templates/IDENTITY.md +7 -0
  176. package/src/config/types.ts +4 -0
  177. package/src/contacts/contact-store.ts +4 -4
  178. package/src/daemon/assistant-attachments.ts +10 -0
  179. package/src/daemon/classifier.ts +3 -1
  180. package/src/daemon/computer-use-session.ts +3 -1
  181. package/src/daemon/date-context.ts +136 -0
  182. package/src/daemon/handlers/apps.ts +16 -1
  183. package/src/daemon/handlers/browser.ts +54 -0
  184. package/src/daemon/handlers/computer-use.ts +7 -1
  185. package/src/daemon/handlers/config.ts +163 -5
  186. package/src/daemon/handlers/diagnostics.ts +5 -1
  187. package/src/daemon/handlers/documents.ts +18 -29
  188. package/src/daemon/handlers/home-base.ts +5 -1
  189. package/src/daemon/handlers/index.ts +40 -277
  190. package/src/daemon/handlers/misc.ts +9 -1
  191. package/src/daemon/handlers/publish.ts +6 -1
  192. package/src/daemon/handlers/sessions.ts +65 -12
  193. package/src/daemon/handlers/shared.ts +36 -1
  194. package/src/daemon/handlers/signing.ts +37 -0
  195. package/src/daemon/handlers/skills.ts +20 -6
  196. package/src/daemon/handlers/subagents.ts +8 -3
  197. package/src/daemon/handlers/twitter-auth.ts +169 -0
  198. package/src/daemon/handlers/work-items.ts +384 -68
  199. package/src/daemon/ipc-contract-inventory.json +28 -4
  200. package/src/daemon/ipc-contract.ts +133 -37
  201. package/src/daemon/ipc-protocol.ts +7 -2
  202. package/src/daemon/lifecycle.ts +21 -0
  203. package/src/daemon/main.ts +10 -4
  204. package/src/daemon/ride-shotgun-handler.ts +74 -10
  205. package/src/daemon/server.ts +143 -26
  206. package/src/daemon/session-agent-loop.ts +887 -0
  207. package/src/daemon/session-attachments.ts +28 -5
  208. package/src/daemon/session-error.ts +24 -3
  209. package/src/daemon/session-lifecycle.ts +147 -0
  210. package/src/daemon/session-media-retry.ts +147 -0
  211. package/src/daemon/session-messaging.ts +145 -0
  212. package/src/daemon/session-notifiers.ts +164 -0
  213. package/src/daemon/session-process.ts +2 -2
  214. package/src/daemon/session-queue-manager.ts +1 -0
  215. package/src/daemon/session-runtime-assembly.ts +52 -0
  216. package/src/daemon/session-skill-tools.ts +124 -5
  217. package/src/daemon/session-slash.ts +3 -0
  218. package/src/daemon/session-surfaces.ts +77 -2
  219. package/src/daemon/session-tool-setup.ts +216 -2
  220. package/src/daemon/session-usage.ts +0 -2
  221. package/src/daemon/session.ts +114 -1404
  222. package/src/daemon/video-thumbnail.ts +60 -0
  223. package/src/doordash/client.ts +121 -27
  224. package/src/doordash/queries.ts +1 -2
  225. package/src/export/formatter.ts +3 -1
  226. package/src/followups/followup-store.ts +4 -2
  227. package/src/followups/types.ts +6 -0
  228. package/src/hooks/templates.ts +1 -1
  229. package/src/index.ts +32 -1153
  230. package/src/memory/attachments-store.ts +28 -83
  231. package/src/memory/channel-delivery-store.ts +7 -21
  232. package/src/memory/clarification-resolver.ts +6 -5
  233. package/src/memory/contradiction-checker.ts +3 -2
  234. package/src/memory/conversation-key-store.ts +10 -29
  235. package/src/memory/conversation-store.ts +2 -1
  236. package/src/memory/db.ts +96 -2
  237. package/src/memory/entity-extractor.ts +6 -3
  238. package/src/memory/items-extractor.ts +5 -4
  239. package/src/memory/jobs-store.ts +3 -2
  240. package/src/memory/llm-usage-store.ts +1 -2
  241. package/src/memory/runs-store.ts +1 -2
  242. package/src/memory/schema.ts +23 -2
  243. package/src/messaging/style-analyzer.ts +3 -2
  244. package/src/messaging/thread-summarizer.ts +8 -12
  245. package/src/messaging/triage-engine.ts +4 -2
  246. package/src/providers/openrouter/client.ts +20 -0
  247. package/src/providers/registry.ts +8 -0
  248. package/src/runtime/http-server.ts +108 -20
  249. package/src/runtime/routes/attachment-routes.ts +2 -3
  250. package/src/runtime/routes/call-routes.ts +140 -0
  251. package/src/runtime/routes/channel-routes.ts +5 -10
  252. package/src/runtime/routes/conversation-routes.ts +5 -5
  253. package/src/runtime/routes/run-routes.ts +2 -2
  254. package/src/runtime/run-orchestrator.ts +9 -3
  255. package/src/schedule/recurrence-engine.ts +138 -0
  256. package/src/schedule/recurrence-types.ts +67 -0
  257. package/src/schedule/schedule-store.ts +102 -57
  258. package/src/schedule/scheduler.ts +9 -6
  259. package/src/security/oauth2.ts +29 -4
  260. package/src/security/secret-allowlist.ts +46 -0
  261. package/src/skills/clawhub.ts +1 -1
  262. package/src/subagent/manager.ts +40 -8
  263. package/src/swarm/backend-claude-code.ts +64 -9
  264. package/src/swarm/worker-prompts.ts +2 -1
  265. package/src/tasks/SPEC.md +34 -28
  266. package/src/tasks/ephemeral-permissions.ts +16 -7
  267. package/src/tasks/task-compiler.ts +5 -4
  268. package/src/tasks/task-runner.ts +10 -5
  269. package/src/tasks/task-scheduler.ts +1 -1
  270. package/src/tasks/tool-sanitizer.ts +36 -0
  271. package/src/tools/assets/search.ts +4 -4
  272. package/src/tools/browser/api-map.ts +220 -0
  273. package/src/tools/browser/auto-navigate.ts +270 -0
  274. package/src/tools/browser/browser-execution.ts +2 -1
  275. package/src/tools/browser/browser-manager.ts +2 -2
  276. package/src/tools/browser/network-recorder.ts +5 -4
  277. package/src/tools/browser/x-auto-navigate.ts +207 -0
  278. package/src/tools/calls/call-end.ts +17 -67
  279. package/src/tools/calls/call-start.ts +24 -85
  280. package/src/tools/calls/call-status.ts +35 -51
  281. package/src/tools/claude-code/claude-code.ts +77 -11
  282. package/src/tools/contacts/contact-merge.ts +46 -78
  283. package/src/tools/contacts/contact-search.ts +35 -79
  284. package/src/tools/contacts/contact-upsert.ts +35 -108
  285. package/src/tools/credentials/vault.ts +20 -4
  286. package/src/tools/document/document-tool.ts +71 -144
  287. package/src/tools/executor.ts +129 -10
  288. package/src/tools/followups/followup_create.ts +46 -88
  289. package/src/tools/followups/followup_list.ts +34 -74
  290. package/src/tools/followups/followup_resolve.ts +31 -66
  291. package/src/tools/host-terminal/cli-discover.ts +2 -1
  292. package/src/tools/host-terminal/host-shell.ts +10 -0
  293. package/src/tools/memory/handlers.ts +5 -4
  294. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  295. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  296. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  297. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  298. package/src/tools/network/web-fetch.ts +18 -6
  299. package/src/tools/playbooks/index.ts +4 -5
  300. package/src/tools/playbooks/playbook-create.ts +3 -47
  301. package/src/tools/playbooks/playbook-delete.ts +1 -25
  302. package/src/tools/playbooks/playbook-list.ts +1 -28
  303. package/src/tools/playbooks/playbook-update.ts +3 -51
  304. package/src/tools/reminder/reminder.ts +5 -78
  305. package/src/tools/schedule/create.ts +69 -74
  306. package/src/tools/schedule/delete.ts +21 -47
  307. package/src/tools/schedule/list.ts +55 -74
  308. package/src/tools/schedule/update.ts +77 -84
  309. package/src/tools/subagent/abort.ts +29 -58
  310. package/src/tools/subagent/message.ts +30 -63
  311. package/src/tools/subagent/read.ts +53 -84
  312. package/src/tools/subagent/spawn.ts +43 -82
  313. package/src/tools/subagent/status.ts +42 -71
  314. package/src/tools/swarm/delegate.ts +2 -1
  315. package/src/tools/tasks/index.ts +8 -8
  316. package/src/tools/tasks/task-delete.ts +60 -88
  317. package/src/tools/tasks/task-list.ts +31 -52
  318. package/src/tools/tasks/task-run.ts +72 -108
  319. package/src/tools/tasks/task-save.ts +33 -65
  320. package/src/tools/tasks/work-item-enqueue.ts +183 -215
  321. package/src/tools/tasks/work-item-list.ts +33 -63
  322. package/src/tools/tasks/work-item-remove.ts +45 -97
  323. package/src/tools/tasks/work-item-update.ts +91 -163
  324. package/src/tools/terminal/backends/native.ts +3 -1
  325. package/src/tools/tool-manifest.ts +0 -62
  326. package/src/tools/types.ts +6 -0
  327. package/src/tools/ui-surface/definitions.ts +3 -1
  328. package/src/tools/watch/screen-watch.ts +3 -1
  329. package/src/tools/watcher/create.ts +52 -98
  330. package/src/tools/watcher/delete.ts +20 -46
  331. package/src/tools/watcher/digest.ts +36 -70
  332. package/src/tools/watcher/list.ts +49 -79
  333. package/src/tools/watcher/update.ts +45 -91
  334. package/src/twitter/client.ts +690 -0
  335. package/src/twitter/session.ts +91 -0
  336. package/src/usage/types.ts +0 -1
  337. package/src/util/truncate.ts +6 -0
  338. package/src/watcher/providers/slack.ts +2 -1
  339. package/src/watcher/watcher-store.ts +3 -2
  340. package/src/work-items/work-item-store.ts +27 -2
  341. package/src/workspace/commit-message-enrichment-service.ts +31 -7
  342. package/src/workspace/git-service.ts +87 -22
  343. package/src/workspace/provider-commit-message-generator.ts +242 -0
  344. package/src/workspace/turn-commit.ts +62 -3
  345. package/src/tools/contacts/index.ts +0 -4
  346. package/src/tools/document/index.ts +0 -5
  347. package/src/tools/followups/index.ts +0 -3
  348. package/src/tools/subagent/index.ts +0 -5
  349. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -0,0 +1,234 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { evaluateRequest, evaluateRequestWithApproval } from '../policy.js';
3
+ import type { CredentialInjectionTemplate } from '../../../credentials/policy-types.js';
4
+
5
+ function makeTemplate(overrides: Partial<CredentialInjectionTemplate> = {}): CredentialInjectionTemplate {
6
+ return {
7
+ hostPattern: '*.example.com',
8
+ injectionType: 'header',
9
+ headerName: 'Authorization',
10
+ valuePrefix: 'Bearer ',
11
+ ...overrides,
12
+ };
13
+ }
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // evaluateRequest
17
+ // ---------------------------------------------------------------------------
18
+
19
+ describe('evaluateRequest', () => {
20
+ test('returns unauthenticated when no credential IDs', () => {
21
+ const result = evaluateRequest('api.example.com', '/', [], new Map());
22
+ expect(result.kind).toBe('unauthenticated');
23
+ });
24
+
25
+ test('returns missing when credential has no templates', () => {
26
+ const result = evaluateRequest('api.example.com', '/', ['cred-1'], new Map());
27
+ expect(result.kind).toBe('missing');
28
+ });
29
+
30
+ test('returns missing when no template matches the host', () => {
31
+ const templates = new Map<string, CredentialInjectionTemplate[]>();
32
+ templates.set('cred-1', [makeTemplate({ hostPattern: '*.other.com' })]);
33
+ const result = evaluateRequest('api.example.com', '/', ['cred-1'], templates);
34
+ expect(result.kind).toBe('missing');
35
+ });
36
+
37
+ test('returns matched for exact host match', () => {
38
+ const tpl = makeTemplate({ hostPattern: 'api.example.com' });
39
+ const templates = new Map<string, CredentialInjectionTemplate[]>();
40
+ templates.set('cred-1', [tpl]);
41
+ const result = evaluateRequest('api.example.com', '/', ['cred-1'], templates);
42
+ expect(result.kind).toBe('matched');
43
+ if (result.kind === 'matched') {
44
+ expect(result.credentialId).toBe('cred-1');
45
+ expect(result.template).toBe(tpl);
46
+ }
47
+ });
48
+
49
+ test('returns matched for wildcard host match', () => {
50
+ const tpl = makeTemplate({ hostPattern: '*.example.com' });
51
+ const templates = new Map<string, CredentialInjectionTemplate[]>();
52
+ templates.set('cred-1', [tpl]);
53
+ const result = evaluateRequest('api.example.com', '/', ['cred-1'], templates);
54
+ expect(result.kind).toBe('matched');
55
+ if (result.kind === 'matched') {
56
+ expect(result.credentialId).toBe('cred-1');
57
+ }
58
+ });
59
+
60
+ test('prefers exact match over wildcard for same credential', () => {
61
+ const wildcard = makeTemplate({ hostPattern: '*.example.com', headerName: 'X-Wildcard' });
62
+ const exact = makeTemplate({ hostPattern: 'api.example.com', headerName: 'X-Exact' });
63
+ const templates = new Map<string, CredentialInjectionTemplate[]>();
64
+ templates.set('cred-1', [wildcard, exact]);
65
+ const result = evaluateRequest('api.example.com', '/', ['cred-1'], templates);
66
+ expect(result.kind).toBe('matched');
67
+ if (result.kind === 'matched') {
68
+ expect(result.template.headerName).toBe('X-Exact');
69
+ }
70
+ });
71
+
72
+ test('returns ambiguous for same-specificity tie within one credential', () => {
73
+ const tpl1 = makeTemplate({ hostPattern: '*.example.com', headerName: 'X-One' });
74
+ const tpl2 = makeTemplate({ hostPattern: '*.example.com', headerName: 'X-Two' });
75
+ const templates = new Map<string, CredentialInjectionTemplate[]>();
76
+ templates.set('cred-1', [tpl1, tpl2]);
77
+ const result = evaluateRequest('api.example.com', '/', ['cred-1'], templates);
78
+ expect(result.kind).toBe('ambiguous');
79
+ if (result.kind === 'ambiguous') {
80
+ expect(result.candidates).toHaveLength(2);
81
+ }
82
+ });
83
+
84
+ test('returns ambiguous for cross-credential match', () => {
85
+ const tpl1 = makeTemplate({ hostPattern: '*.example.com' });
86
+ const tpl2 = makeTemplate({ hostPattern: '*.example.com' });
87
+ const templates = new Map<string, CredentialInjectionTemplate[]>();
88
+ templates.set('cred-1', [tpl1]);
89
+ templates.set('cred-2', [tpl2]);
90
+ const result = evaluateRequest('api.example.com', '/', ['cred-1', 'cred-2'], templates);
91
+ expect(result.kind).toBe('ambiguous');
92
+ if (result.kind === 'ambiguous') {
93
+ expect(result.candidates).toHaveLength(2);
94
+ }
95
+ });
96
+
97
+ test('skips query-type injection templates', () => {
98
+ const tpl = makeTemplate({
99
+ hostPattern: '*.example.com',
100
+ injectionType: 'query',
101
+ queryParamName: 'api_key',
102
+ headerName: undefined,
103
+ });
104
+ const templates = new Map<string, CredentialInjectionTemplate[]>();
105
+ templates.set('cred-1', [tpl]);
106
+ const result = evaluateRequest('api.example.com', '/', ['cred-1'], templates);
107
+ expect(result.kind).toBe('missing');
108
+ });
109
+
110
+ test('wildcard with includeApexForWildcard matches bare domain', () => {
111
+ const tpl = makeTemplate({ hostPattern: '*.example.com' });
112
+ const templates = new Map<string, CredentialInjectionTemplate[]>();
113
+ templates.set('cred-1', [tpl]);
114
+ const result = evaluateRequest('example.com', '/', ['cred-1'], templates);
115
+ expect(result.kind).toBe('matched');
116
+ });
117
+ });
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // evaluateRequestWithApproval
121
+ // ---------------------------------------------------------------------------
122
+
123
+ describe('evaluateRequestWithApproval', () => {
124
+ test('passes through matched decisions', () => {
125
+ const tpl = makeTemplate({ hostPattern: 'api.example.com' });
126
+ const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
127
+ sessionTemplates.set('cred-1', [tpl]);
128
+ const result = evaluateRequestWithApproval(
129
+ 'api.example.com', null, '/',
130
+ ['cred-1'], sessionTemplates, [],
131
+ );
132
+ expect(result.kind).toBe('matched');
133
+ });
134
+
135
+ test('passes through ambiguous decisions', () => {
136
+ const tpl1 = makeTemplate({ hostPattern: '*.example.com', headerName: 'X-One' });
137
+ const tpl2 = makeTemplate({ hostPattern: '*.example.com', headerName: 'X-Two' });
138
+ const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
139
+ sessionTemplates.set('cred-1', [tpl1, tpl2]);
140
+ const result = evaluateRequestWithApproval(
141
+ 'api.example.com', null, '/',
142
+ ['cred-1'], sessionTemplates, [],
143
+ );
144
+ expect(result.kind).toBe('ambiguous');
145
+ });
146
+
147
+ test('returns ask_missing_credential when known templates match', () => {
148
+ const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
149
+ const allKnown = [makeTemplate({ hostPattern: '*.example.com' })];
150
+ const result = evaluateRequestWithApproval(
151
+ 'api.example.com', 443, '/api',
152
+ ['cred-1'], sessionTemplates, allKnown,
153
+ );
154
+ expect(result.kind).toBe('ask_missing_credential');
155
+ if (result.kind === 'ask_missing_credential') {
156
+ expect(result.target.hostname).toBe('api.example.com');
157
+ expect(result.target.port).toBe(443);
158
+ expect(result.target.path).toBe('/api');
159
+ expect(result.matchingPatterns).toContain('*.example.com');
160
+ }
161
+ });
162
+
163
+ test('deduplicates matching patterns', () => {
164
+ const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
165
+ const allKnown = [
166
+ makeTemplate({ hostPattern: '*.example.com', headerName: 'X-One' }),
167
+ makeTemplate({ hostPattern: '*.example.com', headerName: 'X-Two' }),
168
+ ];
169
+ const result = evaluateRequestWithApproval(
170
+ 'api.example.com', null, '/',
171
+ ['cred-1'], sessionTemplates, allKnown,
172
+ );
173
+ expect(result.kind).toBe('ask_missing_credential');
174
+ if (result.kind === 'ask_missing_credential') {
175
+ expect(result.matchingPatterns).toEqual(['*.example.com']);
176
+ }
177
+ });
178
+
179
+ test('returns ask_unauthenticated when no known templates match and no credentials', () => {
180
+ const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
181
+ const result = evaluateRequestWithApproval(
182
+ 'unknown.example.com', null, '/',
183
+ [], sessionTemplates, [],
184
+ );
185
+ expect(result.kind).toBe('ask_unauthenticated');
186
+ if (result.kind === 'ask_unauthenticated') {
187
+ expect(result.target.hostname).toBe('unknown.example.com');
188
+ expect(result.target.scheme).toBe('https');
189
+ }
190
+ });
191
+
192
+ test('returns ask_unauthenticated for unknown host even with known templates for other hosts', () => {
193
+ const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
194
+ const allKnown = [makeTemplate({ hostPattern: '*.other.com' })];
195
+ const result = evaluateRequestWithApproval(
196
+ 'unknown.example.com', null, '/',
197
+ [], sessionTemplates, allKnown,
198
+ );
199
+ expect(result.kind).toBe('ask_unauthenticated');
200
+ });
201
+
202
+ test('skips query templates when scanning allKnownTemplates', () => {
203
+ const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
204
+ const allKnown = [makeTemplate({
205
+ hostPattern: '*.example.com',
206
+ injectionType: 'query',
207
+ queryParamName: 'key',
208
+ headerName: undefined,
209
+ })];
210
+ const result = evaluateRequestWithApproval(
211
+ 'api.example.com', null, '/',
212
+ ['cred-1'], sessionTemplates, allKnown,
213
+ );
214
+ // Query templates are skipped, so no pattern matches → ask_unauthenticated
215
+ // But credentialIds is non-empty so base is 'missing' → then no known header templates → ask_unauthenticated
216
+ // Actually: credentialIds=['cred-1'] but sessionTemplates is empty → base='missing'
217
+ // allKnown only has query type → no header matches → falls through to ask_unauthenticated
218
+ // Wait: base is 'missing' (not unauthenticated), and uniquePatterns.length===0
219
+ // For 'missing' with no matching patterns, the function returns ask_unauthenticated
220
+ expect(result.kind).toBe('ask_unauthenticated');
221
+ });
222
+
223
+ test('uses provided scheme parameter', () => {
224
+ const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
225
+ const result = evaluateRequestWithApproval(
226
+ 'unknown.example.com', 80, '/',
227
+ [], sessionTemplates, [], 'http',
228
+ );
229
+ expect(result.kind).toBe('ask_unauthenticated');
230
+ if (result.kind === 'ask_unauthenticated') {
231
+ expect(result.target.scheme).toBe('http');
232
+ }
233
+ });
234
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { routeConnection } from '../router.js';
3
+ import type { CredentialInjectionTemplate } from '../../../credentials/policy-types.js';
4
+
5
+ function makeTemplate(overrides: Partial<CredentialInjectionTemplate> = {}): CredentialInjectionTemplate {
6
+ return {
7
+ hostPattern: '*.example.com',
8
+ injectionType: 'header',
9
+ headerName: 'Authorization',
10
+ valuePrefix: 'Bearer ',
11
+ ...overrides,
12
+ };
13
+ }
14
+
15
+ describe('routeConnection', () => {
16
+ test('tunnels when no credentials', () => {
17
+ const result = routeConnection('api.example.com', 443, [], new Map());
18
+ expect(result.action).toBe('tunnel');
19
+ expect(result.reason).toBe('tunnel:no_credentials');
20
+ });
21
+
22
+ test('tunnels when credentials exist but no template matches', () => {
23
+ const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
24
+ templates.set('cred-1', [makeTemplate({ hostPattern: '*.other.com' })]);
25
+ const result = routeConnection('api.example.com', 443, ['cred-1'], templates);
26
+ expect(result.action).toBe('tunnel');
27
+ expect(result.reason).toBe('tunnel:no_rewrite');
28
+ });
29
+
30
+ test('tunnels when credential has no templates in map', () => {
31
+ const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
32
+ const result = routeConnection('api.example.com', 443, ['cred-1'], templates);
33
+ expect(result.action).toBe('tunnel');
34
+ expect(result.reason).toBe('tunnel:no_rewrite');
35
+ });
36
+
37
+ test('MITMs when wildcard template matches', () => {
38
+ const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
39
+ templates.set('cred-1', [makeTemplate({ hostPattern: '*.example.com' })]);
40
+ const result = routeConnection('api.example.com', 443, ['cred-1'], templates);
41
+ expect(result.action).toBe('mitm');
42
+ expect(result.reason).toBe('mitm:credential_injection');
43
+ });
44
+
45
+ test('MITMs when exact template matches', () => {
46
+ const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
47
+ templates.set('cred-1', [makeTemplate({ hostPattern: 'api.example.com' })]);
48
+ const result = routeConnection('api.example.com', 443, ['cred-1'], templates);
49
+ expect(result.action).toBe('mitm');
50
+ expect(result.reason).toBe('mitm:credential_injection');
51
+ });
52
+
53
+ test('MITMs when any credential matches (first wins)', () => {
54
+ const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
55
+ templates.set('cred-1', [makeTemplate({ hostPattern: '*.other.com' })]);
56
+ templates.set('cred-2', [makeTemplate({ hostPattern: '*.example.com' })]);
57
+ const result = routeConnection('api.example.com', 443, ['cred-1', 'cred-2'], templates);
58
+ expect(result.action).toBe('mitm');
59
+ expect(result.reason).toBe('mitm:credential_injection');
60
+ });
61
+
62
+ test('wildcard matches bare apex domain with includeApexForWildcard', () => {
63
+ const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
64
+ templates.set('cred-1', [makeTemplate({ hostPattern: '*.example.com' })]);
65
+ const result = routeConnection('example.com', 443, ['cred-1'], templates);
66
+ expect(result.action).toBe('mitm');
67
+ expect(result.reason).toBe('mitm:credential_injection');
68
+ });
69
+
70
+ test('case-insensitive matching', () => {
71
+ const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
72
+ templates.set('cred-1', [makeTemplate({ hostPattern: '*.EXAMPLE.COM' })]);
73
+ const result = routeConnection('API.example.com', 443, ['cred-1'], templates);
74
+ expect(result.action).toBe('mitm');
75
+ });
76
+ });
@@ -298,12 +298,21 @@ function extractFirstMatch(text: string, regex: RegExp, captureGroup = 1): strin
298
298
  }
299
299
 
300
300
  function extractHtmlMetadata(html: string): { title?: string; description?: string } {
301
- const title = extractFirstMatch(html, /<title[^>]*>([\s\S]*?)<\/title>/i);
301
+ // Only search the <head> section (or first 50KB) to avoid catastrophic
302
+ // regex backtracking on large HTML documents.
303
+ // Strip <script> blocks first so that a literal "</head>" inside a script
304
+ // doesn't cause a false match that truncates the search region prematurely.
305
+ const candidate = html.slice(0, 200_000);
306
+ const stripped = candidate.replace(/<script[\s>][\s\S]*?<\/script>/gi, '');
307
+ const headEnd = stripped.search(/<\/head[\s>]/i);
308
+ const searchRegion = headEnd > 0 ? stripped.slice(0, headEnd + 10) : stripped.slice(0, 50_000);
309
+
310
+ const title = extractFirstMatch(searchRegion, /<title[^>]*>([\s\S]*?)<\/title>/i);
302
311
  const description =
303
- extractFirstMatch(html, /<meta\s+[^>]*name=(['"])description\1[^>]*content=(['"])([\s\S]*?)\2[^>]*>/i, 3)
304
- ?? extractFirstMatch(html, /<meta\s+[^>]*content=(['"])([\s\S]*?)\1[^>]*name=(['"])description\3[^>]*>/i, 2)
305
- ?? extractFirstMatch(html, /<meta\s+[^>]*property=(['"])og:description\1[^>]*content=(['"])([\s\S]*?)\2[^>]*>/i, 3)
306
- ?? extractFirstMatch(html, /<meta\s+[^>]*content=(['"])([\s\S]*?)\1[^>]*property=(['"])og:description\3[^>]*>/i, 2);
312
+ extractFirstMatch(searchRegion, /<meta\s+[^>]*name=(['"])description\1[^>]*content=(['"])([\s\S]*?)\2[^>]*>/i, 3)
313
+ ?? extractFirstMatch(searchRegion, /<meta\s+[^>]*content=(['"])([\s\S]*?)\1[^>]*name=(['"])description\3[^>]*>/i, 2)
314
+ ?? extractFirstMatch(searchRegion, /<meta\s+[^>]*property=(['"])og:description\1[^>]*content=(['"])([\s\S]*?)\2[^>]*>/i, 3)
315
+ ?? extractFirstMatch(searchRegion, /<meta\s+[^>]*content=(['"])([\s\S]*?)\1[^>]*property=(['"])og:description\3[^>]*>/i, 2);
307
316
 
308
317
  return { title, description };
309
318
  }
@@ -437,7 +446,10 @@ export async function executeWebFetch(
437
446
  const safeRequestedUrl = sanitizeUrlForOutput(parsedUrl);
438
447
 
439
448
  const controller = new AbortController();
440
- const timeoutHandle = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
449
+ const timeoutHandle = setTimeout(() => {
450
+ log.warn({ url: safeRequestedUrl, timeoutSeconds }, 'Web fetch timeout fired, aborting');
451
+ controller.abort();
452
+ }, timeoutSeconds * 1000);
441
453
 
442
454
  try {
443
455
  log.debug({ url: safeRequestedUrl, timeoutSeconds, maxChars, startIndex, rawMode }, 'Fetching webpage');
@@ -1,5 +1,4 @@
1
- // Barrel export importing this module triggers registration of all playbook tools.
2
- import './playbook-create.js';
3
- import './playbook-list.js';
4
- import './playbook-update.js';
5
- import './playbook-delete.js';
1
+ export { executePlaybookCreate } from './playbook-create.js';
2
+ export { executePlaybookList } from './playbook-list.js';
3
+ export { executePlaybookUpdate } from './playbook-update.js';
4
+ export { executePlaybookDelete } from './playbook-delete.js';
@@ -1,17 +1,16 @@
1
1
  import { and, eq } from 'drizzle-orm';
2
2
  import { v4 as uuid } from 'uuid';
3
- import { RiskLevel } from '../../permissions/types.js';
4
3
  import type { ToolContext, ToolExecutionResult } from '../types.js';
5
- import { registerTool } from '../registry.js';
6
4
  import { getDb } from '../../memory/db.js';
7
5
  import { computeMemoryFingerprint } from '../../memory/fingerprint.js';
8
6
  import { memoryItems } from '../../memory/schema.js';
9
7
  import { enqueueMemoryJob } from '../../memory/jobs-store.js';
10
8
  import type { Playbook, PlaybookAutonomyLevel } from '../../playbooks/types.js';
9
+ import { truncate } from '../../util/truncate.js';
11
10
 
12
11
  const VALID_AUTONOMY_LEVELS = new Set<string>(['auto', 'draft', 'notify']);
13
12
 
14
- async function execute(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
13
+ export async function executePlaybookCreate(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
15
14
  const trigger = input.trigger as string;
16
15
  const action = input.action as string;
17
16
 
@@ -32,7 +31,7 @@ async function execute(input: Record<string, unknown>, context: ToolContext): Pr
32
31
 
33
32
  const playbook: Playbook = { trigger, channel, category, action, autonomyLevel, priority };
34
33
  const statement = JSON.stringify(playbook);
35
- const subject = `Playbook: ${trigger}`.slice(0, 80);
34
+ const subject = truncate(`Playbook: ${trigger}`, 80, '');
36
35
  const scopeId = context.memoryScopeId ?? 'default';
37
36
 
38
37
  const fingerprint = computeMemoryFingerprint(scopeId, 'playbook', subject, statement);
@@ -95,46 +94,3 @@ async function execute(input: Record<string, unknown>, context: ToolContext): Pr
95
94
  return { content: `Error creating playbook: ${msg}`, isError: true };
96
95
  }
97
96
  }
98
-
99
- registerTool({
100
- name: 'playbook_create',
101
- description: 'Create an action playbook — a trigger→action rule that tells the assistant how to handle incoming messages matching a pattern',
102
- category: 'playbook',
103
- defaultRiskLevel: RiskLevel.Low,
104
- getDefinition: () => ({
105
- name: 'playbook_create',
106
- description: 'Create an action playbook — a trigger→action rule that tells the assistant how to handle incoming messages matching a pattern',
107
- input_schema: {
108
- type: 'object',
109
- properties: {
110
- trigger: {
111
- type: 'string',
112
- description: 'Pattern or description that triggers this playbook (e.g. "meeting request", "from:ceo@*", "newsletter")',
113
- },
114
- action: {
115
- type: 'string',
116
- description: 'What to do when the trigger matches — natural language action description (e.g. "check calendar, propose 3 times")',
117
- },
118
- channel: {
119
- type: 'string',
120
- description: 'Channel this rule applies to (e.g. "email", "slack"), or "*" for all channels. Defaults to "*".',
121
- },
122
- category: {
123
- type: 'string',
124
- description: 'Free-form category for grouping (e.g. "scheduling", "triage"). Defaults to "general".',
125
- },
126
- autonomy_level: {
127
- type: 'string',
128
- enum: ['auto', 'draft', 'notify'],
129
- description: 'How much autonomy: "auto" = execute automatically, "draft" = draft for review, "notify" = notify only. Defaults to "draft".',
130
- },
131
- priority: {
132
- type: 'number',
133
- description: 'Relative priority — higher numbers take precedence. Defaults to 0.',
134
- },
135
- },
136
- required: ['trigger', 'action'],
137
- },
138
- }),
139
- execute,
140
- });
@@ -1,12 +1,10 @@
1
1
  import { and, eq } from 'drizzle-orm';
2
- import { RiskLevel } from '../../permissions/types.js';
3
2
  import type { ToolContext, ToolExecutionResult } from '../types.js';
4
- import { registerTool } from '../registry.js';
5
3
  import { getDb } from '../../memory/db.js';
6
4
  import { memoryItems } from '../../memory/schema.js';
7
5
  import { parsePlaybookStatement } from '../../playbooks/types.js';
8
6
 
9
- async function execute(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
7
+ export async function executePlaybookDelete(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
10
8
  const playbookId = input.playbook_id as string;
11
9
  if (!playbookId || typeof playbookId !== 'string') {
12
10
  return { content: 'Error: playbook_id is required and must be a string', isError: true };
@@ -52,25 +50,3 @@ async function execute(input: Record<string, unknown>, context: ToolContext): Pr
52
50
  return { content: `Error deleting playbook: ${msg}`, isError: true };
53
51
  }
54
52
  }
55
-
56
- registerTool({
57
- name: 'playbook_delete',
58
- description: 'Delete an action playbook rule',
59
- category: 'playbook',
60
- defaultRiskLevel: RiskLevel.Low,
61
- getDefinition: () => ({
62
- name: 'playbook_delete',
63
- description: 'Delete an action playbook rule',
64
- input_schema: {
65
- type: 'object',
66
- properties: {
67
- playbook_id: {
68
- type: 'string',
69
- description: 'ID of the playbook to delete (from playbook_list results)',
70
- },
71
- },
72
- required: ['playbook_id'],
73
- },
74
- }),
75
- execute,
76
- });
@@ -1,12 +1,10 @@
1
1
  import { and, desc, eq, isNull } from 'drizzle-orm';
2
- import { RiskLevel } from '../../permissions/types.js';
3
2
  import type { ToolContext, ToolExecutionResult } from '../types.js';
4
- import { registerTool } from '../registry.js';
5
3
  import { getDb } from '../../memory/db.js';
6
4
  import { memoryItems } from '../../memory/schema.js';
7
5
  import { parsePlaybookStatement } from '../../playbooks/types.js';
8
6
 
9
- async function execute(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
7
+ export async function executePlaybookList(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
10
8
  const scopeId = context.memoryScopeId ?? 'default';
11
9
  const channelFilter = typeof input.channel === 'string' ? input.channel : null;
12
10
  const categoryFilter = typeof input.category === 'string' ? input.category : null;
@@ -74,28 +72,3 @@ async function execute(input: Record<string, unknown>, context: ToolContext): Pr
74
72
  return { content: `Error listing playbooks: ${msg}`, isError: true };
75
73
  }
76
74
  }
77
-
78
- registerTool({
79
- name: 'playbook_list',
80
- description: 'List action playbooks, optionally filtered by channel or category',
81
- category: 'playbook',
82
- defaultRiskLevel: RiskLevel.Low,
83
- getDefinition: () => ({
84
- name: 'playbook_list',
85
- description: 'List action playbooks, optionally filtered by channel or category',
86
- input_schema: {
87
- type: 'object',
88
- properties: {
89
- channel: {
90
- type: 'string',
91
- description: 'Filter by channel (e.g. "email", "slack"). Omit to show all.',
92
- },
93
- category: {
94
- type: 'string',
95
- description: 'Filter by category (e.g. "scheduling", "triage"). Omit to show all.',
96
- },
97
- },
98
- },
99
- }),
100
- execute,
101
- });
@@ -1,17 +1,16 @@
1
1
  import { and, eq } from 'drizzle-orm';
2
- import { RiskLevel } from '../../permissions/types.js';
3
2
  import type { ToolContext, ToolExecutionResult } from '../types.js';
4
- import { registerTool } from '../registry.js';
5
3
  import { getDb } from '../../memory/db.js';
6
4
  import { computeMemoryFingerprint } from '../../memory/fingerprint.js';
7
5
  import { memoryItems } from '../../memory/schema.js';
8
6
  import { enqueueMemoryJob } from '../../memory/jobs-store.js';
9
7
  import { parsePlaybookStatement } from '../../playbooks/types.js';
10
8
  import type { Playbook, PlaybookAutonomyLevel } from '../../playbooks/types.js';
9
+ import { truncate } from '../../util/truncate.js';
11
10
 
12
11
  const VALID_AUTONOMY_LEVELS = new Set<string>(['auto', 'draft', 'notify']);
13
12
 
14
- async function execute(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
13
+ export async function executePlaybookUpdate(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
15
14
  const playbookId = input.playbook_id as string;
16
15
  if (!playbookId || typeof playbookId !== 'string') {
17
16
  return { content: 'Error: playbook_id is required and must be a string', isError: true };
@@ -55,7 +54,7 @@ async function execute(input: Record<string, unknown>, context: ToolContext): Pr
55
54
  };
56
55
 
57
56
  const statement = JSON.stringify(updated);
58
- const subject = `Playbook: ${updated.trigger}`.slice(0, 80);
57
+ const subject = truncate(`Playbook: ${updated.trigger}`, 80, '');
59
58
  const now = Date.now();
60
59
 
61
60
  const fingerprint = computeMemoryFingerprint(scopeId, 'playbook', subject, statement);
@@ -110,50 +109,3 @@ async function execute(input: Record<string, unknown>, context: ToolContext): Pr
110
109
  return { content: `Error updating playbook: ${msg}`, isError: true };
111
110
  }
112
111
  }
113
-
114
- registerTool({
115
- name: 'playbook_update',
116
- description: 'Update an existing action playbook rule',
117
- category: 'playbook',
118
- defaultRiskLevel: RiskLevel.Low,
119
- getDefinition: () => ({
120
- name: 'playbook_update',
121
- description: 'Update an existing action playbook rule',
122
- input_schema: {
123
- type: 'object',
124
- properties: {
125
- playbook_id: {
126
- type: 'string',
127
- description: 'ID of the playbook to update (from playbook_list results)',
128
- },
129
- trigger: {
130
- type: 'string',
131
- description: 'Updated trigger pattern',
132
- },
133
- action: {
134
- type: 'string',
135
- description: 'Updated action description',
136
- },
137
- channel: {
138
- type: 'string',
139
- description: 'Updated channel ("*" for all)',
140
- },
141
- category: {
142
- type: 'string',
143
- description: 'Updated category',
144
- },
145
- autonomy_level: {
146
- type: 'string',
147
- enum: ['auto', 'draft', 'notify'],
148
- description: 'Updated autonomy level',
149
- },
150
- priority: {
151
- type: 'number',
152
- description: 'Updated priority',
153
- },
154
- },
155
- required: ['playbook_id'],
156
- },
157
- }),
158
- execute,
159
- });