@desplega.ai/agent-swarm 1.169.0 → 1.171.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 (349) hide show
  1. package/README.md +11 -2
  2. package/dist/{acp-adapter-m47zfx4a.js → acp-adapter-1z9p3fvx.js} +6 -6
  3. package/dist/{actions-ws7k3tvq.js → actions-rc2yjk8a.js} +31 -29
  4. package/dist/{amp-adapter-nvv8k8j5.js → amp-adapter-ypyf5s1x.js} +6 -6
  5. package/dist/{anthropic-messages-13yn502s.js → anthropic-messages-etkbxhhy.js} +11 -11
  6. package/dist/{app-80w6qspx.js → app-04ab2s0z.js} +9 -9
  7. package/dist/{artifact-fey7dvj9.js → artifact-eczsknc3.js} +6 -3
  8. package/dist/{assistant-jtq439kw.js → assistant-vhmjd8wf.js} +34 -32
  9. package/dist/{azure-openai-responses-9st34pfz.js → azure-openai-responses-xap57x4b.js} +7 -7
  10. package/dist/{boot-reembed-feda8wn8.js → boot-reembed-3p0m259x.js} +8 -8
  11. package/dist/{boot-reembed-3nc0aaed.js → boot-reembed-wew3gwk8.js} +8 -8
  12. package/dist/{boot-scrub-sweep-grsa2er0.js → boot-scrub-sweep-5ytwtq24.js} +9 -9
  13. package/dist/{boot-seeds-gcfgq44f.js → boot-seeds-s8p2xry8.js} +12 -12
  14. package/dist/{claude-adapter-pjv2q7t8.js → claude-adapter-akrzg6jh.js} +15 -15
  15. package/dist/{claude-managed-adapter-0wa8se82.js → claude-managed-adapter-kqqqhp2q.js} +2 -2
  16. package/dist/{claude-managed-setup-zxa9p9tx.js → claude-managed-setup-bnmjarjc.js} +11 -8
  17. package/dist/{claude-sdk-session-c9j6k9kx.js → claude-sdk-session-exq1jadx.js} +15 -15
  18. package/dist/{cli-a8vps458.js → cli-01qndrvw.js} +17274 -17242
  19. package/dist/{cli-rg1c8ccw.js → cli-0d2rqhvq.js} +497 -73
  20. package/dist/cli-0p3enk35.js +188 -0
  21. package/dist/cli-1srmzpr3.js +16 -0
  22. package/dist/{cli-zzev32jw.js → cli-231av4y6.js} +6 -6
  23. package/dist/{cli-kp3vfknv.js → cli-280gnz4v.js} +2 -2
  24. package/dist/{cli-mv2520nm.js → cli-2bw3w47j.js} +1 -1
  25. package/dist/{cli-km8559dm.js → cli-2qbs2tvf.js} +18 -10
  26. package/dist/{cli-efnd2kce.js → cli-2rbt7at8.js} +6 -1
  27. package/dist/{cli-xvc3q9br.js → cli-451ks6aw.js} +1 -1
  28. package/dist/{cli-jkfxpg1b.js → cli-4bm25xtw.js} +2 -2
  29. package/dist/{cli-zp38xvtm.js → cli-5rqrj7q0.js} +1 -1
  30. package/dist/{cli-km4x66j0.js → cli-5y67yrt6.js} +13 -13
  31. package/dist/{cli-qg2zhjfc.js → cli-62bwwc2q.js} +8 -8
  32. package/dist/{cli-s0k2j7q9.js → cli-6xxxb8qt.js} +10 -10
  33. package/dist/cli-6zx22mv6.js +0 -0
  34. package/dist/{cli-zp67qg5x.js → cli-7a0sde2x.js} +72 -1
  35. package/dist/{cli-v1w7pbd8.js → cli-7jykv7ef.js} +1 -1
  36. package/dist/{cli-rfphkzh5.js → cli-7pz3b7x5.js} +24 -4
  37. package/dist/{cli-mb75wpxf.js → cli-81r1wjyv.js} +1 -1
  38. package/dist/{cli-rbr91whd.js → cli-8mr695sb.js} +9 -5
  39. package/dist/{cli-f14fvzag.js → cli-a1b9b32n.js} +6 -1
  40. package/dist/cli-cd0hv9nh.js +32 -0
  41. package/dist/{cli-dv76vq5d.js → cli-ctret352.js} +685 -209
  42. package/dist/{cli-wt7ccy4r.js → cli-d161g69q.js} +9 -1
  43. package/dist/cli-db3m6zdt.js +362 -0
  44. package/dist/{cli-hf5s9pgf.js → cli-e3pmyt39.js} +1 -1
  45. package/dist/{cli-ysnkvdx6.js → cli-ec7hrgqp.js} +313 -91
  46. package/dist/{cli-m23r88re.js → cli-erbp6b4g.js} +3 -3
  47. package/dist/{cli-9k7mqz00.js → cli-ge593j7e.js} +30 -3
  48. package/dist/cli-h8w3fg83.js +17 -0
  49. package/dist/{cli-xvdgasgy.js → cli-hsem16hb.js} +114 -9
  50. package/dist/{cli-yvxfaem2.js → cli-hsk5gd15.js} +9 -5
  51. package/dist/{cli-7vz49g7h.js → cli-jxsgrnmq.js} +101 -4
  52. package/dist/{cli-b7y8113e.js → cli-k2v5x13t.js} +1 -1
  53. package/dist/{cli-bqrs5ycj.js → cli-m4csbs67.js} +2 -2
  54. package/dist/{cli-91r17hb1.js → cli-mzj3aqac.js} +3 -3
  55. package/dist/{cli-swrz3mj8.js → cli-n805bb98.js} +1 -1
  56. package/dist/{cli-sajr098b.js → cli-n8ybnv1k.js} +3 -3
  57. package/dist/{cli-e5qk3m2a.js → cli-nkjwe4z7.js} +19 -19
  58. package/dist/{cli-3ry5gx4f.js → cli-p2pjfq1r.js} +2 -2
  59. package/dist/{cli-61tbnjpf.js → cli-p8f7dqgt.js} +767 -541
  60. package/dist/{cli-r7eeddkq.js → cli-ph3145b7.js} +3 -3
  61. package/dist/cli-ppy4nnnh.js +168 -0
  62. package/dist/cli-r455yd58.js +832 -0
  63. package/dist/{cli-jv36y8wz.js → cli-rrbctenq.js} +1 -1
  64. package/dist/{cli-8xjst8q2.js → cli-s88my3br.js} +11 -11
  65. package/dist/{cli-vejds37t.js → cli-srmej5tp.js} +3 -3
  66. package/dist/{cli-eqfbsxsn.js → cli-tg8q0b5y.js} +14 -11
  67. package/dist/cli-w0zx9ya1.js +1351 -0
  68. package/dist/{cli-rh44xx46.js → cli-wfvedq5w.js} +15 -15
  69. package/dist/{cli-n4z21pt8.js → cli-x207g4w1.js} +1 -1
  70. package/dist/{cli-fyeft04q.js → cli-x3fy2tgp.js} +2 -2
  71. package/dist/{cli-7mhy3ths.js → cli-x61srfpp.js} +5 -5
  72. package/dist/{cli-w6xa5nf5.js → cli-y1j1ggbf.js} +8 -8
  73. package/dist/{cli-jvmtxqtc.js → cli-ywh2a87k.js} +7 -2
  74. package/dist/{cli-3qhyh31j.js → cli-ywm1azg9.js} +6 -0
  75. package/dist/{cli-z48ggfyq.js → cli-zxm9p3x4.js} +2 -2
  76. package/dist/cli.js +42 -24
  77. package/dist/{codex-adapter-0kpg09ca.js → codex-adapter-9ag75whm.js} +13 -13
  78. package/dist/{codex-hook-295jpbfa.js → codex-hook-7ba15hjd.js} +5 -3
  79. package/dist/{codex-login-g1cyt5zm.js → codex-login-841kv5fk.js} +7 -5
  80. package/dist/{codex-session-runner-8n672hxh.js → codex-session-runner-9fp7y4cf.js} +13 -13
  81. package/dist/{commands-mhmg5n0x.js → commands-d7eah0y9.js} +10 -10
  82. package/dist/controller-hq5ggpcp.js +193 -0
  83. package/dist/controllers-connect-8t0k65x2.js +265 -0
  84. package/dist/{cursor-adapter-wn8n5zzk.js → cursor-adapter-t4pr1m3c.js} +5 -5
  85. package/dist/db-2rmd23wq.js +71 -0
  86. package/dist/{db-wa6s1mwv.js → db-sp39qv72.js} +8 -8
  87. package/dist/{devin-adapter-bffjj5n4.js → devin-adapter-xgtj680j.js} +2 -2
  88. package/dist/{dispatcher-nhs2pkr8.js → dispatcher-g7fsybsd.js} +30 -28
  89. package/dist/docker-f503k9p0.js +201 -0
  90. package/dist/{dsh-adapter-5423p70m.js → dsh-adapter-2h8m4cab.js} +5 -5
  91. package/dist/e2b-66xra4az.js +202 -0
  92. package/dist/{e2b-nd50eccg.js → e2b-xwhyj8cw.js} +43 -527
  93. package/dist/{event-bus-vnfthafg.js → event-bus-vjtv82gm.js} +2 -2
  94. package/dist/{grok-adapter-28pkdt80.js → grok-adapter-1err1me7.js} +7 -7
  95. package/dist/{handlers-5rtf9nng.js → handlers-c7yggm37.js} +33 -31
  96. package/dist/heartbeat-dkm924fk.js +124 -0
  97. package/dist/{hook-qkm73zk6.js → hook-g8e7sfnf.js} +13 -13
  98. package/dist/{hook-krjfkpzy.js → hook-wgx4exw6.js} +20 -17
  99. package/dist/{http-a397xcn4.js → http-1d23k928.js} +1958 -205
  100. package/dist/{index-kqre45fr.js → index-gm10tmye.js} +30 -28
  101. package/dist/{index-rtxg15ah.js → index-q7cj9bn2.js} +39 -29
  102. package/dist/{index-vvsq7v45.js → index-va4zbvzp.js} +30 -28
  103. package/dist/{index-ac0v8v9d.js → index-vertaxs1.js} +8 -8
  104. package/dist/{index-ytdhtdry.js → index-y5vvczrs.js} +31 -22
  105. package/dist/{keepalive-psfh8ysx.js → keepalive-1jc5e7ct.js} +10 -10
  106. package/dist/{lead-m6rppjn7.js → lead-yxw2f1sg.js} +48 -46
  107. package/dist/{maintenance-41f837ke.js → maintenance-h7w2m10y.js} +8 -8
  108. package/dist/{node-8zkdq42h.js → node-c9we95xf.js} +9 -9
  109. package/dist/{oauth-refresh-sweep-6hr55hm8.js → oauth-refresh-sweep-wh9ypd1b.js} +9 -9
  110. package/dist/{onboard-ktrtd7cz.js → onboard-dfr9j762.js} +25 -15
  111. package/dist/{openai-codex-responses-jzxkta9b.js → openai-codex-responses-pgvtddvv.js} +3 -3
  112. package/dist/{openai-responses-m3g43rrt.js → openai-responses-9dyzwzkr.js} +9 -9
  113. package/dist/{opencode-adapter-yjyqynzf.js → opencode-adapter-f1zby94p.js} +3 -3
  114. package/dist/{otel-impl-dq4f2k3m.js → otel-impl-and31m5s.js} +5 -5
  115. package/dist/{pi-mono-adapter-e5v9w0hv.js → pi-mono-adapter-fx6k5wwh.js} +38 -27
  116. package/dist/{pricing-refresh-kmk7j1ra.js → pricing-refresh-23zr6j3w.js} +10 -10
  117. package/dist/{rbac-roles-9ykx9tc5.js → rbac-roles-nc7k9d0t.js} +10 -10
  118. package/dist/{rbac-roles-9w1dkkxd.js → rbac-roles-wnx35x4x.js} +11 -11
  119. package/dist/{render-v2-ycqm53np.js → render-v2-81cg1y52.js} +9 -9
  120. package/dist/{resume-cq2pkp8q.js → resume-d4b6j2n2.js} +16 -16
  121. package/dist/{setup-0dcmdvbn.js → setup-g68r1w28.js} +7 -5
  122. package/dist/static-t1gr2cr7.js +30 -0
  123. package/dist/{virtual-modules-1x9k4jcs.js → virtual-modules-rdjmcg7q.js} +3 -3
  124. package/dist/{work-objects-x01x2fh8.js → work-objects-0zzdnbt9.js} +35 -33
  125. package/dist/{worker-py1ssb1h.js → worker-p1g8qm6h.js} +48 -46
  126. package/dist/{x-ma4gnfgy.js → x-qfmygqqv.js} +2 -2
  127. package/openapi.json +3382 -167
  128. package/package.json +3 -3
  129. package/src/be/db/agents.ts +6 -0
  130. package/src/be/db/instance-signals.ts +30 -0
  131. package/src/be/db/tasks/read.ts +2 -0
  132. package/src/be/db/tasks/write.ts +39 -5
  133. package/src/be/db/workflow-runs.ts +22 -0
  134. package/src/be/db.ts +96 -40
  135. package/src/be/fleet-benchmark-stats.ts +39 -0
  136. package/src/be/migrations/205_favorites_per_dashboard_user.sql +29 -0
  137. package/src/be/migrations/206_runtime_pools.sql +62 -0
  138. package/src/be/migrations/207_runtime_session_tokens.sql +31 -0
  139. package/src/be/migrations/208_task_runtime_dispatch.sql +4 -0
  140. package/src/be/model-tier-resolution.ts +9 -2
  141. package/src/be/multi-runtime.ts +102 -13
  142. package/src/be/org-profile.ts +35 -0
  143. package/src/be/rbac-audit.ts +4 -0
  144. package/src/be/runtime-pools/controllers.ts +150 -0
  145. package/src/be/runtime-pools/db.ts +526 -0
  146. package/src/be/runtime-pools/demand.ts +65 -0
  147. package/src/be/runtime-pools/e2b-images.ts +109 -0
  148. package/src/be/runtime-pools/errors.ts +8 -0
  149. package/src/be/runtime-pools/lifecycle.ts +44 -0
  150. package/src/be/runtime-pools/reachability.ts +30 -0
  151. package/src/be/runtime-pools/recovery.ts +248 -0
  152. package/src/be/runtime-pools/remote-controller.ts +144 -0
  153. package/src/be/runtime-pools/routing.ts +304 -0
  154. package/src/be/runtime-pools/seed.ts +110 -0
  155. package/src/be/runtime-pools/supervisor.ts +981 -0
  156. package/src/be/runtime-pools/tokens.ts +57 -0
  157. package/src/be/scripts/typecheck.ts +48 -3
  158. package/src/be/secret-registry.ts +17 -1
  159. package/src/be/skill-usage.ts +467 -0
  160. package/src/be/swarm-config-guard.ts +59 -2
  161. package/src/be/telemetry-enable.ts +66 -0
  162. package/src/be/telemetry-ingest-anchor.ts +77 -0
  163. package/src/be/users.ts +173 -23
  164. package/src/be/worker-nudge-listener.ts +28 -0
  165. package/src/be/worker-nudges.ts +22 -0
  166. package/src/cli.tsx +22 -3
  167. package/src/commands/controller.ts +180 -0
  168. package/src/commands/onboard/non-interactive.ts +36 -12
  169. package/src/commands/onboard/types.ts +1 -1
  170. package/src/commands/onboard.tsx +3 -0
  171. package/src/commands/push-delivery.ts +230 -0
  172. package/src/commands/runner.ts +560 -276
  173. package/src/e2b/dispatch.ts +15 -1
  174. package/src/extensions/contract-types.generated.ts +1 -1
  175. package/src/fleet-benchmark.ts +157 -0
  176. package/src/heartbeat/heartbeat.ts +53 -135
  177. package/src/hooks/hook.ts +3 -2
  178. package/src/http/agents.ts +21 -0
  179. package/src/http/all-routes.ts +6 -0
  180. package/src/http/apps.ts +4 -2
  181. package/src/http/auth.ts +4 -7
  182. package/src/http/config.ts +58 -21
  183. package/src/http/controllers-connect.ts +206 -0
  184. package/src/http/core.ts +108 -14
  185. package/src/http/favorite-owner.ts +31 -1
  186. package/src/http/fleet-benchmark.ts +206 -0
  187. package/src/http/index.ts +38 -6
  188. package/src/http/mcp.ts +3 -8
  189. package/src/http/poll.ts +31 -2
  190. package/src/http/realtime.ts +1 -1
  191. package/src/http/request-principal.ts +2 -0
  192. package/src/http/runtime-pools.ts +527 -0
  193. package/src/http/session-data.ts +2 -0
  194. package/src/http/sessions.ts +100 -23
  195. package/src/http/skills.ts +173 -0
  196. package/src/http/status.ts +25 -1
  197. package/src/http/tasks.ts +14 -1
  198. package/src/http/telemetry-ui.ts +102 -0
  199. package/src/http/used-by.ts +140 -0
  200. package/src/http/utils.ts +7 -1
  201. package/src/http/workflows.ts +10 -2
  202. package/src/jira/app.ts +1 -2
  203. package/src/kv-reserved-namespaces.ts +1 -0
  204. package/src/linear/app.ts +1 -2
  205. package/src/providers/acp-target-catalog.ts +75 -1
  206. package/src/providers/acp-targets.ts +114 -0
  207. package/src/providers/amp-adapter.ts +1 -1
  208. package/src/providers/claude-adapter.ts +1 -1
  209. package/src/providers/codex-adapter.ts +5 -1
  210. package/src/providers/opencode-adapter.ts +2 -1
  211. package/src/providers/pi-mono-adapter.ts +41 -26
  212. package/src/providers/pricing-sources.md +2 -0
  213. package/src/providers/skill-invoke.ts +39 -3
  214. package/src/rbac/legacy-policy.ts +8 -0
  215. package/src/rbac/permissions.ts +16 -0
  216. package/src/rbac/types.ts +2 -0
  217. package/src/realtime/auth.ts +13 -0
  218. package/src/realtime/channels.ts +24 -0
  219. package/src/realtime/transport.ts +94 -15
  220. package/src/realtime/worker-client.ts +176 -0
  221. package/src/runtime-providers/controller-protocol.ts +97 -0
  222. package/src/runtime-providers/controller.ts +86 -0
  223. package/src/runtime-providers/docker.ts +235 -0
  224. package/src/runtime-providers/e2b.ts +217 -0
  225. package/src/runtime-providers/index.ts +37 -0
  226. package/src/runtime-providers/static.ts +26 -0
  227. package/src/runtime-providers/types.ts +92 -0
  228. package/src/runtime-providers/worker-api-url.ts +11 -0
  229. package/src/scripts-runtime/sdk-allowlist.ts +6 -0
  230. package/src/scripts-runtime/types/stdlib.d.ts +15 -0
  231. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  232. package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
  233. package/src/server.ts +12 -0
  234. package/src/slack/router.ts +7 -5
  235. package/src/tasks/worker-follow-up.ts +80 -52
  236. package/src/telemetry-context.ts +6 -0
  237. package/src/telemetry-contract/EVENTS.md +14 -1
  238. package/src/telemetry-contract/SOURCE.json +4 -4
  239. package/src/telemetry-contract/catalog.json +155 -0
  240. package/src/telemetry-contract/types.gen.ts +248 -4
  241. package/src/telemetry-ui-events.ts +130 -0
  242. package/src/telemetry.ts +43 -3
  243. package/src/tests/acp-adapter.test.ts +129 -0
  244. package/src/tests/claude-adapter.test.ts +6 -2
  245. package/src/tests/codex-adapter.test.ts +6 -2
  246. package/src/tests/config-internal-keys.test.ts +27 -0
  247. package/src/tests/config-session-auth.test.ts +19 -3
  248. package/src/tests/db-retention.test.ts +50 -10
  249. package/src/tests/docs-index-footprint.test.js +3 -3
  250. package/src/tests/e2b-dispatch.test.ts +28 -1
  251. package/src/tests/e2e-runtime-pools.test.ts +51 -0
  252. package/src/tests/entrypoint-acp-target-binary.test.ts +38 -1
  253. package/src/tests/favorites.test.ts +188 -0
  254. package/src/tests/fetch-resolved-env.test.ts +59 -1
  255. package/src/tests/fleet-benchmark.test.ts +453 -0
  256. package/src/tests/harness-model-support.test.ts +70 -2
  257. package/src/tests/heartbeat-orphan-resume.test.ts +24 -0
  258. package/src/tests/heartbeat-reboot-live.test.ts +31 -0
  259. package/src/tests/heartbeat-supersede-resume.test.ts +21 -0
  260. package/src/tests/hook-runtime-close.test.ts +64 -0
  261. package/src/tests/http-task-write-ownership.test.ts +8 -1
  262. package/src/tests/integration-disable-flags.test.ts +45 -0
  263. package/src/tests/migration-206-runtime-pools.test.ts +134 -0
  264. package/src/tests/migration-207-runtime-session-tokens.test.ts +45 -0
  265. package/src/tests/migration-208-task-runtime-dispatch.test.ts +30 -0
  266. package/src/tests/multi-runtime-registration.test.ts +219 -4
  267. package/src/tests/onboard-non-interactive.test.ts +104 -6
  268. package/src/tests/opencode-adapter.test.ts +6 -2
  269. package/src/tests/org-website.test.ts +115 -0
  270. package/src/tests/pi-mono-adapter.test.ts +74 -10
  271. package/src/tests/rbac-admission-e2e.test.ts +4 -4
  272. package/src/tests/rbac-engine.test.ts +8 -1
  273. package/src/tests/realtime-room-auth.test.ts +49 -0
  274. package/src/tests/repo-allow-merge-gate.test.ts +8 -1
  275. package/src/tests/runner-exit-after-tasks.test.ts +163 -0
  276. package/src/tests/runner-finish-retry.test.ts +466 -0
  277. package/src/tests/runner-harness-telemetry-env.test.ts +105 -0
  278. package/src/tests/runner-push-delivery.test.ts +670 -0
  279. package/src/tests/runner-slot-reconcile.test.ts +89 -1
  280. package/src/tests/runtime-child-token-cleanup.test.ts +172 -0
  281. package/src/tests/runtime-controller-token.test.ts +254 -0
  282. package/src/tests/runtime-demand.test.ts +119 -0
  283. package/src/tests/runtime-e2b-pools.test.ts +847 -0
  284. package/src/tests/runtime-pool-reachability.test.ts +328 -0
  285. package/src/tests/runtime-pool-routing.test.ts +817 -0
  286. package/src/tests/runtime-pools-db.test.ts +275 -0
  287. package/src/tests/runtime-pools-http.test.ts +506 -0
  288. package/src/tests/runtime-pools-ownership-guards.test.ts +235 -0
  289. package/src/tests/runtime-pools-seed.test.ts +203 -0
  290. package/src/tests/runtime-pools-test-utils.ts +128 -0
  291. package/src/tests/runtime-provider-registration.test.ts +133 -0
  292. package/src/tests/runtime-providers-docker.test.ts +400 -0
  293. package/src/tests/runtime-providers-e2b-transport.test.ts +50 -0
  294. package/src/tests/runtime-providers-e2b.test.ts +335 -0
  295. package/src/tests/runtime-recovery.test.ts +745 -0
  296. package/src/tests/runtime-remote-controller.test.ts +846 -0
  297. package/src/tests/runtime-supervisor-failures.test.ts +387 -0
  298. package/src/tests/runtime-supervisor.test.ts +1039 -0
  299. package/src/tests/runtime-token-auth.test.ts +897 -0
  300. package/src/tests/script-connections.test.ts +7 -0
  301. package/src/tests/secret-scrubber.test.ts +17 -2
  302. package/src/tests/session-costs-codex-recompute.test.ts +49 -0
  303. package/src/tests/session-tokens.test.ts +13 -3
  304. package/src/tests/skill-invoke.test.ts +142 -0
  305. package/src/tests/skill-usage-cohorts.test.ts +232 -0
  306. package/src/tests/skill-usage.test.ts +348 -0
  307. package/src/tests/slack-attachments-block.test.ts +23 -0
  308. package/src/tests/slack-render-v2.test.ts +6 -0
  309. package/src/tests/status-instance.test.ts +71 -0
  310. package/src/tests/task-supersede-resume.test.ts +87 -4
  311. package/src/tests/telemetry-enable.test.ts +113 -0
  312. package/src/tests/telemetry-ui-route.test.ts +195 -0
  313. package/src/tests/tool-annotations.test.ts +1 -1
  314. package/src/tests/used-by-route.test.ts +237 -0
  315. package/src/tests/worker-nudges.test.ts +653 -0
  316. package/src/tests/worker-realtime-client.test.ts +188 -0
  317. package/src/tests/workflow-sub-workflow.test.ts +22 -0
  318. package/src/tools/app-upsert.ts +1 -1
  319. package/src/tools/runtime-controller-list.ts +28 -0
  320. package/src/tools/runtime-pool-bind-agent.ts +29 -0
  321. package/src/tools/runtime-pool-common.ts +37 -0
  322. package/src/tools/runtime-pool-create-agent.ts +29 -0
  323. package/src/tools/runtime-pool-delete.ts +27 -0
  324. package/src/tools/runtime-pool-list.ts +27 -0
  325. package/src/tools/runtime-pool-upsert.ts +31 -0
  326. package/src/tools/script-common.ts +1 -1
  327. package/src/tools/send-task.ts +3 -2
  328. package/src/tools/tool-config.ts +6 -0
  329. package/src/types.ts +137 -2
  330. package/src/utils/config-secrets.ts +1 -0
  331. package/src/utils/lead-monitor-channels.ts +3 -0
  332. package/src/utils/org-website.ts +104 -0
  333. package/src/utils/request-auth-context.ts +10 -2
  334. package/src/utils/runtime-pools-flag.ts +11 -0
  335. package/src/utils/secret-scrubber.ts +9 -0
  336. package/src/utils/session-token-connections.ts +20 -0
  337. package/templates/skills/apps/content.md +1 -1
  338. package/templates/skills/code-quality/content.md +9 -1
  339. package/templates/skills/db-query-guidance/content.md +9 -0
  340. package/templates/skills/swarm-scripts/SKILL.md +1 -1
  341. package/templates/skills/swarm-scripts/content.md +1 -1
  342. package/templates/skills/work-on-task/SKILL.md +1 -1
  343. package/templates/skills/work-on-task/content.md +1 -1
  344. package/dist/{cli-qd97v6z2.js → cli-1qtgd1h1.js} +3 -3
  345. package/dist/{cli-cmxkbzzs.js → cli-j0w59p15.js} +6 -6
  346. package/dist/{google-generative-ai-b8zxrpax.js → google-generative-ai-pvhm0dq1.js} +1 -1
  347. package/dist/{google-vertex-nrdd697t.js → google-vertex-tek9sqe2.js} +4 -4
  348. package/dist/{mistral-conversations-059y51zt.js → mistral-conversations-rjxa9s1j.js} +3 -3
  349. package/dist/{openai-completions-663yncfq.js → openai-completions-44gb0zsk.js} +12 -12
