@desplega.ai/agent-swarm 1.150.0 → 1.151.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 (293) hide show
  1. package/README.md +11 -0
  2. package/dist/{acp-adapter-d5mzggyk.js → acp-adapter-ex8hzjfc.js} +5 -5
  3. package/dist/{actions-wn6ew0p7.js → actions-2bgpyhww.js} +38 -11
  4. package/dist/{anthropic-messages-ec35ktvx.js → anthropic-messages-2y8nktar.js} +16 -16
  5. package/dist/{app-dty7p9hg.js → app-1706zhr1.js} +7 -7
  6. package/dist/{assistant-q4vhgjtx.js → assistant-bavke5p7.js} +41 -16
  7. package/dist/{azure-openai-responses-g85jv7m8.js → azure-openai-responses-f38x2avq.js} +13 -13
  8. package/dist/{boot-reembed-92292pgf.js → boot-reembed-3cmrrmpa.js} +8 -8
  9. package/dist/{boot-reembed-n15q6s9x.js → boot-reembed-nafcmmmc.js} +13 -15
  10. package/dist/{boot-scrub-logs-44pc955m.js → boot-scrub-logs-cd6a2sq0.js} +5 -5
  11. package/dist/{claude-adapter-cjegjbje.js → claude-adapter-wfp2gzvz.js} +12 -13
  12. package/dist/{claude-managed-adapter-p3ts5m9m.js → claude-managed-adapter-dsmkcrjy.js} +3 -3
  13. package/dist/{claude-sdk-session-7ywbn12v.js → claude-sdk-session-bht9jjak.js} +12 -13
  14. package/dist/{cli-ceb5y160.js → cli-0qb1z6s2.js} +1 -1
  15. package/dist/{cli-ryz8q8p1.js → cli-2tbvf1z1.js} +40 -40
  16. package/dist/{cli-zthatyrx.js → cli-3v9xhjcn.js} +5 -5
  17. package/dist/{cli-mzb0n642.js → cli-4wppfkcm.js} +350 -20
  18. package/dist/{cli-3ckzxdt1.js → cli-5jd2z7av.js} +90 -81
  19. package/dist/{cli-3mh119qv.js → cli-5tpgq60f.js} +9726 -693
  20. package/dist/{cli-ck1vqzdj.js → cli-7tgcstn2.js} +9 -4
  21. package/dist/{cli-ttje5a4z.js → cli-9004vfdf.js} +105 -56
  22. package/dist/{cli-asvrh7kj.js → cli-9xvdm3bx.js} +1 -1
  23. package/dist/{cli-9gqddmxs.js → cli-a1z3h107.js} +8 -8
  24. package/dist/{cli-72xdszyx.js → cli-czjtgfys.js} +2 -2
  25. package/dist/{cli-5my3bjsd.js → cli-d3p82erb.js} +7 -7
  26. package/dist/{cli-kyysm7m3.js → cli-f1wky6zp.js} +1 -1
  27. package/dist/{cli-a45d5v9b.js → cli-fm4ytsdk.js} +2 -2
  28. package/dist/{cli-111ey3ra.js → cli-frcta2w4.js} +5 -5
  29. package/dist/{cli-h505yhex.js → cli-g69fdc9g.js} +2 -2
  30. package/dist/{cli-cmp8g3sw.js → cli-gbfwcd3k.js} +44 -9
  31. package/dist/{cli-hbagregr.js → cli-gjyvhmmh.js} +88 -13
  32. package/dist/{cli-hmyynq9b.js → cli-ht5rs63n.js} +1 -1
  33. package/dist/{cli-dyx0237s.js → cli-j4nv2wqy.js} +2 -4
  34. package/dist/{cli-vr6qj104.js → cli-j9xczhaq.js} +1 -1
  35. package/dist/cli-pf9q3696.js +172 -0
  36. package/dist/{cli-7fesa1ky.js → cli-pgj4a5fb.js} +1 -1
  37. package/dist/{cli-7kwmt3r5.js → cli-ppm4975n.js} +9 -3
  38. package/dist/{cli-8chp8xab.js → cli-q88st9aj.js} +2 -2
  39. package/dist/{cli-kt7wevqh.js → cli-r2q6pevn.js} +132 -46
  40. package/dist/{cli-07jd68a1.js → cli-re8z5p8p.js} +1 -1
  41. package/dist/{cli-qp69dd5d.js → cli-s14agm5s.js} +105 -12
  42. package/dist/{cli-zd21jrrc.js → cli-sjaae6ff.js} +9872 -424
  43. package/dist/{cli-taym3dmg.js → cli-vwm416m6.js} +13 -13
  44. package/dist/{cli-1mx65de9.js → cli-wbyk465z.js} +1 -1
  45. package/dist/{cli-n75jt793.js → cli-z61ykvam.js} +6 -5
  46. package/dist/{cli-gm0fhz13.js → cli-zkxymas5.js} +2 -1
  47. package/dist/cli.js +22 -21
  48. package/dist/{codex-adapter-rn3aq7jm.js → codex-adapter-jjjk8w4m.js} +8 -9
  49. package/dist/{codex-hook-gthcmm0g.js → codex-hook-8jexc34x.js} +2 -2
  50. package/dist/{codex-session-runner-bge0cbsw.js → codex-session-runner-nd6ayxkx.js} +8 -9
  51. package/dist/{commands-26phw0pc.js → commands-9v4psyq1.js} +5 -5
  52. package/dist/{db-jxjx6s1b.js → db-wnt58tpe.js} +5 -5
  53. package/dist/dispatcher-05jh1aqk.js +74 -0
  54. package/dist/{google-generative-ai-tsskx7te.js → google-generative-ai-ndjmj5fa.js} +7 -7
  55. package/dist/{google-vertex-pkhpkbq9.js → google-vertex-fr665tqs.js} +10 -10
  56. package/dist/handlers-tw90pkh5.js +65 -0
  57. package/dist/{hook-t6fqsggw.js → hook-123e2scb.js} +9 -10
  58. package/dist/{hook-jw0ztn96.js → hook-sxe3r349.js} +9 -10
  59. package/dist/{http-hyc7s57s.js → http-ecsar8ez.js} +1431 -579
  60. package/dist/{index-sfhaq59d.js → index-16js521z.js} +119 -101
  61. package/dist/{index-wtwpthe1.js → index-hw26vj40.js} +25 -19
  62. package/dist/{index-bcyx5djw.js → index-nnrbpcjp.js} +25 -17
  63. package/dist/index-ymn7em0p.js +25 -0
  64. package/dist/{index-1d9r6a74.js → index-zj518gf9.js} +26 -20
  65. package/dist/{keepalive-fbsak4ya.js → keepalive-asqtcm4y.js} +7 -7
  66. package/dist/{lead-rdzdm627.js → lead-xbfr7fvt.js} +41 -49
  67. package/dist/{maintenance-25z1c1vf.js → maintenance-qma55qvw.js} +8 -8
  68. package/dist/{node-209sxmg3.js → node-x1g4v0pq.js} +9 -9
  69. package/dist/{oauth-refresh-sweep-j3d7gtse.js → oauth-refresh-sweep-3s04c6qw.js} +6 -6
  70. package/dist/{onboard-dkcwrc75.js → onboard-drskd386.js} +13 -4
  71. package/dist/{openai-codex-responses-pm5gnyfa.js → openai-codex-responses-wy6a43wd.js} +11 -11
  72. package/dist/{openai-responses-vwvk1taj.js → openai-responses-6xwf7bzg.js} +13 -13
  73. package/dist/{opencode-adapter-dce4wskd.js → opencode-adapter-28axqa09.js} +4 -4
  74. package/dist/{otel-impl-8rgyy3j8.js → otel-impl-943c96er.js} +1 -1
  75. package/dist/{pi-mono-adapter-4s2bbkgq.js → pi-mono-adapter-jcqt71cg.js} +14 -16
  76. package/dist/{pricing-refresh-9cek71nq.js → pricing-refresh-mx51az05.js} +7 -7
  77. package/dist/{rbac-roles-z98s2ypy.js → rbac-roles-78baecqd.js} +13 -9
  78. package/dist/{rbac-roles-vk8mqp9v.js → rbac-roles-wcmrg6sv.js} +7 -7
  79. package/dist/{render-v2-3j26g0qh.js → render-v2-8zjntgf5.js} +7 -7
  80. package/dist/{seed-pricing-7rxhcnzm.js → seed-pricing-jqjs6cws.js} +6 -6
  81. package/dist/{setup-e2t7vz0c.js → setup-4pks0vfk.js} +2 -2
  82. package/dist/{work-objects-8d3nd3x2.js → work-objects-wangt792.js} +38 -14
  83. package/dist/{worker-0x9fn7nq.js → worker-p2mr422x.js} +41 -49
  84. package/openapi.json +1351 -265
  85. package/package.json +2 -1
  86. package/src/agentmail/handlers.ts +95 -71
  87. package/src/be/db-client.ts +5 -0
  88. package/src/be/db.ts +11 -3
  89. package/src/be/extensions/db.ts +461 -0
  90. package/src/be/extensions/validate.ts +107 -0
  91. package/src/be/memory/boot-reembed.ts +17 -12
  92. package/src/be/memory/index.ts +12 -0
  93. package/src/be/memory/providers/openai-embedding.ts +4 -0
  94. package/src/be/memory/types.ts +2 -0
  95. package/src/be/migrations/155_extensions.sql +66 -0
  96. package/src/be/migrations/156_inbox_item_state_read_at.sql +7 -0
  97. package/src/be/rbac-roles.ts +3 -2
  98. package/src/be/scripts/typecheck.ts +52 -38
  99. package/src/be/seed-skills/index.ts +7 -0
  100. package/src/be/swarm-config-guard.ts +3 -1
  101. package/src/commands/onboard/compose-generator.ts +14 -2
  102. package/src/commands/onboard/env-generator.ts +7 -0
  103. package/src/commands/runner.ts +1 -1
  104. package/src/extensions/apply-task-create.ts +135 -0
  105. package/src/extensions/bundle-path.ts +10 -0
  106. package/src/extensions/contract-runtime.ts +7 -0
  107. package/src/extensions/contract-types.generated.ts +4 -0
  108. package/src/extensions/contract.ts +270 -0
  109. package/src/extensions/ctx.ts +186 -0
  110. package/src/extensions/dispatcher.ts +479 -0
  111. package/src/extensions/identity.ts +44 -0
  112. package/src/extensions/lifecycle.ts +281 -0
  113. package/src/extensions/loader.ts +207 -0
  114. package/src/extensions/post-bridge.ts +161 -0
  115. package/src/github/handlers.ts +185 -149
  116. package/src/gitlab/handlers.ts +97 -81
  117. package/src/heartbeat/heartbeat.ts +152 -83
  118. package/src/http/agents.ts +1 -1
  119. package/src/http/all-routes.ts +1 -0
  120. package/src/http/apps.ts +14 -10
  121. package/src/http/core.ts +34 -0
  122. package/src/http/extensions.ts +547 -0
  123. package/src/http/inbox-state.ts +2 -0
  124. package/src/http/index.ts +25 -0
  125. package/src/http/mcp-bridge.ts +96 -62
  126. package/src/http/mcp.ts +1 -1
  127. package/src/http/status.ts +6 -4
  128. package/src/http/tasks.ts +51 -35
  129. package/src/integrations/kapso/inbound.ts +15 -11
  130. package/src/jira/sync.ts +26 -18
  131. package/src/linear/sync.ts +26 -18
  132. package/src/prompts/session-templates.ts +5 -4
  133. package/src/rbac/admission.ts +1 -1
  134. package/src/rbac/elevated-agents.ts +24 -0
  135. package/src/rbac/legacy-policy.ts +35 -6
  136. package/src/rbac/permissions.ts +8 -0
  137. package/src/scheduler/deferred-task-waits.ts +133 -51
  138. package/src/scheduler/schedule-task.ts +50 -24
  139. package/src/scheduler/scheduler.ts +4 -2
  140. package/src/scripts-runtime/executors/native.ts +5 -1
  141. package/src/scripts-runtime/executors/types.ts +5 -0
  142. package/src/scripts-runtime/import-allowlist.ts +79 -19
  143. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  144. package/src/scripts-runtime/swarm-config.ts +8 -0
  145. package/src/scripts-runtime/swarm-sdk.ts +3 -0
  146. package/src/scripts-runtime/types/stdlib.d.ts +9 -0
  147. package/src/scripts-runtime/types/swarm-extension.d.ts +562 -0
  148. package/src/scripts-runtime/types/swarm-sdk.d.ts +9 -0
  149. package/src/server.ts +6 -0
  150. package/src/slack/HEURISTICS.md +1 -1
  151. package/src/slack/actions.ts +21 -17
  152. package/src/slack/blocks.ts +1 -1
  153. package/src/slack/handlers.ts +98 -5
  154. package/src/slack/inbound-files.ts +6 -2
  155. package/src/slack/render-v2.ts +171 -35
  156. package/src/slack/router.ts +13 -3
  157. package/src/slack/steering.ts +3 -3
  158. package/src/tasks/errors.ts +9 -0
  159. package/src/tasks/sibling-awareness.ts +35 -1
  160. package/src/tasks/worker-follow-up.ts +64 -12
  161. package/src/tests/base-prompt.test.ts +1 -0
  162. package/src/tests/deferred-task-wake.test.ts +161 -9
  163. package/src/tests/docs-index-footprint.test.js +3 -3
  164. package/src/tests/env-flag.test.ts +1 -0
  165. package/src/tests/extensions-db.test.ts +216 -0
  166. package/src/tests/extensions-dispatcher.test.ts +275 -0
  167. package/src/tests/extensions-example-heartbeat-remediate.test.ts +69 -0
  168. package/src/tests/extensions-example-mcp-install.test.ts +63 -0
  169. package/src/tests/extensions-example-no-exclamation-marks.test.ts +72 -0
  170. package/src/tests/extensions-example-rewrite-and-suppress.test.ts +70 -0
  171. package/src/tests/extensions-example-route-slack-channel.test.ts +217 -0
  172. package/src/tests/extensions-heartbeat.test.ts +291 -0
  173. package/src/tests/extensions-http.test.ts +431 -0
  174. package/src/tests/extensions-lead-equivalence.test.ts +49 -0
  175. package/src/tests/extensions-lifecycle.test.ts +209 -0
  176. package/src/tests/extensions-loader.test.ts +95 -0
  177. package/src/tests/extensions-mcp-tools.test.ts +245 -0
  178. package/src/tests/extensions-pre-task.test.ts +487 -0
  179. package/src/tests/extensions-run-context.test.ts +41 -0
  180. package/src/tests/extensions-tool-call.test.ts +288 -0
  181. package/src/tests/extensions-validate.test.ts +196 -0
  182. package/src/tests/fixtures/extensions/bad-import/hooks.ts +8 -0
  183. package/src/tests/fixtures/extensions/bad-import/manifest.json +9 -0
  184. package/src/tests/fixtures/extensions/bad-return-shape/hooks.ts +10 -0
  185. package/src/tests/fixtures/extensions/bad-return-shape/manifest.json +9 -0
  186. package/src/tests/fixtures/extensions/block-tasks-from-source/hooks.ts +22 -0
  187. package/src/tests/fixtures/extensions/block-tasks-from-source/manifest.json +9 -0
  188. package/src/tests/fixtures/extensions/ignore-channel/hooks.ts +22 -0
  189. package/src/tests/fixtures/extensions/ignore-channel/manifest.json +9 -0
  190. package/src/tests/fixtures/extensions/load.ts +10 -0
  191. package/src/tests/fixtures/extensions/minimal/hooks.ts +7 -0
  192. package/src/tests/fixtures/extensions/minimal/manifest.json +9 -0
  193. package/src/tests/fixtures/extensions/never-fail-long-tasks/hooks.ts +11 -0
  194. package/src/tests/fixtures/extensions/never-fail-long-tasks/manifest.json +9 -0
  195. package/src/tests/fixtures/extensions/no-exclamation-marks/hooks.ts +23 -0
  196. package/src/tests/fixtures/extensions/no-exclamation-marks/manifest.json +9 -0
  197. package/src/tests/fixtures/extensions/post-logger/hooks.ts +9 -0
  198. package/src/tests/fixtures/extensions/post-logger/manifest.json +9 -0
  199. package/src/tests/fixtures/extensions/priority-a/hooks.ts +7 -0
  200. package/src/tests/fixtures/extensions/priority-a/manifest.json +9 -0
  201. package/src/tests/fixtures/extensions/priority-b/hooks.ts +7 -0
  202. package/src/tests/fixtures/extensions/priority-b/manifest.json +9 -0
  203. package/src/tests/fixtures/extensions/record-only-on-tag/hooks.ts +16 -0
  204. package/src/tests/fixtures/extensions/record-only-on-tag/manifest.json +9 -0
  205. package/src/tests/fixtures/extensions/reserved-assets/hooks.ts +5 -0
  206. package/src/tests/fixtures/extensions/reserved-assets/manifest.json +10 -0
  207. package/src/tests/fixtures/extensions/rewrite-progress-prefix/hooks.ts +22 -0
  208. package/src/tests/fixtures/extensions/rewrite-progress-prefix/manifest.json +9 -0
  209. package/src/tests/fixtures/extensions/rewrite-task-priority/hooks.ts +7 -0
  210. package/src/tests/fixtures/extensions/rewrite-task-priority/manifest.json +9 -0
  211. package/src/tests/fixtures/extensions/route-channel-to-agent/hooks.ts +26 -0
  212. package/src/tests/fixtures/extensions/route-channel-to-agent/manifest.json +9 -0
  213. package/src/tests/fixtures/extensions/slow/hooks.ts +10 -0
  214. package/src/tests/fixtures/extensions/slow/manifest.json +9 -0
  215. package/src/tests/fixtures/extensions/suppress-lead-follow-up/hooks.ts +12 -0
  216. package/src/tests/fixtures/extensions/suppress-lead-follow-up/manifest.json +9 -0
  217. package/src/tests/fixtures/extensions/throws/hooks.ts +12 -0
  218. package/src/tests/fixtures/extensions/throws/manifest.json +9 -0
  219. package/src/tests/fixtures/extensions/worker-runtime/hooks.ts +5 -0
  220. package/src/tests/fixtures/extensions/worker-runtime/manifest.json +9 -0
  221. package/src/tests/memory-edit-authorization.test.ts +32 -0
  222. package/src/tests/multi-runtime-enabled-stats.test.ts +2 -2
  223. package/src/tests/multi-runtime-registration.test.ts +14 -14
  224. package/src/tests/onboard-compose.test.ts +44 -0
  225. package/src/tests/preload.ts +5 -0
  226. package/src/tests/rbac-admission.test.ts +18 -3
  227. package/src/tests/rbac-engine.test.ts +39 -0
  228. package/src/tests/realtime-room-auth.test.ts +1 -1
  229. package/src/tests/reload-config.test.ts +44 -0
  230. package/src/tests/routing-affinity-inventory.test.ts +1 -1
  231. package/src/tests/script-connections.test.ts +2 -0
  232. package/src/tests/scripts-boot-reembed.test.ts +4 -0
  233. package/src/tests/scripts-embeddings.test.ts +4 -0
  234. package/src/tests/scripts-import-allowlist.test.ts +44 -1
  235. package/src/tests/scripts-mcp-e2e.test.ts +80 -0
  236. package/src/tests/scripts-only-gating.test.ts +2 -0
  237. package/src/tests/scripts-runtime-identity.test.ts +1 -1
  238. package/src/tests/slack-blocks.test.ts +12 -1
  239. package/src/tests/slack-render-v2.test.ts +76 -3
  240. package/src/tests/slack-router.test.ts +17 -2
  241. package/src/tests/slack-steering.test.ts +19 -0
  242. package/src/tests/status.test.ts +12 -0
  243. package/src/tests/steering-enabled.test.ts +6 -8
  244. package/src/tests/task-tool-preload-mcp.test.ts +4 -1
  245. package/src/tests/tool-annotations.test.ts +1 -1
  246. package/src/tests/tool-registrar-no-input.test.ts +42 -1
  247. package/src/tools/defer-task.ts +1 -1
  248. package/src/tools/extension-common.ts +84 -0
  249. package/src/tools/extension-install.ts +65 -0
  250. package/src/tools/extension-list.ts +66 -0
  251. package/src/tools/script-common.ts +43 -18
  252. package/src/tools/send-task.ts +69 -107
  253. package/src/tools/task-action.ts +43 -22
  254. package/src/tools/task-tool-ctx.ts +2 -0
  255. package/src/tools/tool-config.ts +3 -1
  256. package/src/tools/utils.ts +140 -16
  257. package/src/types.ts +133 -0
  258. package/src/utils/multi-runtime.ts +2 -2
  259. package/src/utils/steering-enabled.ts +4 -4
  260. package/src/workflows/executors/agent-task.ts +13 -3
  261. package/templates/extensions/README.md +17 -0
  262. package/templates/extensions/notify-on-complete/README.md +11 -0
  263. package/templates/extensions/notify-on-complete/hooks.ts +61 -0
  264. package/templates/extensions/notify-on-complete/manifest.json +7 -0
  265. package/templates/extensions/require-ticket-ref/README.md +11 -0
  266. package/templates/extensions/require-ticket-ref/hooks.ts +30 -0
  267. package/templates/extensions/require-ticket-ref/manifest.json +7 -0
  268. package/templates/extensions/require-verification-note/README.md +11 -0
  269. package/templates/extensions/require-verification-note/hooks.ts +32 -0
  270. package/templates/extensions/require-verification-note/manifest.json +7 -0
  271. package/templates/skills/scheduling/content.md +2 -0
  272. package/templates/skills/swarm-extensions/config.json +14 -0
  273. package/templates/skills/swarm-extensions/content.md +120 -0
  274. package/dist/cli-b8x19vbh.js +0 -823
  275. package/dist/cli-n1yj5fd5.js +0 -164
  276. package/dist/cli-q04q68be.js +0 -3066
  277. package/dist/cli-qgm2pfpq.js +0 -6377
  278. package/dist/cli-rnsr8asm.js +0 -4323
  279. package/dist/cli-rwp3ksm7.js +0 -545
  280. package/dist/cli-xjb9wghj.js +0 -668
  281. package/dist/cli-xvkmedgn.js +0 -332
  282. package/dist/cli-ydk0wjj8.js +0 -338
  283. package/dist/handlers-vr1djg4n.js +0 -41
  284. package/dist/{claude-managed-setup-f5pmsyrp.js → claude-managed-setup-48dwg9bv.js} +3 -3
  285. package/dist/{cli-81r683bv.js → cli-ch38q5z4.js} +0 -0
  286. package/dist/{cli-wk0gjrn3.js → cli-kj76p1ez.js} +6 -6
  287. package/dist/{cli-c71mg51n.js → cli-r2fpe7cx.js} +5 -5
  288. package/dist/{cli-8qx7jnrk.js → cli-y6x0gqtg.js} +16 -16
  289. package/dist/{index-aa3sa6s8.js → index-2wsdsnmm.js} +1 -1
  290. package/dist/{mistral-conversations-4ryk7h83.js → mistral-conversations-qhr84ph3.js} +9 -9
  291. package/dist/{openai-completions-qdcmkfnr.js → openai-completions-32rdwmze.js} +17 -17
  292. package/dist/{openrouter-images-qx18fyp8.js → openrouter-images-9f9ek4rw.js} +3 -3
  293. package/dist/{pi-messages-79jvzz1d.js → pi-messages-b1mbr6gp.js} +4 -4