package/src/http/core.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { z } from "zod";
2
3
  import { initAgentMail, resetAgentMail } from "../agentmail";
3
4
  import {
4
5
  initAzureDevOps,
@@ -17,17 +18,20 @@ import {
17
18
  updateAgentStatus,
18
19
  } from "../be/db";
19
20
  import {
21
+ getRuntimeInstanceById,
20
22
  markRuntimeInstanceOffline,
23
+ markRuntimeTerminated,
21
24
  reconcileAgentStatusFromRuntimes,
22
25
  touchRuntimeInstance,
23
26
  } from "../be/multi-runtime";
24
27
  import { enqueueAdmissionRow } from "../be/rbac-audit";
25
28
  import { getUserGrant } from "../be/rbac-roles";
29
+ import { syncTelemetryEnabled } from "../be/telemetry-enable";
26
30
  import { initGitHub, resetGitHub } from "../github";
27
31
  import { initGitLab, isGitLabEnabled, resetGitLab } from "../gitlab";
28
32
  import { initJira, resetJira } from "../jira";
29
33
  import { initLinear, resetLinear } from "../linear";
30
- import { decideAdmission, isRbacEnabled } from "../rbac";
34
+ import { can, decideAdmission, isRbacEnabled } from "../rbac";
31
35
  import { startSlackApp, stopSlackApp } from "../slack";
32
36
  import type { AgentStatus } from "../types";
33
37
  import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
@@ -40,7 +44,13 @@ import { refreshSecretScrubberCache, registerSensitiveKeyName } from "../utils/s
40
44
  import { resolveHttpRequestAuth } from "./auth";
41
45
  import { generateOpenApiSpec, SCALAR_HTML } from "./openapi";
42
46
  import { findRoute, isPublicRoute, route, runtimeInstanceHeader } from "./route-def";
43
- import { agentWithCapacity, getPathSegments, jsonError, parseQueryParams } from "./utils";
47
+ import {
48
+ agentWithCapacity,
49
+ duplicateGuardedQueryKey,
50
+ getPathSegments,
51
+ jsonError,
52
+ parseQueryParams,
53
+ } from "./utils";
44
54
 
45
55
  /**
46
56
  * Load global swarm_config entries into process.env.
@@ -159,6 +169,12 @@ async function reloadGlobalConfigsAndIntegrationsInner(): Promise<ReloadConfigRe
159
169
 
160
170
  const updated = await loadGlobalConfigsIntoEnv(true);
161
171
 
172
+ // ANONYMIZED_TELEMETRY may have just flipped on: mint the install ID now
173
+ // instead of dropping every event until a restart.
174
+ await syncTelemetryEnabled("settings").catch((err) => {
175
+ console.error("[config-reload] telemetry re-init failed (non-fatal):", err);
176
+ });
177
+
162
178
  // Retire the old transport before any other integration can fail during
163
179
  // reload. HTTP/invalid selection must never leave the previous socket live.
164
180
  await stopSlackApp();
@@ -380,17 +396,18 @@ const closeRoute = route({
380
396
  method: "post",
381
397
  path: "/close",
382
398
  pattern: ["close"],
383
- summary: "Mark an agent or runtime offline on shutdown",
399
+ summary: "End a session or terminate an agent runtime",
384
400
  description:
385
- `Retires the calling process. ${RUNTIME_HEADER_DOC} With multi-runtime mode on, the header ` +
386
- "is required and only that runtime is retired; the agent goes offline once no live runtime " +
387
- "remains. With the flag off, the agent is marked offline as before.",
401
+ `Closes the calling session. ${RUNTIME_HEADER_DOC} Pooled runtimes remain active unless ` +
402
+ "runtimeShutdown is true. With multi-runtime mode on, the header is required and static " +
403
+ "runtimes retire independently. With the flag off, static agents are marked offline.",
388
404
  tags: ["Core"],
389
405
  auth: { apiKey: true, agentId: true },
390
406
  headers: runtimeInstanceHeader("retire a runtime"),
407
+ body: z.object({ runtimeShutdown: z.boolean().optional() }).optional(),
391
408
  rbac: { ungated: "self-scoped: an agent retires its own runtime" },
392
409
  responses: {
393
- 204: { description: "Runtime (and agent, when last) marked offline" },
410
+ 204: { description: "Session closed or runtime retired" },
394
411
  400: {
395
412
  description: "Missing X-Agent-ID, or missing X-Runtime-Instance-ID in multi-runtime mode",
396
413
  },
@@ -409,6 +426,14 @@ export async function handleCore(
409
426
  // request pipeline does afterwards, including audit columns in the DB layer.
410
427
  beginRequestAuthScope();
411
428
 
429
+ // Controller credentials authorize only their separate WebSocket transport.
430
+ let auth = await resolveHttpRequestAuth(req, apiKey);
431
+ if (auth?.kind === "controller") {
432
+ setRequestAuth(req, auth);
433
+ jsonError(res, "Controller tokens can only connect to the controller socket", 403);
434
+ return true;
435
+ }
436
+
412
437
  // Handle preflight
413
438
  if (req.method === "OPTIONS") {
414
439
  res.writeHead(204);
@@ -455,14 +480,12 @@ export async function handleCore(
455
480
  // either the global swarm key or an active user-bound `aswt_` token.
456
481
  const pathSegments = getPathSegments(req.url || "");
457
482
  const isUserMcpRoute = req.url === "/mcp-user";
458
- let auth = null as Awaited<ReturnType<typeof resolveHttpRequestAuth>>;
459
483
  // `/mcp-user` runs its own `aswt_`-token auth in `handleMcpUser`; the swarm
460
484
  // API key must not gate it.
461
485
  if (isUserMcpRoute || isPublicRoute(req.method, pathSegments)) {
486
+ auth = null;
462
487
  setRequestAuth(req, null);
463
488
  } else {
464
- auth = await resolveHttpRequestAuth(req, apiKey);
465
-
466
489
  if (!auth) {
467
490
  setRequestAuth(req, null);
468
491
  res.writeHead(401, { "Content-Type": "application/json" });
@@ -472,6 +495,65 @@ export async function handleCore(
472
495
  setRequestAuth(req, auth);
473
496
  }
474
497
 
498
+ if (auth?.kind === "agent") {
499
+ const query = parseQueryParams(req.url || "");
500
+ const duplicate = duplicateGuardedQueryKey(query);
501
+ if (duplicate) {
502
+ jsonError(res, `Duplicate query parameter: ${duplicate}`, 400);
503
+ return true;
504
+ }
505
+ if (req.headers["x-page-id"] !== undefined) {
506
+ jsonError(res, "Agent tokens cannot send X-Page-Id", 403);
507
+ return true;
508
+ }
509
+ const header = req.headers["x-agent-id"];
510
+ const agentId = Array.isArray(header) ? header[0] : header;
511
+ if (agentId !== undefined && agentId !== auth.agentId) {
512
+ jsonError(res, "X-Agent-ID does not match token", 403);
513
+ return true;
514
+ }
515
+ req.headers["x-agent-id"] = auth.agentId;
516
+ myAgentId = auth.agentId;
517
+ if (auth.runtimeId) {
518
+ const header = req.headers["x-runtime-instance-id"];
519
+ const runtimeId = Array.isArray(header) ? header[0] : header;
520
+ if (runtimeId !== undefined && runtimeId !== auth.runtimeId) {
521
+ jsonError(res, "X-Runtime-Instance-ID does not match token", 403);
522
+ return true;
523
+ }
524
+ req.headers["x-runtime-instance-id"] = auth.runtimeId;
525
+ const sourceHeader = req.headers["x-source-task-id"];
526
+ const taskId = Array.isArray(sourceHeader) ? sourceHeader[0] : sourceHeader;
527
+ if (taskId !== undefined && (await getTaskById(taskId))?.agentId !== auth.agentId) {
528
+ jsonError(res, "X-Source-Task-Id does not belong to token agent", 403);
529
+ return true;
530
+ }
531
+ }
532
+ const targetAgentId = pathSegments[2];
533
+ if (
534
+ pathSegments[0] === "api" &&
535
+ pathSegments[1] === "agents" &&
536
+ targetAgentId &&
537
+ targetAgentId !== auth.agentId &&
538
+ (req.method !== "GET" ||
539
+ query.get("resolveSecrets") === "true" ||
540
+ query.get("includeSecrets") === "true")
541
+ ) {
542
+ const agent = await getAgentById(auth.agentId);
543
+ if (
544
+ !can({
545
+ principal: { kind: "agent", agentId: auth.agentId, isLead: agent?.isLead ?? false },
546
+ verb: "agent.act.any",
547
+ resource: { kind: "agent", agentId: targetAgentId },
548
+ source: "http",
549
+ }).allow
550
+ ) {
551
+ jsonError(res, "Token cannot access another agent's scope", 403);
552
+ return true;
553
+ }
554
+ }
555
+ }
556
+
475
557
  if (auth?.kind === "user" && isRbacEnabled()) {
476
558
  const grant = await getUserGrant(auth.userId);
477
559
  if (!grant.grantsAll) {
@@ -682,6 +764,14 @@ export async function handleCore(
682
764
  return true;
683
765
  }
684
766
 
767
+ const parsed = await closeRoute.parse(
768
+ req,
769
+ res,
770
+ getPathSegments(req.url || ""),
771
+ new URLSearchParams(),
772
+ );
773
+ if (!parsed) return true;
774
+
685
775
  const found = await getDbClient().transaction(async () => {
686
776
  const agent = await getAgentById(myAgentId);
687
777
 
@@ -689,10 +779,14 @@ export async function handleCore(
689
779
  return false;
690
780
  }
691
781
 
692
- if (multiRuntime && runtimeInstanceId) {
693
- // One process exiting retires only its own runtime; the logical state
694
- // is then recomputed from whatever is left — offline when nothing is,
695
- // otherwise reflecting the surviving runtimes' readiness and work.
782
+ const runtime = runtimeInstanceId ? await getRuntimeInstanceById(runtimeInstanceId) : null;
783
+ if (runtime?.agentId === agent.id && runtime.poolId) {
784
+ // Session hooks do not end the runtime that owns their sessions.
785
+ if (parsed.body?.runtimeShutdown) {
786
+ await markRuntimeTerminated(runtime.id, "Runner shutdown");
787
+ }
788
+ } else if (multiRuntime && runtimeInstanceId) {
789
+ // Static runtimes retain independent close behavior.
696
790
  await markRuntimeInstanceOffline(runtimeInstanceId, agent.id);
697
791
  await reconcileAgentStatusFromRuntimes(agent.id);
698
792
  } else {
@@ -1,5 +1,6 @@
1
1
  import type { IncomingMessage } from "node:http";
2
2
  import { resolveHttpAuditUserId } from "../be/audit-user";
3
+ import { findUserById } from "../be/users";
3
4
  import { getRequestAuth } from "../utils/request-auth-context";
4
5
 
5
6
  export type FavoriteOwner = {
@@ -8,6 +9,19 @@ export type FavoriteOwner = {
8
9
  actorId: string;
9
10
  };
10
11
 
12
+ /**
13
+ * Header the dashboard sends with the identity picked in its user picker.
14
+ * The dashboard authenticates with the shared operator key, so without it
15
+ * every person using the dashboard would share one favorite set.
16
+ */
17
+ export const DASHBOARD_USER_HEADER = "x-swarm-user-id";
18
+
19
+ function dashboardUserIdHeader(req: IncomingMessage): string | null {
20
+ const raw = req.headers[DASHBOARD_USER_HEADER];
21
+ const value = (Array.isArray(raw) ? raw[0] : raw)?.trim();
22
+ return value ? value : null;
23
+ }
24
+
11
25
  /**
12
26
  * Resolve the authenticated principal that owns favorite state.
13
27
  *
@@ -16,20 +30,36 @@ export type FavoriteOwner = {
16
30
  * clients authenticate with user-bound `aswt_` tokens. Agent traffic must be
17
31
  * resolved through its owned task before the transport-level operator auth is
18
32
  * allowed to select the dashboard's shared scope.
33
+ *
34
+ * An operator-key dashboard request names its picked user in
35
+ * `X-Swarm-User-Id`; an active user there owns the favorites. The operator key
36
+ * is fully trusted, so the header needs no further proof. A user token or a
37
+ * page session never honors the header. `dashboardUser: false` keeps the
38
+ * shared operator scope for callers whose stored state predates the header.
19
39
  */
20
40
  export async function resolveHttpFavoriteOwner(
21
41
  req: IncomingMessage,
22
42
  callerAgentId: string | undefined,
43
+ opts: { dashboardUser?: boolean } = {},
23
44
  ): Promise<FavoriteOwner | null> {
24
45
  const auth = getRequestAuth(req);
25
46
  if (auth?.kind === "user") {
26
47
  return { scope: `user:${auth.userId}`, userId: auth.userId, actorId: auth.userId };
27
48
  }
28
49
  if (auth?.kind === "operator" && !callerAgentId) {
50
+ const pickedUserId =
51
+ opts.dashboardUser === false || auth.page ? null : dashboardUserIdHeader(req);
52
+ if (pickedUserId) {
53
+ const user = await findUserById(pickedUserId);
54
+ if (user?.status === "active") {
55
+ return { scope: `user:${user.id}`, userId: user.id, actorId: user.id };
56
+ }
57
+ }
29
58
  return {
30
59
  // A deployment has one configured operator key. Keep its dashboard
31
60
  // favorites stable across key rotation; the fingerprint remains the
32
- // audit actor, not the storage scope.
61
+ // audit actor, not the storage scope. Tabs with no picked identity
62
+ // land here.
33
63
  scope: "operator",
34
64
  userId: null,
35
65
  actorId: auth.fingerprint,
@@ -0,0 +1,206 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { z } from "zod";
3
+ import { getSwarmConfigs } from "../be/db";
4
+ import { getLocalFleetStats } from "../be/fleet-benchmark-stats";
5
+ import {
6
+ compareWithFleet,
7
+ FLEET_MIN_FINISHED_TASKS,
8
+ type FleetBenchmarks,
9
+ fleetBucketFor,
10
+ type LocalFleetStats,
11
+ localSuccessRate,
12
+ localTasksPerWeek,
13
+ } from "../fleet-benchmark";
14
+ import { isTelemetryEnabled } from "../telemetry";
15
+ import { rejectGuest } from "./request-principal";
16
+ import { route } from "./route-def";
17
+ import { telemetryProxyEndpoint } from "./used-by";
18
+
19
+ /** The fleet changes slowly; one proxy call a day per install is plenty. */
20
+ const FLEET_TTL_MS = 24 * 60 * 60 * 1000;
21
+ /** After a failed call, wait before asking the proxy again. */
22
+ const FLEET_FAILURE_TTL_MS = 60 * 60 * 1000;
23
+ /** Local counts scan agent_tasks; the card does not need them fresher. */
24
+ const LOCAL_TTL_MS = 10 * 60 * 1000;
25
+ const TIMEOUT_MS = 10_000;
26
+
27
+ const FleetStateSchema = z.enum([
28
+ "ready",
29
+ "telemetry_off",
30
+ "no_org_name",
31
+ "not_enough_tasks",
32
+ "unavailable",
33
+ ]);
34
+
35
+ const FleetBenchmarkResponseSchema = z.object({
36
+ state: FleetStateSchema,
37
+ local: z
38
+ .object({
39
+ tasksPerWeek: z.number(),
40
+ taskSuccessRate: z.number().nullable(),
41
+ finishedTasks: z.number().int(),
42
+ agents: z.number().int(),
43
+ bucket: z.enum(["1", "2-5", "6+"]).nullable(),
44
+ })
45
+ .nullable(),
46
+ minFinishedTasks: z.number().int(),
47
+ generatedAt: z.string().nullable(),
48
+ comparisons: z.array(
49
+ z.object({
50
+ key: z.enum(["tasksPerWeek", "taskSuccessRate"]),
51
+ value: z.number(),
52
+ p50: z.number(),
53
+ band: z.enum(["top_10", "top_25", "above_median", "below_median", "bottom_25"]),
54
+ peers: z.enum(["1", "2-5", "6+", "all"]),
55
+ installs: z.number().int(),
56
+ }),
57
+ ),
58
+ });
59
+
60
+ export type FleetBenchmarkResponse = z.infer<typeof FleetBenchmarkResponseSchema>;
61
+
62
+ // ─── Route Definitions ───────────────────────────────────────────────────────
63
+
64
+ const fleetBenchmark = route({
65
+ method: "get",
66
+ path: "/api/fleet-benchmark",
67
+ pattern: ["api", "fleet-benchmark"],
68
+ summary: "Compare this swarm with similar swarms",
69
+ description:
70
+ "Places this swarm's tasks per week and task success rate (last 30 days) in the fleet percentiles published by the telemetry proxy, against swarms with the same agent count when enough exist. Needs telemetry on and an organization name; otherwise `state` says what is missing and the proxy is not called. `unavailable` also covers an install whose first event the proxy saw cannot be established locally (until 30 days after the first accepted event). The fleet numbers are cached for a day.",
71
+ tags: ["Telemetry"],
72
+ responses: {
73
+ 200: {
74
+ description: "Benchmark, or the reason it is not shown",
75
+ schema: FleetBenchmarkResponseSchema,
76
+ },
77
+ 401: { description: "Unauthorized" },
78
+ 403: { description: "Guest page sessions cannot read the benchmark" },
79
+ },
80
+ });
81
+
82
+ // ─── Caches ──────────────────────────────────────────────────────────────────
83
+
84
+ let fleetCache: { value: FleetBenchmarks | null; until: number } | null = null;
85
+ let localCache: { value: LocalFleetStats | null; until: number } | null = null;
86
+
87
+ const ProxyMetricSchema = z.object({
88
+ n: z.number(),
89
+ p25: z.number().optional(),
90
+ p50: z.number().optional(),
91
+ p75: z.number().optional(),
92
+ p90: z.number().optional(),
93
+ omitted: z.string().optional(),
94
+ });
95
+ const ProxyBlockSchema = z.object({
96
+ installs: z.number(),
97
+ tasksPerWeek: ProxyMetricSchema,
98
+ taskSuccessRate: ProxyMetricSchema,
99
+ });
100
+ /**
101
+ * Everything `compareWithFleet` reads from `GET /v1/fleet/benchmarks`. A body
102
+ * that fails it counts as a failed call, so a malformed 200 is never cached.
103
+ */
104
+ const ProxyFleetBenchmarksSchema = z.object({
105
+ window: z.string(),
106
+ generatedAt: z.string(),
107
+ minInstallsPerBucket: z.number(),
108
+ minFinishedTasks: z.number(),
109
+ overall: ProxyBlockSchema,
110
+ byAgentCount: z.object({
111
+ "1": ProxyBlockSchema.optional(),
112
+ "2-5": ProxyBlockSchema.optional(),
113
+ "6+": ProxyBlockSchema.optional(),
114
+ }),
115
+ }) satisfies z.ZodType<FleetBenchmarks>;
116
+
117
+ /** The proxy's fleet percentiles. The request carries no install ID. */
118
+ async function fleetBenchmarks(now: number): Promise<FleetBenchmarks | null> {
119
+ if (fleetCache && fleetCache.until > now) return fleetCache.value;
120
+ let value: FleetBenchmarks | null = null;
121
+ try {
122
+ const response = await fetch(
123
+ `${await telemetryProxyEndpoint("/v1/fleet/benchmarks")}?window=30d`,
124
+ {
125
+ signal: AbortSignal.timeout(TIMEOUT_MS),
126
+ },
127
+ );
128
+ if (response.ok) {
129
+ const parsed = ProxyFleetBenchmarksSchema.safeParse(await response.json());
130
+ if (parsed.success) value = parsed.data;
131
+ }
132
+ } catch {
133
+ // Unreachable proxy: the card says the comparison is unavailable.
134
+ }
135
+ fleetCache = { value, until: now + (value ? FLEET_TTL_MS : FLEET_FAILURE_TTL_MS) };
136
+ return value;
137
+ }
138
+
139
+ async function localStats(now: Date): Promise<LocalFleetStats | null> {
140
+ if (localCache && localCache.until > now.getTime()) return localCache.value;
141
+ const value = await getLocalFleetStats(now);
142
+ localCache = { value, until: now.getTime() + LOCAL_TTL_MS };
143
+ return value;
144
+ }
145
+
146
+ async function orgName(): Promise<string | undefined> {
147
+ const stored = (
148
+ await getSwarmConfigs({ scope: "global", key: "SWARM_ORG_NAME" })
149
+ )?.[0]?.value?.trim();
150
+ return stored || process.env.SWARM_ORG_NAME?.trim() || undefined;
151
+ }
152
+
153
+ export async function buildFleetBenchmarkResponse(
154
+ now = new Date(),
155
+ ): Promise<FleetBenchmarkResponse> {
156
+ const empty = {
157
+ local: null,
158
+ minFinishedTasks: FLEET_MIN_FINISHED_TASKS,
159
+ generatedAt: null,
160
+ comparisons: [],
161
+ };
162
+ // Telemetry off means no call to our proxy at all, not even an anonymous one.
163
+ if (!isTelemetryEnabled()) return { state: "telemetry_off", ...empty };
164
+ if (!(await orgName())) return { state: "no_org_name", ...empty };
165
+
166
+ const stats = await localStats(now);
167
+ // Where the proxy started counting this install is unknown: no comparison.
168
+ if (!stats) return { state: "unavailable", ...empty };
169
+ const finishedTasks = stats.tasksCompleted + stats.tasksFailed;
170
+ const local = {
171
+ tasksPerWeek: localTasksPerWeek(stats, now),
172
+ taskSuccessRate: localSuccessRate(stats),
173
+ finishedTasks,
174
+ agents: stats.agents,
175
+ bucket: fleetBucketFor(stats.agents),
176
+ };
177
+ if (finishedTasks < FLEET_MIN_FINISHED_TASKS)
178
+ return { ...empty, state: "not_enough_tasks", local };
179
+
180
+ const fleet = await fleetBenchmarks(now.getTime());
181
+ const comparisons = fleet ? compareWithFleet(stats, fleet, now) : [];
182
+ if (!fleet || comparisons.length === 0) return { ...empty, state: "unavailable", local };
183
+ return {
184
+ state: "ready",
185
+ local,
186
+ minFinishedTasks: FLEET_MIN_FINISHED_TASKS,
187
+ generatedAt: fleet.generatedAt,
188
+ comparisons,
189
+ };
190
+ }
191
+
192
+ // ─── Handler ─────────────────────────────────────────────────────────────────
193
+
194
+ export async function handleFleetBenchmark(
195
+ req: IncomingMessage,
196
+ res: ServerResponse,
197
+ pathSegments: string[],
198
+ queryParams: URLSearchParams,
199
+ ): Promise<boolean> {
200
+ if (!fleetBenchmark.match(req.method, pathSegments)) return false;
201
+ if (rejectGuest(req, res)) return true;
202
+ const parsed = await fleetBenchmark.parse(req, res, pathSegments, queryParams);
203
+ if (!parsed) return true;
204
+ fleetBenchmark.respond(res, 200, await buildFleetBenchmarkResponse());
205
+ return true;
206
+ }
package/src/http/index.ts CHANGED
@@ -25,9 +25,14 @@ import {
25
25
  stopAuditGc,
26
26
  stopAuditWriter,
27
27
  } from "../be/rbac-audit";
28
+ import { startPoolRouting, stopPoolRouting } from "../be/runtime-pools/routing";
29
+ import { seedRuntimePools } from "../be/runtime-pools/seed";
30
+ import { startRuntimeSupervisor, stopRuntimeSupervisor } from "../be/runtime-pools/supervisor";
28
31
  import { startScratchScriptGc, stopScratchScriptGc } from "../be/scripts/retention";
29
32
  import { loadSecretRegistry } from "../be/secret-registry";
30
33
  import { seedLegacyCapabilitiesConfig } from "../be/seed-capabilities";
34
+ import { initApiTelemetry } from "../be/telemetry-enable";
35
+ import { startWorkerNudgeListener } from "../be/worker-nudge-listener";
31
36
  import {
32
37
  loadEnabledExtensions,
33
38
  setExtensionLoopbackBaseUrl,
@@ -53,7 +58,7 @@ import { attachRealtimeTransport } from "../realtime/transport";
53
58
  import { startScriptRunSupervisor, stopScriptRunSupervisor } from "../script-workflows/supervisor";
54
59
  import { getServerSessionsProcessed } from "../server-runtime-counters";
55
60
  import { startSlackApp, stopSlackApp } from "../slack";
56
- import { initTelemetry, telemetry } from "../telemetry";
61
+ import { telemetry } from "../telemetry";
57
62
  import { startTelemetryTicker } from "../telemetry-snapshot";
58
63
  import { API_DRAINING_HEADER } from "../utils/api-drain";
59
64
  import { getApiKey } from "../utils/api-key";
@@ -80,6 +85,7 @@ import { handleEcosystem } from "./ecosystem";
80
85
  import { handleEvents } from "./events";
81
86
  import { handleExtensions } from "./extensions";
82
87
  import { handleFavorites } from "./favorites";
88
+ import { handleFleetBenchmark } from "./fleet-benchmark";
83
89
  import { handleFs } from "./fs";
84
90
  import { handleHeartbeat } from "./heartbeat";
85
91
  import { handleInboxState } from "./inbox-state";
@@ -113,6 +119,7 @@ import { handleRealtimeAsset, handleRealtimeTicket } from "./realtime";
113
119
  import { handleRepos } from "./repos";
114
120
  import { handleRooms } from "./rooms";
115
121
  import { describeRequestRoute } from "./route-def";
122
+ import { handleRuntimePools } from "./runtime-pools";
116
123
  import { handleSchedules } from "./schedules";
117
124
  import { handleScriptConnectionProxy } from "./script-connection-proxy";
118
125
  import { handleScriptConnections } from "./script-connections";
@@ -126,7 +133,9 @@ import { handleStats } from "./stats";
126
133
  import { handleStatus } from "./status";
127
134
  import { handleTaskTemplates } from "./task-templates";
128
135
  import { handleTasks } from "./tasks";
136
+ import { handleTelemetryUi } from "./telemetry-ui";
129
137
  import { handleTrackers } from "./trackers";
138
+ import { handleUsedBy } from "./used-by";
130
139
  import { handleUsers } from "./users";
131
140
  import {
132
141
  getPathSegments,
@@ -329,7 +338,7 @@ const httpServer = createHttpServer(async (req, res) => {
329
338
  if (isApiDraining()) res.setHeader(API_DRAINING_HEADER, "1");
330
339
 
331
340
  const queryParams = parseQueryParams(req.url || "");
332
- const myAgentId = req.headers["x-agent-id"] as string | undefined;
341
+ let myAgentId = req.headers["x-agent-id"] as string | undefined;
333
342
 
334
343
  // ── Route handlers (order matters — first match wins) ──
335
344
  const handlers: (() => Promise<boolean>)[] = [
@@ -389,11 +398,15 @@ const httpServer = createHttpServer(async (req, res) => {
389
398
  () => handleApiKeys(req, res, pathSegments, queryParams),
390
399
  () => handleHeartbeat(req, res, pathSegments),
391
400
  () => handleEvents(req, res, pathSegments, queryParams, myAgentId),
401
+ () => handleRuntimePools(req, res, pathSegments, queryParams, myAgentId),
392
402
  () => handleExtensions(req, res, pathSegments, queryParams, myAgentId),
393
403
  () => handleFavorites(req, res, pathSegments, queryParams, myAgentId),
394
404
  () => handleUsers(req, res, pathSegments, queryParams),
395
405
  () => handleSessions(req, res, pathSegments, queryParams),
396
406
  () => handleInboxState(req, res, pathSegments, queryParams),
407
+ () => handleTelemetryUi(req, res, pathSegments, queryParams, myAgentId),
408
+ () => handleUsedBy(req, res, pathSegments, queryParams, myAgentId),
409
+ () => handleFleetBenchmark(req, res, pathSegments, queryParams),
397
410
  () => handleTaskTemplates(req, res, pathSegments, queryParams),
398
411
  () => handleMcp(req, res, transports, transportActivity, mcpSessionAgents),
399
412
  () => handleMcpUser(req, res, transportsUser, sessionUsers, transportActivityUser),
@@ -406,6 +419,7 @@ const httpServer = createHttpServer(async (req, res) => {
406
419
  // the async listener as an unhandled rejection and the client got a
407
420
  // dropped connection instead of a 500.
408
421
  if (await handleCore(req, res, myAgentId, apiKey)) return;
422
+ myAgentId = req.headers["x-agent-id"] as string | undefined;
409
423
 
410
424
  for (const handler of handlers) {
411
425
  if (await handler()) return;
@@ -433,6 +447,9 @@ const httpServer = createHttpServer(async (req, res) => {
433
447
 
434
448
  // Store references in globalThis for hot reload persistence
435
449
  const detachRealtime = attachRealtimeTransport(httpServer);
450
+ const { attachControllerTransport } = await import("./controllers-connect");
451
+ const detachControllers = attachControllerTransport(httpServer);
452
+ const stopWorkerNudges = startWorkerNudgeListener();
436
453
  const stopRoomDeletion = realtimeBus.subscribe("room:namespace-deleted", (namespace) => {
437
454
  void Promise.resolve(removeNamespaceRooms(String(namespace))).catch((error) =>
438
455
  console.error("[rooms] Deletion failed:", scrubSecrets(String(error))),
@@ -440,6 +457,8 @@ const stopRoomDeletion = realtimeBus.subscribe("room:namespace-deleted", (namesp
440
457
  });
441
458
  globalState.__closeRealtime = () => {
442
459
  detachRealtime();
460
+ detachControllers();
461
+ stopWorkerNudges();
443
462
  stopRoomDeletion();
444
463
  };
445
464
  globalState.__httpServer = httpServer;
@@ -468,6 +487,8 @@ async function shutdown() {
468
487
 
469
488
  // Stop heartbeat triage
470
489
  stopHeartbeat();
490
+ stopPoolRouting();
491
+ await stopRuntimeSupervisor();
471
492
 
472
493
  // Stop the out-of-band queue alarm before disconnecting its Slack notifier.
473
494
  stopQueueStallAlarm();
@@ -599,7 +620,7 @@ warnIfCorsAllowsAnyOrigin();
599
620
  try {
600
621
  const loaded = await loadSecretRegistry();
601
622
  console.log(
602
- `[secret-registry] registered config=${loaded.config} oauth=${loaded.oauth} scriptApi=${loaded.scriptApi} failed=${loaded.failed}`,
623
+ `[secret-registry] registered config=${loaded.config} oauth=${loaded.oauth} scriptApi=${loaded.scriptApi} runtimeController=${loaded.runtimeController} failed=${loaded.failed}`,
603
624
  );
604
625
  } catch (err) {
605
626
  console.error(
@@ -608,6 +629,15 @@ try {
608
629
  );
609
630
  }
610
631
 
632
+ try {
633
+ await seedRuntimePools();
634
+ } catch (err) {
635
+ console.error(
636
+ "[startup] Failed to seed runtime pools:",
637
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
638
+ );
639
+ }
640
+
611
641
  // Upgrade seed: explicit CAPABILITIES env values that predate capability
612
642
  // gating get the previously always-registered groups backfilled into a
613
643
  // global swarm_config row (operator-editable; skipped when a row exists).
@@ -712,9 +742,8 @@ httpServer
712
742
  const telemetrySetConfig = async (key: string, value: string) => {
713
743
  await upsertSwarmConfig({ scope: "global", key, value });
714
744
  };
715
- await initTelemetry("api-server", telemetryGetConfig, telemetrySetConfig, {
716
- generateIfMissing: true,
717
- });
745
+ // Also keeps the accessors so turning telemetry on at runtime re-inits it.
746
+ await initApiTelemetry(telemetryGetConfig, telemetrySetConfig);
718
747
  telemetry.server("started", { port });
719
748
  // Org email domain (hourly + on user changes) and the daily org.snapshot.
720
749
  startTelemetryTicker({ getConfig: telemetryGetConfig, setConfig: telemetrySetConfig });
@@ -780,6 +809,9 @@ httpServer
780
809
  startHeartbeat(heartbeatMs);
781
810
  }
782
811
 
812
+ startPoolRouting();
813
+ startRuntimeSupervisor();
814
+
783
815
  // Start OAuth token keepalive (proactive refresh to prevent expiry)
784
816
  if (process.env.OAUTH_KEEPALIVE_DISABLE !== "true") {
785
817
  const { startOAuthKeepalive } = await import("../oauth/keepalive");
package/src/http/mcp.ts CHANGED
@@ -92,20 +92,15 @@ function forbidden(res: ServerResponse, message: string): true {
92
92
 
93
93
  /**
94
94
  * When a request is authenticated via an aseph_ session token (kind: "agent"),
95
- * the X-Agent-ID and X-Source-Task-Id headers MUST match the token's bound
96
- * agentId and taskId. A valid token cannot be used on behalf of a different
97
- * agent or task.
95
+ * the X-Source-Task-Id header must match a task-bound token.
96
+ * Central HTTP authentication enforces the agent and runtime headers.
98
97
  */
99
98
  function validateAgentTokenIdentity(req: IncomingMessage, res: ServerResponse): true | undefined {
100
99
  const auth = getRequestAuth(req);
101
- if (!auth || auth.kind !== "agent") return undefined;
100
+ if (!auth || auth.kind !== "agent" || auth.taskId === null) return undefined;
102
101
 
103
- const agentId = headerValue(req.headers["x-agent-id"]);
104
102
  const taskId = headerValue(req.headers["x-source-task-id"]);
105
103
 
106
- if (!agentId || agentId !== auth.agentId) {
107
- return forbidden(res, "X-Agent-ID does not match session token");
108
- }
109
104
  if (!taskId || taskId !== auth.taskId) {
110
105
  return forbidden(res, "X-Source-Task-Id does not match session token");
111
106
  }