@@ -0,0 +1,84 @@
1
+ import * as z from "zod";
2
+ import { proxySwarmApi } from "./script-common";
3
+ import { type RequestInfo, type SwarmToolResult, swarmToolOutputSchema, toolErr } from "./utils";
4
+
5
+ export const EXTENSION_TRANSPORT_ERROR =
6
+ "extension_* tools require HTTP MCP transport because agent identity is unavailable over stdio. Use MCP_BASE_URL=http://... or invoke the HTTP API directly.";
7
+
8
+ const extensionListItemSchema = z.looseObject({
9
+ id: z.string().optional(),
10
+ name: z.string().optional(),
11
+ version: z.number().optional(),
12
+ activeVersion: z.number().optional(),
13
+ enabled: z.boolean().optional(),
14
+ status: z.string().optional(),
15
+ priority: z.number().optional(),
16
+ consecutiveFailures: z.number().optional(),
17
+ });
18
+
19
+ export const extensionToolOutputSchema = swarmToolOutputSchema({
20
+ id: z.string().optional(),
21
+ name: z.string().optional(),
22
+ version: z.number().optional(),
23
+ activeVersion: z.number().optional(),
24
+ enabled: z.boolean().optional(),
25
+ status: z.string().optional(),
26
+ priority: z.number().optional(),
27
+ consecutiveFailures: z.number().optional(),
28
+ contentDeduped: z.boolean().optional(),
29
+ extensions: z.array(extensionListItemSchema).optional(),
30
+ });
31
+
32
+ function isRecord(value: unknown): value is Record<string, unknown> {
33
+ return typeof value === "object" && value !== null && !Array.isArray(value);
34
+ }
35
+
36
+ export function coerceExtensionSummary(entry: unknown) {
37
+ const extension = isRecord(entry) ? entry : {};
38
+ return {
39
+ id: typeof extension.id === "string" ? extension.id : undefined,
40
+ name: typeof extension.name === "string" ? extension.name : undefined,
41
+ version: typeof extension.version === "number" ? extension.version : undefined,
42
+ activeVersion:
43
+ typeof extension.activeVersion === "number" ? extension.activeVersion : undefined,
44
+ enabled: typeof extension.enabled === "boolean" ? extension.enabled : undefined,
45
+ status: typeof extension.status === "string" ? extension.status : undefined,
46
+ priority: typeof extension.priority === "number" ? extension.priority : undefined,
47
+ consecutiveFailures:
48
+ typeof extension.consecutiveFailures === "number" ? extension.consecutiveFailures : undefined,
49
+ };
50
+ }
51
+
52
+ function errorMessage(data: unknown, status: number): string {
53
+ if (isRecord(data) && typeof data.error === "string" && data.error) return data.error;
54
+ return `Extensions API request failed with ${status}`;
55
+ }
56
+
57
+ function renderDiagnostics(data: unknown): string | undefined {
58
+ if (!isRecord(data) || !Array.isArray(data.diagnostics)) return undefined;
59
+ const diagnostics = data.diagnostics.map(String).filter(Boolean);
60
+ return diagnostics.length > 0 ? diagnostics.join("\n") : undefined;
61
+ }
62
+
63
+ export async function proxyExtensionsApi<T>(args: {
64
+ method: "GET" | "POST";
65
+ path: string;
66
+ body?: unknown;
67
+ requestInfo: RequestInfo;
68
+ success: (data: unknown) => SwarmToolResult<T & Record<string, unknown>>;
69
+ }): Promise<SwarmToolResult<T & Record<string, unknown>>> {
70
+ return proxySwarmApi({
71
+ ...args,
72
+ transportError: EXTENSION_TRANSPORT_ERROR,
73
+ respond: (data, response) => {
74
+ if (!response.ok) {
75
+ const diagnostics = renderDiagnostics(data);
76
+ if (response.status === 400 && diagnostics) {
77
+ return toolErr("Extension rejected by typecheck.", { details: diagnostics });
78
+ }
79
+ return toolErr(errorMessage(data, response.status), { details: diagnostics });
80
+ }
81
+ return args.success(data);
82
+ },
83
+ });
84
+ }
@@ -0,0 +1,65 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createToolRegistrar, toolOk } from "@/tools/utils";
4
+ import { ExtensionManifestSchema } from "@/types";
5
+ import {
6
+ coerceExtensionSummary,
7
+ extensionToolOutputSchema,
8
+ proxyExtensionsApi,
9
+ } from "./extension-common";
10
+
11
+ export const EXTENSION_INSTALL_DESCRIPTION =
12
+ "Validate and store a TypeScript extension bundle (manifest plus a files map with one hooks.ts). Fetch the hook contract first: GET /api/extensions/type-defs returns swarm-extension.d.ts with every event and modify shape. The extension remains disabled until an operator enables it.";
13
+
14
+ function extensionInstallResult(data: unknown, fallbackName: string) {
15
+ const body = data as {
16
+ extension?: unknown;
17
+ contentDeduped?: unknown;
18
+ };
19
+ const extension = coerceExtensionSummary(body.extension);
20
+ const name = extension.name ?? fallbackName;
21
+ const version = typeof extension.version === "number" ? ` v${extension.version}` : "";
22
+ const deduped = body.contentDeduped === true ? " Content was unchanged." : "";
23
+ const enableInstruction =
24
+ "An operator must enable it from the dashboard or `POST /api/extensions/{id}/enable`.";
25
+
26
+ return toolOk(`Extension \`${name}\`${version} installed.${deduped} ${enableInstruction}`, {
27
+ data: {
28
+ ...extension,
29
+ name,
30
+ contentDeduped: body.contentDeduped === true,
31
+ },
32
+ });
33
+ }
34
+
35
+ export const registerExtensionInstallTool = (server: McpServer) => {
36
+ createToolRegistrar(server)(
37
+ "extension-install",
38
+ {
39
+ title: "Extension Install",
40
+ description: EXTENSION_INSTALL_DESCRIPTION,
41
+ annotations: { openWorldHint: false },
42
+ inputSchema: z
43
+ .object({
44
+ manifest: ExtensionManifestSchema.describe("Extension bundle manifest."),
45
+ files: z.record(z.string(), z.string()).describe("Bundle files keyed by relative path."),
46
+ priority: z
47
+ .number()
48
+ .int()
49
+ .optional()
50
+ .describe("Handler priority. Lower values run first."),
51
+ config: z.record(z.string(), z.unknown()).optional().describe("Extension configuration."),
52
+ })
53
+ .strict(),
54
+ outputSchema: extensionToolOutputSchema,
55
+ },
56
+ async (args, requestInfo) =>
57
+ proxyExtensionsApi({
58
+ method: "POST",
59
+ path: "/api/extensions/install",
60
+ body: args,
61
+ requestInfo,
62
+ success: (data) => extensionInstallResult(data, args.manifest.name),
63
+ }),
64
+ );
65
+ };
@@ -0,0 +1,66 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { createToolRegistrar, toolOk } from "@/tools/utils";
4
+ import {
5
+ coerceExtensionSummary,
6
+ extensionToolOutputSchema,
7
+ proxyExtensionsApi,
8
+ } from "./extension-common";
9
+
10
+ export const EXTENSION_LIST_DESCRIPTION =
11
+ "List installed extensions and their activation state. To write a new one, fetch GET /api/extensions/type-defs for the hook contract and install it with extension-install.";
12
+
13
+ type ExtensionListItem = ReturnType<typeof coerceExtensionSummary>;
14
+
15
+ function extensionListItems(data: unknown): ExtensionListItem[] {
16
+ if (
17
+ typeof data !== "object" ||
18
+ data === null ||
19
+ !Array.isArray((data as { extensions?: unknown }).extensions)
20
+ ) {
21
+ return [];
22
+ }
23
+ return (data as { extensions: unknown[] }).extensions.map(coerceExtensionSummary);
24
+ }
25
+
26
+ function renderExtensionsTable(extensions: ExtensionListItem[]): string {
27
+ const header = "| Name | Version | Active | Enabled | Status | Priority | Failures |";
28
+ const divider = "| --- | ---: | ---: | --- | --- | ---: | ---: |";
29
+ const rows = extensions.map(
30
+ (extension) =>
31
+ `| ${extension.name ?? "?"} | ${extension.version ?? "?"} | ${extension.activeVersion ?? "?"} | ${extension.enabled ?? false} | ${extension.status ?? "unknown"} | ${extension.priority ?? "?"} | ${extension.consecutiveFailures ?? 0} |`,
32
+ );
33
+ return [header, divider, ...rows].join("\n");
34
+ }
35
+
36
+ export const registerExtensionListTool = (server: McpServer) => {
37
+ createToolRegistrar(server)(
38
+ "extension-list",
39
+ {
40
+ title: "Extension List",
41
+ description: EXTENSION_LIST_DESCRIPTION,
42
+ annotations: { readOnlyHint: true, openWorldHint: false },
43
+ inputSchema: z
44
+ .object({
45
+ enabledOnly: z.boolean().optional().describe("Return only enabled extensions."),
46
+ })
47
+ .strict(),
48
+ outputSchema: extensionToolOutputSchema,
49
+ },
50
+ async ({ enabledOnly }, requestInfo) =>
51
+ proxyExtensionsApi({
52
+ method: "GET",
53
+ path: "/api/extensions",
54
+ requestInfo,
55
+ success: (data) => {
56
+ const extensions = extensionListItems(data).filter(
57
+ (extension) => !enabledOnly || extension.enabled,
58
+ );
59
+ return toolOk(`Found ${extensions.length} extension(s).`, {
60
+ details: renderExtensionsTable(extensions),
61
+ data: { extensions },
62
+ });
63
+ },
64
+ }),
65
+ );
66
+ };
@@ -3,6 +3,7 @@ import { getApiKey } from "@/utils/api-key";
3
3
  import { getMcpBaseUrl } from "@/utils/constants";
4
4
  import {
5
5
  type RequestInfo,
6
+ type SwarmToolData,
6
7
  type SwarmToolResult,
7
8
  swarmToolOutputSchema,
8
9
  toolErr,
@@ -161,7 +162,37 @@ export async function proxyScriptsApi(args: {
161
162
  */
162
163
  uncappedDetails?: boolean;
163
164
  }): Promise<SwarmToolResult> {
164
- if (!args.requestInfo.agentId) return toolErr(SCRIPT_TRANSPORT_ERROR);
165
+ return proxySwarmApi({
166
+ ...args,
167
+ transportError: SCRIPT_TRANSPORT_ERROR,
168
+ respond: (data, res) => {
169
+ const toolData = { status: res.status, data };
170
+ const failure = describeScriptFailure(res.ok, res.status, data);
171
+ if (failure) {
172
+ return toolErr(failure.message, {
173
+ details: failure.details ?? capDetails(args.failureDetails?.(data)),
174
+ data: toolData,
175
+ });
176
+ }
177
+
178
+ const successDetails = args.successDetails?.(data);
179
+ return toolOk(args.successMessage(data), {
180
+ details: args.uncappedDetails ? successDetails : capDetails(successDetails),
181
+ data: toolData,
182
+ });
183
+ },
184
+ });
185
+ }
186
+
187
+ export async function proxySwarmApi<T extends Record<string, unknown>>(args: {
188
+ method: "GET" | "POST" | "PATCH" | "DELETE";
189
+ path: string;
190
+ body?: unknown;
191
+ requestInfo: RequestInfo;
192
+ transportError: string;
193
+ respond: (data: unknown, response: { ok: boolean; status: number }) => SwarmToolResult<T>;
194
+ }): Promise<SwarmToolResult<T>> {
195
+ if (!args.requestInfo.agentId) return toolErr(args.transportError);
165
196
 
166
197
  const apiKey = getApiKey();
167
198
  const headers: Record<string, string> = {
@@ -172,8 +203,7 @@ export async function proxyScriptsApi(args: {
172
203
  if (args.requestInfo.sourceTaskId) {
173
204
  headers["X-Source-Task-Id"] = args.requestInfo.sourceTaskId;
174
205
  }
175
- // Runtime identity is process context from the worker's MCP session — it
176
- // rides the proxy like the agent identity, never as tool or script input.
206
+ // Forward runtime identity from the worker's MCP session, like agent identity.
177
207
  if (args.requestInfo.runtimeInstanceId) {
178
208
  headers["X-Runtime-Instance-ID"] = args.requestInfo.runtimeInstanceId;
179
209
  }
@@ -189,22 +219,17 @@ export async function proxyScriptsApi(args: {
189
219
  try {
190
220
  data = JSON.parse(text);
191
221
  } catch {
192
- data = { error: text };
222
+ const contentType = res.headers.get("content-type") ?? "unknown";
223
+ // Transport-level failure: only `{ status }` is known here, and that shape
224
+ // is not statically related to the caller's `T`. Tool output schemas are
225
+ // loose with every data field optional, so the wire result stays valid.
226
+ const transportData: SwarmToolData = { status: res.status };
227
+ return toolErr<T>(
228
+ `Swarm API returned non-JSON response (HTTP ${res.status}, Content-Type: ${contentType}). Check API/proxy routing.`,
229
+ { details: capDetails(text), data: transportData as T },
230
+ );
193
231
  }
194
232
  }
195
233
 
196
- const toolData = { status: res.status, data };
197
- const failure = describeScriptFailure(res.ok, res.status, data);
198
- if (failure) {
199
- return toolErr(failure.message, {
200
- details: failure.details ?? capDetails(args.failureDetails?.(data)),
201
- data: toolData,
202
- });
203
- }
204
-
205
- const successDetails = args.successDetails?.(data);
206
- return toolOk(args.successMessage(data), {
207
- details: args.uncappedDetails ? successDetails : capDetails(successDetails),
208
- data: toolData,
209
- });
234
+ return args.respond(data, res);
210
235
  }
@@ -15,6 +15,7 @@ import {
15
15
  hasCapacity,
16
16
  } from "@/be/db";
17
17
  import { repointTrackerSyncBySwarmId } from "@/be/db-queries/tracker";
18
+ import { applyPreTaskCreate } from "@/extensions/apply-task-create";
18
19
  import { checkSlackRoutingCoherence } from "@/tasks/slack-routing";
19
20
  import { findDuplicateTask } from "@/tools/task-dedup";
20
21
  import { ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
@@ -28,6 +29,7 @@ import {
28
29
  import {
29
30
  type AgentTask,
30
31
  AssetKeySchema,
32
+ type CreateTaskOptions,
31
33
  FollowUpConfigSchema,
32
34
  ModelTierSchema,
33
35
  ReasoningEffortSchema,
@@ -382,6 +384,53 @@ export async function sendTaskHandler(
382
384
  const effectiveRoutingSource =
383
385
  agentId !== undefined ? "declared" : effectiveAgentId ? "engine_default" : undefined;
384
386
 
387
+ const requestedTaskOptions: CreateTaskOptions = {
388
+ key: assetKey,
389
+ agentId: offerMode ? undefined : effectiveAgentId,
390
+ offeredTo: offerMode ? effectiveAgentId : undefined,
391
+ creatorAgentId,
392
+ requestedByUserId,
393
+ source: "mcp",
394
+ sourceTaskId,
395
+ taskType,
396
+ tags,
397
+ priority,
398
+ dependsOn,
399
+ dir,
400
+ parentTaskId: effectiveParentTaskId,
401
+ vcsRepo: effectiveVcsRepo,
402
+ model: normalizedModel.model,
403
+ modelTier: normalizedModel.modelTier,
404
+ effort,
405
+ slackChannelId,
406
+ slackThreadTs,
407
+ slackUserId,
408
+ overrideSlackContext,
409
+ followUpConfig,
410
+ outputSchema,
411
+ routingReason: effectiveRoutingReason,
412
+ routingSource: effectiveRoutingSource,
413
+ routingNote: effectiveRoutingNote,
414
+ routingAffinity:
415
+ effectiveLeadOnly || requiredCapabilities?.length
416
+ ? { leadOnly: effectiveLeadOnly, capabilities: requiredCapabilities ?? [] }
417
+ : undefined,
418
+ };
419
+ const preCreate = await applyPreTaskCreate({
420
+ description: task,
421
+ options: requestedTaskOptions,
422
+ origin: "mcp",
423
+ requestInfo: ctx.kind === "owner" ? ctx.requestInfo : undefined,
424
+ });
425
+ if (preCreate.kind === "blocked") {
426
+ return toolErr(preCreate.reason, {
427
+ data: { yourAgentId: creatorAgentId },
428
+ details: JSON.stringify({ extension: preCreate.extension }),
429
+ });
430
+ }
431
+ const taskDescription = preCreate.description;
432
+ const taskOptions = preCreate.options;
433
+
385
434
  // The three dedup guards are pure reads, so they run twice: once here as a
386
435
  // fast path (keeping this tool's existing early-exit responses), and once
387
436
  // inside the write transaction below, where the check is authoritative.
@@ -405,9 +454,9 @@ export async function sendTaskHandler(
405
454
  // Dedup guard: check for similar recent tasks
406
455
  if (!allowDuplicate && creatorAgentId) {
407
456
  const duplicate = await findDuplicateTask({
408
- taskDescription: task,
409
- creatorAgentId,
410
- targetAgentId: effectiveAgentId ?? undefined,
457
+ taskDescription,
458
+ creatorAgentId: taskOptions.creatorAgentId ?? creatorAgentId,
459
+ targetAgentId: taskOptions.agentId ?? taskOptions.offeredTo,
411
460
  });
412
461
  if (duplicate) {
413
462
  return {
@@ -487,42 +536,13 @@ export async function sendTaskHandler(
487
536
  const raced = await evaluateDedupGuards();
488
537
  if (raced) return { success: raced.ok, message: raced.message, task: raced.task };
489
538
 
490
- const finalTags = tags;
491
-
492
539
  // If no agentId (and no auto-routed agentId), create an unassigned task for the pool
493
- if (!effectiveAgentId) {
494
- const newTask = await createTaskExtended(task, {
495
- key: assetKey,
496
- creatorAgentId,
497
- requestedByUserId,
498
- sourceTaskId,
499
- taskType,
500
- tags: finalTags,
501
- priority,
502
- dependsOn,
503
- dir,
504
- parentTaskId: effectiveParentTaskId,
505
- vcsRepo: effectiveVcsRepo,
506
- model: normalizedModel.model,
507
- modelTier: normalizedModel.modelTier,
508
- effort,
509
- slackChannelId,
510
- slackThreadTs,
511
- slackUserId,
512
- overrideSlackContext,
513
- followUpConfig,
514
- outputSchema,
515
- routingReason: effectiveRoutingReason,
516
- routingSource: effectiveRoutingSource,
517
- routingNote: effectiveRoutingNote,
518
- routingAffinity:
519
- effectiveLeadOnly || requiredCapabilities?.length
520
- ? { leadOnly: effectiveLeadOnly, capabilities: requiredCapabilities ?? [] }
521
- : undefined,
522
- });
540
+ const targetAgentId = taskOptions.offeredTo ?? taskOptions.agentId ?? undefined;
541
+ if (!targetAgentId) {
542
+ const newTask = await createTaskExtended(taskDescription, taskOptions);
523
543
  await transferTrackerSyncToResumeChild({
524
- parentTaskId: effectiveParentTaskId,
525
- taskType,
544
+ parentTaskId: taskOptions.parentTaskId,
545
+ taskType: taskOptions.taskType,
526
546
  child: newTask,
527
547
  });
528
548
 
@@ -533,16 +553,16 @@ export async function sendTaskHandler(
533
553
  };
534
554
  }
535
555
 
536
- const agent = await getAgentById(effectiveAgentId);
556
+ const agent = await getAgentById(targetAgentId);
537
557
 
538
558
  if (!agent) {
539
559
  return {
540
560
  success: false,
541
- message: `Agent with ID "${effectiveAgentId}" not found.`,
561
+ message: `Agent with ID "${targetAgentId}" not found.`,
542
562
  };
543
563
  }
544
564
 
545
- if (effectiveLeadOnly && !agent.isLead) {
565
+ if (taskOptions.routingAffinity?.leadOnly && !agent.isLead) {
546
566
  return {
547
567
  success: false,
548
568
  message: `Lead-only task requires a Lead agent; "${agent.name}" is not a Lead.`,
@@ -550,49 +570,20 @@ export async function sendTaskHandler(
550
570
  }
551
571
 
552
572
  // For direct assignment (not offer), check if agent has capacity
553
- if (!offerMode && !(await hasCapacity(effectiveAgentId))) {
554
- const activeCount = await getActiveTaskCount(effectiveAgentId);
573
+ if (!taskOptions.offeredTo && !(await hasCapacity(targetAgentId))) {
574
+ const activeCount = await getActiveTaskCount(targetAgentId);
555
575
  return {
556
576
  success: false,
557
577
  message: `Agent "${agent.name}" is at capacity (${activeCount}/${agent.maxTasks ?? 1} tasks). Use offerMode: true to offer the task instead, or wait for a task to complete.`,
558
578
  };
559
579
  }
560
580
 
561
- if (offerMode) {
581
+ if (taskOptions.offeredTo) {
562
582
  // Offer the task to the agent (they must accept/reject)
563
- const newTask = await createTaskExtended(task, {
564
- key: assetKey,
565
- offeredTo: effectiveAgentId,
566
- creatorAgentId,
567
- requestedByUserId,
568
- sourceTaskId,
569
- taskType,
570
- tags: finalTags,
571
- priority,
572
- dependsOn,
573
- dir,
574
- parentTaskId: effectiveParentTaskId,
575
- vcsRepo: effectiveVcsRepo,
576
- model: normalizedModel.model,
577
- modelTier: normalizedModel.modelTier,
578
- effort,
579
- slackChannelId,
580
- slackThreadTs,
581
- slackUserId,
582
- overrideSlackContext,
583
- followUpConfig,
584
- outputSchema,
585
- routingReason: effectiveRoutingReason,
586
- routingSource: effectiveRoutingSource,
587
- routingNote: effectiveRoutingNote,
588
- routingAffinity:
589
- effectiveLeadOnly || requiredCapabilities?.length
590
- ? { leadOnly: effectiveLeadOnly, capabilities: requiredCapabilities ?? [] }
591
- : undefined,
592
- });
583
+ const newTask = await createTaskExtended(taskDescription, taskOptions);
593
584
  await transferTrackerSyncToResumeChild({
594
- parentTaskId: effectiveParentTaskId,
595
- taskType,
585
+ parentTaskId: taskOptions.parentTaskId,
586
+ taskType: taskOptions.taskType,
596
587
  child: newTask,
597
588
  });
598
589
 
@@ -604,39 +595,10 @@ export async function sendTaskHandler(
604
595
  }
605
596
 
606
597
  // Direct assignment
607
- const newTask = await createTaskExtended(task, {
608
- key: assetKey,
609
- agentId: effectiveAgentId,
610
- creatorAgentId,
611
- requestedByUserId,
612
- sourceTaskId,
613
- taskType,
614
- tags: finalTags,
615
- priority,
616
- dependsOn,
617
- dir,
618
- parentTaskId: effectiveParentTaskId,
619
- vcsRepo: effectiveVcsRepo,
620
- model: normalizedModel.model,
621
- modelTier: normalizedModel.modelTier,
622
- effort,
623
- slackChannelId,
624
- slackThreadTs,
625
- slackUserId,
626
- overrideSlackContext,
627
- followUpConfig,
628
- outputSchema,
629
- routingReason: effectiveRoutingReason,
630
- routingSource: effectiveRoutingSource,
631
- routingNote: effectiveRoutingNote,
632
- routingAffinity:
633
- effectiveLeadOnly || requiredCapabilities?.length
634
- ? { leadOnly: effectiveLeadOnly, capabilities: requiredCapabilities ?? [] }
635
- : undefined,
636
- });
598
+ const newTask = await createTaskExtended(taskDescription, taskOptions);
637
599
  await transferTrackerSyncToResumeChild({
638
- parentTaskId: effectiveParentTaskId,
639
- taskType,
600
+ parentTaskId: taskOptions.parentTaskId,
601
+ taskType: taskOptions.taskType,
640
602
  child: newTask,
641
603
  });
642
604
 
@@ -28,6 +28,7 @@ import {
28
28
  updateTaskClaudeSessionId,
29
29
  } from "@/be/db";
30
30
  import { touchRuntimeInstance } from "@/be/multi-runtime";
31
+ import { applyPreTaskCreate } from "@/extensions/apply-task-create";
31
32
  import { assertOwnsTask, ownerCtx, type ToolCtx } from "@/tools/task-tool-ctx";
32
33
  import {
33
34
  createToolRegistrar,
@@ -39,6 +40,7 @@ import {
39
40
  import {
40
41
  AssetKeySchema,
41
42
  BudgetRefusalCauseSchema,
43
+ type CreateTaskOptions,
42
44
  ModelTierSchema,
43
45
  ReasoningEffortSchema,
44
46
  splitLegacyModelAlias,
@@ -265,7 +267,14 @@ export async function taskActionHandler(
265
267
 
266
268
  const agentId = ctx.agentId;
267
269
  let assetKey: string | undefined;
270
+ let preparedCreate: { description: string; options: CreateTaskOptions } | undefined;
268
271
  if (action === "create") {
272
+ if (!task) {
273
+ return taskActionResult(
274
+ { success: false, message: "Task description is required for 'create' action." },
275
+ agentId,
276
+ );
277
+ }
269
278
  try {
270
279
  assetKey = key
271
280
  ? await authorizeAssetKeyWrite(key, await resolveTaskAuditUserId(ctx.sourceTaskId, agentId))
@@ -279,33 +288,45 @@ export async function taskActionHandler(
279
288
  : String(error);
280
289
  return taskActionResult({ success: false, message }, agentId);
281
290
  }
291
+
292
+ const preCreate = await applyPreTaskCreate({
293
+ description: task,
294
+ options: {
295
+ key: assetKey,
296
+ creatorAgentId: agentId,
297
+ source: "mcp",
298
+ taskType,
299
+ tags,
300
+ priority,
301
+ dependsOn,
302
+ dir,
303
+ model: normalizedModel.model,
304
+ modelTier: normalizedModel.modelTier,
305
+ effort: input.effort,
306
+ routingAffinity:
307
+ leadOnly || requiredCapabilities?.length
308
+ ? { leadOnly, capabilities: requiredCapabilities ?? [] }
309
+ : undefined,
310
+ },
311
+ origin: "mcp",
312
+ requestInfo: ctx.requestInfo,
313
+ });
314
+ if (preCreate.kind === "blocked") {
315
+ return taskActionResult({ success: false, message: preCreate.reason }, agentId);
316
+ }
317
+ preparedCreate = {
318
+ description: preCreate.description,
319
+ options: preCreate.options,
320
+ };
282
321
  }
283
322
 
284
323
  const result = await getDbClient().transaction(async (): Promise<TaskActionResult> => {
285
324
  switch (action) {
286
325
  case "create": {
287
- if (!task) {
288
- return {
289
- success: false,
290
- message: "Task description is required for 'create' action.",
291
- };
292
- }
293
- const newTask = await createTaskExtended(task, {
294
- key: assetKey,
295
- creatorAgentId: agentId,
296
- taskType,
297
- tags,
298
- priority,
299
- dependsOn,
300
- dir,
301
- model: normalizedModel.model,
302
- modelTier: normalizedModel.modelTier,
303
- effort: input.effort,
304
- routingAffinity:
305
- leadOnly || requiredCapabilities?.length
306
- ? { leadOnly, capabilities: requiredCapabilities ?? [] }
307
- : undefined,
308
- });
326
+ const newTask = await createTaskExtended(
327
+ preparedCreate!.description,
328
+ preparedCreate!.options,
329
+ );
309
330
  return {
310
331
  success: true,
311
332
  message: `Created unassigned task "${newTask.id}".`,
@@ -9,6 +9,7 @@ export type ToolCtx =
9
9
  runtimeInstanceId?: string;
10
10
  sourceTaskId?: string;
11
11
  sessionId?: string;
12
+ requestInfo?: RequestInfo;
12
13
  }
13
14
  | { kind: "user"; userId: string; user: User; sessionId?: string };
14
15
 
@@ -19,6 +20,7 @@ export function ownerCtx(info: RequestInfo): ToolCtx {
19
20
  runtimeInstanceId: info.runtimeInstanceId,
20
21
  sourceTaskId: info.sourceTaskId,
21
22
  sessionId: info.sessionId,
23
+ requestInfo: info,
22
24
  };
23
25
  }
24
26
 
@@ -197,13 +197,15 @@ export const DEFERRED_TOOLS = new Set([
197
197
  "room-reset",
198
198
  "room-decode",
199
199
 
200
- // Reusable scripts (6)
200
+ // Reusable scripts and extensions (8)
201
201
  "script-upsert",
202
202
  "script-delete",
203
203
  "script-apis",
204
204
  "launch-script-run",
205
205
  "get-script-run",
206
206
  "list-script-runs",
207
+ "extension-install",
208
+ "extension-list",
207
209
 
208
210
  // External command routes (1)
209
211
  "swarm_x",