@desplega.ai/agent-swarm 2.0.0 → 2.1.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 (238) hide show
  1. package/dist/{acp-adapter-sj2g0m84.js → acp-adapter-kr4abrsk.js} +10 -10
  2. package/dist/{actions-fegk44gr.js → actions-eh4g6w2r.js} +27 -27
  3. package/dist/{amp-adapter-ad8ft89x.js → amp-adapter-7h7wdt8q.js} +5 -5
  4. package/dist/{app-kfvgbvzy.js → app-7k8s9w5p.js} +9 -9
  5. package/dist/{assistant-z7zdy9ja.js → assistant-wswt2y7p.js} +28 -28
  6. package/dist/{boot-reembed-j8xa181e.js → boot-reembed-9apm9qw6.js} +7 -7
  7. package/dist/{boot-reembed-5c2z0mbe.js → boot-reembed-whzbdthj.js} +7 -7
  8. package/dist/{boot-scrub-sweep-hnkn3rgt.js → boot-scrub-sweep-ndzmvmgt.js} +8 -8
  9. package/dist/{boot-seeds-kwjq4d2z.js → boot-seeds-e9sta0yt.js} +13 -11
  10. package/dist/{claude-adapter-9vmvpvgd.js → claude-adapter-4mqd3r6r.js} +11 -11
  11. package/dist/{claude-managed-adapter-k9qae05j.js → claude-managed-adapter-9wswb52m.js} +3 -3
  12. package/dist/{claude-managed-setup-q54zx5q2.js → claude-managed-setup-tpe1effx.js} +4 -4
  13. package/dist/{claude-sdk-session-9jye6cxr.js → claude-sdk-session-q49ghmh9.js} +10340 -10197
  14. package/dist/{cli-ke896fcf.js → cli-01xvj1rq.js} +10 -10
  15. package/dist/{cli-tr4sxp68.js → cli-035vekcw.js} +2 -2
  16. package/dist/{cli-z5jjpaxw.js → cli-0pd8hzhc.js} +3 -3
  17. package/dist/{cli-n5vwg842.js → cli-0veb92a3.js} +6 -1
  18. package/dist/{cli-wayw93nt.js → cli-10jesr25.js} +3 -3
  19. package/dist/{cli-se8w6wny.js → cli-1y7yywmj.js} +1 -1
  20. package/dist/{cli-v1yb0rqb.js → cli-20krdhz5.js} +47 -19
  21. package/dist/{cli-d5f9ed5c.js → cli-29rja109.js} +39 -20
  22. package/dist/{cli-apx87jne.js → cli-2pwa0q4d.js} +15 -4
  23. package/dist/{cli-ee9gtz70.js → cli-4mengw46.js} +348 -25
  24. package/dist/{cli-03gedhng.js → cli-5twdke5x.js} +6 -6
  25. package/dist/{cli-6razj6t4.js → cli-711vwbxn.js} +2 -2
  26. package/dist/{cli-s2sp957d.js → cli-78vv7446.js} +1 -1
  27. package/dist/{cli-3fbmmawm.js → cli-7n6s5man.js} +4 -4
  28. package/dist/{cli-hnhtk6cj.js → cli-8wwxw8a3.js} +1 -1
  29. package/dist/{cli-hnmhtzvh.js → cli-9jzdj90z.js} +2 -0
  30. package/dist/{cli-skxvvpap.js → cli-cvv9k0hh.js} +28 -25
  31. package/dist/{cli-74ahz61x.js → cli-d9286m9h.js} +2 -2
  32. package/dist/{cli-4jxm2qtz.js → cli-gxzkg1de.js} +28 -16
  33. package/dist/{cli-zkpks3r3.js → cli-h1arqn31.js} +1 -1
  34. package/dist/{cli-r62a35ea.js → cli-hqr88ew6.js} +1 -1
  35. package/dist/{cli-ra0xzggj.js → cli-j67mh2vg.js} +2 -2
  36. package/dist/{cli-29jc5q7w.js → cli-ksphgh48.js} +2 -2
  37. package/dist/{cli-eejsgvm4.js → cli-mgz4nbgg.js} +4 -4
  38. package/dist/{cli-vatyhshy.js → cli-mz5as5ha.js} +4 -4
  39. package/dist/{cli-f7kr7svz.js → cli-n8ndw8j7.js} +2 -2
  40. package/dist/{cli-1e4b6bzh.js → cli-naqzr17j.js} +7 -7
  41. package/dist/{cli-q0ke1yww.js → cli-nm1p8jfr.js} +6 -6
  42. package/dist/{cli-m50t4309.js → cli-p3zbc1rw.js} +5 -5
  43. package/dist/{cli-cgtksawx.js → cli-pe05f8yk.js} +23 -3
  44. package/dist/{cli-vdach5a9.js → cli-qmzsbkvy.js} +4 -4
  45. package/dist/{cli-n3hzg89b.js → cli-qnm4e9e4.js} +57 -579
  46. package/dist/{cli-s4setq91.js → cli-qqy4q48b.js} +3 -3
  47. package/dist/{cli-k382tshb.js → cli-r35rw5de.js} +8 -7
  48. package/dist/{cli-k3dp5tpf.js → cli-r610p9bs.js} +2 -2
  49. package/dist/{cli-e6mpjhed.js → cli-rc6pf5kv.js} +1 -1
  50. package/dist/{cli-3dj8k4vy.js → cli-rdxm77k3.js} +1 -1
  51. package/dist/{cli-tqaazemc.js → cli-s69nfyeh.js} +1 -1
  52. package/dist/{cli-6jyj59nb.js → cli-sawv63e8.js} +2 -2
  53. package/dist/{cli-q50942rs.js → cli-sg19gbf0.js} +1 -1
  54. package/dist/{cli-p1x85d2t.js → cli-shk7r5cs.js} +12 -9
  55. package/dist/{cli-przpmdgr.js → cli-sksetb2q.js} +11 -1
  56. package/dist/{cli-d2qr7aj4.js → cli-t02z59f2.js} +1 -1
  57. package/dist/{cli-j0pfptdp.js → cli-v7g551te.js} +39 -43
  58. package/dist/{cli-bzszjf2p.js → cli-vgqzp4em.js} +519 -126
  59. package/dist/cli-wfnemr1t.js +86 -0
  60. package/dist/{cli-ne236afy.js → cli-wms6x9db.js} +8 -8
  61. package/dist/{cli-5hkm7whv.js → cli-x1r545x8.js} +1 -1
  62. package/dist/{cli-kt6zqg0e.js → cli-xczsgshn.js} +2 -2
  63. package/dist/{cli-ef1hg1wv.js → cli-y3167wt1.js} +430 -238
  64. package/dist/{cli-tsde1hdr.js → cli-yjmqpxt3.js} +12 -11
  65. package/dist/{cli-9nfx44eh.js → cli-z780ec7c.js} +1 -1
  66. package/dist/{cli-r2pnvj8g.js → cli-zm9t2vde.js} +2 -2
  67. package/dist/cli.js +24 -23
  68. package/dist/{codex-adapter-y81sssk0.js → codex-adapter-4z16v9n0.js} +8 -8
  69. package/dist/{codex-login-f2rcfpp9.js → codex-login-pkepy2j1.js} +2 -2
  70. package/dist/{codex-session-runner-8vx4j6a9.js → codex-session-runner-rvms9y2x.js} +8 -8
  71. package/dist/{commands-kcpvpcrr.js → commands-2a9rtyat.js} +8 -8
  72. package/dist/{controller-ywrmt6yd.js → controller-3k3madbn.js} +4 -4
  73. package/dist/{controllers-connect-kwpvj3rf.js → controllers-connect-v9yn8zb9.js} +30 -30
  74. package/dist/{cursor-adapter-cehv1qns.js → cursor-adapter-98gbhyzn.js} +4 -4
  75. package/dist/{db-ww5d7jq3.js → db-eywwsnjw.js} +9 -7
  76. package/dist/{db-k8ankj20.js → db-ncypk9np.js} +9 -9
  77. package/dist/{devin-adapter-x1022qb1.js → devin-adapter-zeq8b9gz.js} +2 -2
  78. package/dist/{dispatcher-e8gq5m68.js → dispatcher-w94kc575.js} +26 -26
  79. package/dist/{docker-ff6y1190.js → docker-b1e4cqab.js} +2 -2
  80. package/dist/{dsh-adapter-3epjm3af.js → dsh-adapter-47ztej36.js} +4 -4
  81. package/dist/{e2b-8ygwhg0f.js → e2b-bbt8ey45.js} +3 -3
  82. package/dist/{e2b-swhnpw3n.js → e2b-dp804sen.js} +3 -3
  83. package/dist/{event-bus-6axnngdj.js → event-bus-j2jdg3qb.js} +2 -2
  84. package/dist/{external-channel-h3359cwn.js → external-channel-zrkg8ar9.js} +8 -8
  85. package/dist/{grok-adapter-0d4x7gkx.js → grok-adapter-s9ncp9vy.js} +11 -11
  86. package/dist/{handlers-an1gzv21.js → handlers-kqftvjgw.js} +29 -29
  87. package/dist/{heartbeat-6dakeb61.js → heartbeat-9q96p8v9.js} +31 -31
  88. package/dist/{hook-6v6cv6d5.js → hook-56qe10dp.js} +7 -7
  89. package/dist/{hook-sa5xye4y.js → hook-yy6rw42k.js} +8 -8
  90. package/dist/{http-r7f3eky0.js → http-r2dnb8f8.js} +963 -75
  91. package/dist/{index-7ep4gzdk.js → index-3gtmkhc6.js} +26 -26
  92. package/dist/{index-v1v6n7hg.js → index-3k5hkz7r.js} +31 -31
  93. package/dist/{index-rmpw9a8q.js → index-5e101240.js} +26 -26
  94. package/dist/{index-s2wxgzr0.js → index-5wyfjc2c.js} +392 -15
  95. package/dist/{index-zk96aq1k.js → index-vxvhxxbn.js} +7 -7
  96. package/dist/{keepalive-be9vwy5h.js → keepalive-psj6j54c.js} +9 -9
  97. package/dist/{lead-0861cwrf.js → lead-hajz4688.js} +43 -43
  98. package/dist/{maintenance-mzzgc8bn.js → maintenance-ps02sb5m.js} +7 -7
  99. package/dist/{node-mn1zs7an.js → node-mwb5r697.js} +1 -1
  100. package/dist/{oauth-refresh-sweep-cpdnfwdj.js → oauth-refresh-sweep-0284h93b.js} +8 -8
  101. package/dist/{onboard-fh8qs4r3.js → onboard-rbf6bg01.js} +2 -2
  102. package/dist/{opencode-adapter-nm42wwj8.js → opencode-adapter-13dcatgq.js} +8 -8
  103. package/dist/{otel-impl-x6c1c9g4.js → otel-impl-zvqcbhp7.js} +2 -2
  104. package/dist/{pi-mono-adapter-9n2ya240.js → pi-mono-adapter-xxy47q4v.js} +7 -7
  105. package/dist/{pricing-refresh-qb0rv0ap.js → pricing-refresh-kefx9v3t.js} +9 -9
  106. package/dist/{rbac-roles-1gzdwbh1.js → rbac-roles-3m36rxm2.js} +8 -8
  107. package/dist/{rbac-roles-3mraey52.js → rbac-roles-z76xv0hq.js} +9 -9
  108. package/dist/{render-v2-agpa45zz.js → render-v2-wz73bs25.js} +9 -9
  109. package/dist/{resume-x0ydy9s9.js → resume-k4wzge8w.js} +14 -14
  110. package/dist/{setup-as9ga0d6.js → setup-2fp2pypp.js} +2 -2
  111. package/dist/{work-objects-y5g1y7ev.js → work-objects-6k0ywz3m.js} +29 -29
  112. package/dist/{worker-860c3e38.js → worker-5v3rxsdh.js} +43 -43
  113. package/dist/{x-m477fr0h.js → x-4mtdeym1.js} +2 -2
  114. package/openapi.json +777 -6
  115. package/package.json +8 -7
  116. package/src/agentmail/handlers.ts +21 -11
  117. package/src/apps/definition.ts +16 -0
  118. package/src/azure-devops/api.ts +52 -2
  119. package/src/azure-devops/boards-sync.ts +50 -20
  120. package/src/azure-devops/index.ts +1 -0
  121. package/src/azure-devops/outbound.ts +95 -0
  122. package/src/be/boot-seeds.ts +11 -0
  123. package/src/be/db/model-profiles.ts +315 -0
  124. package/src/be/db/tasks/read.ts +7 -0
  125. package/src/be/db.ts +53 -47
  126. package/src/be/migrations/219_relabel_pi_context_compactions.sql +22 -0
  127. package/src/be/migrations/220_model_profiles.sql +137 -0
  128. package/src/be/model-tier-resolution.ts +145 -11
  129. package/src/be/model-validation.ts +8 -1
  130. package/src/be/scripts/typecheck.ts +12 -0
  131. package/src/be/secret-registry.ts +6 -2
  132. package/src/be/seed-model-profiles.ts +41 -0
  133. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +368 -0
  134. package/src/be/seed-scripts/index.ts +9 -0
  135. package/src/be/swarm-config-guard.ts +1 -0
  136. package/src/commands/runner.ts +3 -17
  137. package/src/extensions/catalog.generated.json +17 -0
  138. package/src/extensions/contract-types.generated.ts +1 -1
  139. package/src/extensions/loader.ts +10 -5
  140. package/src/heartbeat/heartbeat.ts +2 -1
  141. package/src/http/agents.ts +49 -2
  142. package/src/http/all-routes.ts +1 -0
  143. package/src/http/apps.ts +30 -3
  144. package/src/http/config.ts +26 -2
  145. package/src/http/core.ts +4 -0
  146. package/src/http/index.ts +11 -1
  147. package/src/http/model-profiles.ts +303 -0
  148. package/src/http/pages-public.ts +11 -5
  149. package/src/http/poll.ts +16 -5
  150. package/src/http/script-runs.ts +1 -1
  151. package/src/http/scripts.ts +25 -2
  152. package/src/http/tasks.ts +18 -0
  153. package/src/http/webhooks.ts +1 -2
  154. package/src/pages/feedback-overlay.ts +65 -6
  155. package/src/providers/acp-targets.ts +1 -1
  156. package/src/providers/amp-adapter.ts +1 -1
  157. package/src/providers/dsh-adapter.ts +1 -1
  158. package/src/providers/pi-mono-extension.ts +17 -3
  159. package/src/providers/reasoning-effort.ts +10 -0
  160. package/src/rbac/legacy-policy.ts +8 -0
  161. package/src/rbac/permissions.ts +5 -0
  162. package/src/scheduler/deferred-task-waits.ts +18 -5
  163. package/src/scheduler/scheduler.ts +7 -1
  164. package/src/scripts-runtime/executors/quickjs-prelude.ts +75 -6
  165. package/src/scripts-runtime/executors/quickjs-runner.ts +85 -8
  166. package/src/scripts-runtime/executors/quickjs-web-shims.ts +1041 -0
  167. package/src/scripts-runtime/extract-args-schema.ts +9 -8
  168. package/src/scripts-runtime/quickjs-compat.ts +464 -0
  169. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  170. package/src/scripts-runtime/types/stdlib.d.ts +12 -0
  171. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +12 -0
  173. package/src/server.ts +10 -1
  174. package/src/slack/blocks.ts +4 -2
  175. package/src/slack/reaction-shortcode.ts +14 -0
  176. package/src/slack/render-v2.ts +12 -7
  177. package/src/slack/responses.ts +5 -4
  178. package/src/slack/thread-buffer.ts +41 -22
  179. package/src/tasks/worker-follow-up.ts +11 -2
  180. package/src/tests/agentmail-filters.test.ts +47 -0
  181. package/src/tests/apps-spike2.test.ts +35 -0
  182. package/src/tests/azure-devops-outbound.test.ts +420 -0
  183. package/src/tests/db-task-read-contract.test.ts +2 -0
  184. package/src/tests/db-task-write-contract.test.ts +1 -0
  185. package/src/tests/deferred-task-wake.test.ts +78 -1
  186. package/src/tests/docs-index-footprint.test.js +2 -2
  187. package/src/tests/dsh-adapter.test.ts +1 -1
  188. package/src/tests/env-flag.test.ts +1 -0
  189. package/src/tests/extensions-catalog.test.ts +1 -0
  190. package/src/tests/extensions-example-comms-adherence-gate.test.ts +413 -0
  191. package/src/tests/extensions-loader.test.ts +15 -0
  192. package/src/tests/heartbeat-jev-gate-credentials.test.ts +168 -0
  193. package/src/tests/heartbeat-jev-gate.test.ts +385 -0
  194. package/src/tests/model-profile-backcompat.test.ts +327 -0
  195. package/src/tests/model-profile-providers.test.ts +302 -0
  196. package/src/tests/model-profile-runtime-secrets.test.ts +223 -0
  197. package/src/tests/model-profiles-api.test.ts +614 -0
  198. package/src/tests/pages-feedback-keys.test.ts +121 -0
  199. package/src/tests/pages-feedback.test.ts +3 -0
  200. package/src/tests/pi-context-compaction.test.ts +193 -0
  201. package/src/tests/profile-claim.test.ts +269 -0
  202. package/src/tests/quickjs-compat.test.ts +213 -0
  203. package/src/tests/rbac-engine.test.ts +21 -0
  204. package/src/tests/script-connections.test.ts +4 -0
  205. package/src/tests/script-executor-conformance.test.ts +252 -1
  206. package/src/tests/script-executor-quickjs.test.ts +96 -0
  207. package/src/tests/script-runs-http.test.ts +89 -0
  208. package/src/tests/scripts-http.test.ts +16 -0
  209. package/src/tests/secret-scrubber.test.ts +10 -0
  210. package/src/tests/seed-model-profiles.test.ts +174 -0
  211. package/src/tests/seed-scripts.test.ts +2 -2
  212. package/src/tests/skill-invocations.test.ts +4 -2
  213. package/src/tests/slack-blocks.test.ts +36 -0
  214. package/src/tests/slack-inline-output.test.ts +14 -0
  215. package/src/tests/slack-reaction-config.test.ts +20 -0
  216. package/src/tests/slack-render-v2.test.ts +37 -0
  217. package/src/tests/slack-thread-buffer.test.ts +98 -1
  218. package/src/tests/workflow-llm.test.ts +61 -0
  219. package/src/tools/model-profiles/list-model-profiles.ts +54 -0
  220. package/src/tools/request-human-input.ts +11 -3
  221. package/src/tools/script-apis.ts +5 -1
  222. package/src/tools/script-upsert.ts +4 -1
  223. package/src/tools/send-task.ts +24 -1
  224. package/src/tools/system-one-decide.ts +104 -0
  225. package/src/tools/task-action.ts +22 -1
  226. package/src/tools/tool-config.ts +6 -0
  227. package/src/types.ts +36 -1
  228. package/src/utils/secret-scrubber.ts +19 -12
  229. package/src/utils/vcs-repo-match.ts +29 -0
  230. package/src/workflows/executors/raw-llm.ts +8 -3
  231. package/src/workflows/executors/system-one-decision.ts +21 -11
  232. package/src/workflows/executors/workflow-llm.ts +22 -0
  233. package/src/workflows/index.ts +21 -12
  234. package/templates/extensions/README.md +1 -0
  235. package/templates/extensions/comms-adherence-gate/README.md +88 -0
  236. package/templates/extensions/comms-adherence-gate/hooks.ts +489 -0
  237. package/templates/extensions/comms-adherence-gate/manifest.json +8 -0
  238. package/dist/cli-5t4f29ez.js +0 -33
@@ -37,7 +37,7 @@ import type {
37
37
  } from "./types";
38
38
 
39
39
  // npm's `latest` is older and lacks the JSON/stdin contract used here.
40
- export const DSH_PACKAGE = "@deepseek-ai/dsh@0.2.1-alpha.4";
40
+ export const DSH_PACKAGE = "@deepseek-ai/dsh@0.2.1-alpha.5";
41
41
 
42
42
  export function checkDshCredentials(env: Record<string, string | undefined>): CredStatus {
43
43
  return env.DEEPSEEK_API_KEY?.trim() || env.OPENROUTER_API_KEY?.trim()
@@ -653,9 +653,22 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
653
653
  // === session_compact → PreCompact ===
654
654
  // Re-anchor the model on its task once compaction has summarized the
655
655
  // history. `context` fires before every LLM call, so it cannot carry this.
656
- pi.on("session_compact", async () => {
656
+ pi.on("session_compact", async (event, ctx) => {
657
657
  if (!config.taskId) return;
658
658
 
659
+ // The one real compaction signal: shaped like the claude runner's rows.
660
+ fireAndForget(`${config.apiUrl}/api/tasks/${config.taskId}/context`, {
661
+ method: "POST",
662
+ headers: apiHeaders(config),
663
+ body: JSON.stringify({
664
+ eventType: "compaction",
665
+ sessionId: `pi-${config.taskId}`,
666
+ compactTrigger: event.reason === "manual" ? "manual" : "auto",
667
+ preCompactTokens: event.compactionEntry?.tokensBefore,
668
+ contextTotalTokens: ctx.getContextUsage?.()?.contextWindow,
669
+ }),
670
+ });
671
+
659
672
  const taskDetails = await fetchTaskDetails(config);
660
673
  if (taskDetails) {
661
674
  const reminder = await renderGoalReminder(taskDetails);
@@ -667,14 +680,15 @@ export function createSwarmHooksExtension(config: SwarmHooksConfig): ExtensionFa
667
680
  pi.on("context", async (_event, ctx) => {
668
681
  if (!config.taskId) return undefined;
669
682
 
670
- // Report context usage as a compaction event
683
+ // Per-call usage snapshot: "progress", as the claude/codex runner posts
684
+ // per-turn usage. Only session_compact reports a compaction.
671
685
  const usage = ctx.getContextUsage?.();
672
686
  if (usage) {
673
687
  fireAndForget(`${config.apiUrl}/api/tasks/${config.taskId}/context`, {
674
688
  method: "POST",
675
689
  headers: apiHeaders(config),
676
690
  body: JSON.stringify({
677
- eventType: "compaction",
691
+ eventType: "progress",
678
692
  sessionId: `pi-${config.taskId}`,
679
693
  contextUsedTokens: usage.tokens ?? undefined,
680
694
  contextTotalTokens: usage.contextWindow,
@@ -257,6 +257,16 @@ const REASONING_HARNESSES: ReadonlySet<string> = new Set<ReasoningHarness>([
257
257
  "grok",
258
258
  ]);
259
259
 
260
+ /**
261
+ * The levels a session on this harness and model accepts; empty for a harness
262
+ * this feature does not cover. Unlike `effectiveReasoningEffort`, a supported
263
+ * level with no transport effect (Opencode `off`) is still listed.
264
+ */
265
+ export function supportedReasoningLevels(harness: string, model: string): ReasoningEffort[] {
266
+ if (!REASONING_HARNESSES.has(harness)) return [];
267
+ return reasoningCapability(harness as ReasoningHarness, model).levels;
268
+ }
269
+
260
270
  /**
261
271
  * The effort a session on this harness and model actually runs at: `level`
262
272
  * when the adapter applies it, null when it is unset or dropped (the harness
@@ -49,6 +49,13 @@ const operatorOrUser: LegacyRule = {
49
49
  evaluate: (principal) => principal.kind === "operator" || principal.kind === "user",
50
50
  };
51
51
 
52
+ /** The shared API key only: swarm-wide settings that no session user or agent may change. */
53
+ const operatorOnly: LegacyRule = {
54
+ name: "operator-only",
55
+ denyReason: "requires operator authentication",
56
+ evaluate: (principal) => principal.kind === "operator",
57
+ };
58
+
52
59
  const leadOrOperatorOrUser: LegacyRule = {
53
60
  name: "lead-or-operator-or-user",
54
61
  denyReason: "requires lead agent, operator, or user authentication",
@@ -313,6 +320,7 @@ export const LEGACY_POLICY = {
313
320
  // Lead tokens retain config.read.secrets. This exception does not grant config administration.
314
321
  "models.catalog.write": leadOrOperator,
315
322
  "models.harness-support.write": agentOrOperator,
323
+ "models.profiles.write": operatorOnly,
316
324
  "skill.create.swarm": leadOnly,
317
325
  "skill.install.any": leadOnly,
318
326
  "skill.install.global": leadOnly,
@@ -219,6 +219,11 @@ export const PERMISSIONS = {
219
219
  "Record whether a pinned harness CLI version accepts a model (a worker reports its own probe results; the operator may correct any row).",
220
220
  namespace: "models",
221
221
  },
222
+ "models.profiles.write": {
223
+ description:
224
+ "Create, update, or delete model profiles (named harness + model + effort). Operator (shared API key) only.",
225
+ namespace: "models",
226
+ },
222
227
  "config.read.secrets": {
223
228
  description: "Read unmasked secret config values.",
224
229
  namespace: "config",
@@ -98,13 +98,12 @@ async function prepareDeferredWakeTask(
98
98
  */
99
99
  export const DEFERRED_WAIT_WOKE_EVENT = "task_deferred_wait_woke";
100
100
 
101
- /** Undefined means a normal schedule; an empty result means another caller won. */
102
- export async function dispatchDeferredTaskWait(
101
+ /** Undefined means a normal schedule; an empty preparation means the wait is not ready. */
102
+ export async function prepareDeferredTaskWait(
103
103
  scheduleId: string,
104
104
  reason: WakeReason,
105
105
  extraTags: string[] = [],
106
- triggerTaskId?: string,
107
- ): Promise<{ task?: AgentTask } | undefined> {
106
+ ): Promise<{ task?: PreparedTaskCreate } | undefined> {
108
107
  const client = getDbClient();
109
108
  // Keep ordinary schedule dispatch off the write-lock path.
110
109
  if (
@@ -117,8 +116,22 @@ export async function dispatchDeferredTaskWait(
117
116
  // first; the transaction below re-reads the wait and its member set before claiming.
118
117
  // The prepared description carries the cause seen here; for an `any` wait a member
119
118
  // that settles between this read and the claim is missing from that text only.
120
- const preparedForWait = await prepareDeferredWakeTask(scheduleId, reason, extraTags);
119
+ return { task: await prepareDeferredWakeTask(scheduleId, reason, extraTags) };
120
+ }
121
+
122
+ /** Undefined means a normal schedule; an empty result means another caller won. */
123
+ export async function dispatchDeferredTaskWait(
124
+ scheduleId: string,
125
+ reason: WakeReason,
126
+ extraTags: string[] = [],
127
+ triggerTaskId?: string,
128
+ prepared?: { task?: PreparedTaskCreate },
129
+ ): Promise<{ task?: AgentTask } | undefined> {
130
+ const preparation = prepared ?? (await prepareDeferredTaskWait(scheduleId, reason, extraTags));
131
+ if (!preparation) return undefined;
132
+ const preparedForWait = preparation.task;
121
133
  if (!preparedForWait) return {};
134
+ const client = getDbClient();
122
135
  return client.transaction(async () => {
123
136
  const resolved = await resolveWaitCause(scheduleId, reason);
124
137
  if (!resolved) return {};
@@ -38,6 +38,7 @@ import { scrubObject, scrubSecrets } from "../utils/secret-scrubber";
38
38
  import {
39
39
  dispatchDeferredTaskWait,
40
40
  initDeferredTaskWaits,
41
+ prepareDeferredTaskWait,
41
42
  reapSpentDeferredSchedules,
42
43
  reconcileDeferredTaskWaits,
43
44
  stopDeferredTaskWaits,
@@ -303,6 +304,9 @@ async function throwIfWorkflowNeedsSetup(scheduleId: string, runIds: string[]):
303
304
  * recovery pass, or schedule edit invalidates it before any target is dispatched.
304
305
  */
305
306
  async function claimScheduleOccurrence(snapshot: ScheduledTask, extraTags: string[]) {
307
+ // Run extension pre-hooks before opening the claim transaction. The snapshot
308
+ // guard below still prevents a moved ceiling from consuming this preparation.
309
+ const preparedWait = await prepareDeferredTaskWait(snapshot.id, "ceiling", extraTags);
306
310
  return getDbClient().transaction(async () => {
307
311
  const current = await getScheduledTaskById(snapshot.id);
308
312
  if (!current?.enabled || !current.nextRunAt || current.nextRunAt !== snapshot.nextRunAt) {
@@ -313,7 +317,9 @@ async function claimScheduleOccurrence(snapshot: ScheduledTask, extraTags: strin
313
317
  // Event-driven waits already atomically create their child and consume the
314
318
  // ceiling. Keep that transaction (and its afterCommit emits) intact: disabling
315
319
  // a one-time schedule first would prevent its wake-up task from being created.
316
- const deferred = await dispatchDeferredTaskWait(schedule.id, "ceiling", extraTags);
320
+ const deferred = preparedWait
321
+ ? await dispatchDeferredTaskWait(schedule.id, "ceiling", extraTags, undefined, preparedWait)
322
+ : undefined;
317
323
  if (deferred) {
318
324
  return {
319
325
  schedule,
@@ -5,11 +5,14 @@
5
5
  *
6
6
  * - `__host_call(path, argsJson)`: async host call, resolves to a JSON envelope.
7
7
  * - `__host_sync(op, json)`: sync host call, returns a JSON envelope.
8
+ * `crypto.random` returns that many random bytes.
8
9
  * - `__host_log(stream, text)`: append to the captured stdout or stderr.
9
10
  * - `__host_sleep(ms)`: resolves after `ms` on the host clock.
10
11
  * - `__host_done(json)`: reports the final outcome of the run.
11
12
  *
12
13
  * Every value crosses the boundary as JSON. The sandbox has no other way out.
14
+ * `SANDBOX_WEB_SHIMS` (quickjs-web-shims.ts) runs first and adds URL,
15
+ * URLSearchParams, TextEncoder and TextDecoder.
13
16
  * `__swarm_shape` (injected before this file) lists the configured api/mcp
14
17
  * connections so `ctx.api.<slug>` matches the native executor exactly.
15
18
  */
@@ -64,6 +67,39 @@ const setTimeout = (fn, ms, ...args) => {
64
67
  const clearTimeout = (id) => {
65
68
  timers.delete(id);
66
69
  };
70
+ // Shares the timer ids with setTimeout, so either clear function stops either.
71
+ const setInterval = (fn, ms, ...args) => {
72
+ const id = ++timerSeq;
73
+ const delay = Math.max(1, Number(ms) || 0);
74
+ timers.add(id);
75
+ const tick = () =>
76
+ __host_sleep(delay).then(() => {
77
+ if (!timers.has(id)) return;
78
+ tick();
79
+ fn(...args);
80
+ });
81
+ tick();
82
+ return id;
83
+ };
84
+ const clearInterval = clearTimeout;
85
+
86
+ // ── crypto (bytes come from the host CSPRNG) ──
87
+ const INTEGER_ARRAYS = [Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, BigInt64Array, BigUint64Array];
88
+ const getRandomValues = (array) => {
89
+ if (!INTEGER_ARRAYS.some((type) => array instanceof type)) {
90
+ throw new TypeError("crypto.getRandomValues: the argument must be an integer TypedArray");
91
+ }
92
+ new Uint8Array(array.buffer, array.byteOffset, array.byteLength).set(hostSync("crypto.random", array.byteLength));
93
+ return array;
94
+ };
95
+ const randomUUID = () => {
96
+ const bytes = hostSync("crypto.random", 16);
97
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
98
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
99
+ const hex = bytes.map((b) => (b + 256).toString(16).slice(1)).join("");
100
+ return hex.slice(0, 8) + "-" + hex.slice(8, 12) + "-" + hex.slice(12, 16) + "-" + hex.slice(16, 20) + "-" + hex.slice(20);
101
+ };
102
+ const crypto = Object.freeze({ getRandomValues, randomUUID });
67
103
 
68
104
  // ── Redacted ──
69
105
  const redactedEntries = new WeakMap();
@@ -115,7 +151,7 @@ const config = Object.freeze({
115
151
  },
116
152
  });
117
153
 
118
- // ── fetch (text bodies only) ──
154
+ // ── fetch ──
119
155
  class Headers {
120
156
  constructor(init) {
121
157
  this._map = new Map();
@@ -165,14 +201,44 @@ class Response {
165
201
  return JSON.parse(this._body);
166
202
  }
167
203
  }
204
+ const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
205
+ const toBase64 = (bytes) => {
206
+ let out = "";
207
+ let i = 0;
208
+ for (; i + 2 < bytes.length; i += 3) {
209
+ const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
210
+ out += BASE64[n >> 18] + BASE64[(n >> 12) & 63] + BASE64[(n >> 6) & 63] + BASE64[n & 63];
211
+ }
212
+ if (i < bytes.length) {
213
+ const n = (bytes[i] << 16) | ((bytes[i + 1] || 0) << 8);
214
+ out += BASE64[n >> 18] + BASE64[(n >> 12) & 63] + (i + 1 < bytes.length ? BASE64[(n >> 6) & 63] : "=") + "=";
215
+ }
216
+ return out;
217
+ };
218
+ // Binary bodies cross the host boundary as base64; the host decodes them.
219
+ const encodeBody = (body, headers) => {
220
+ if (body == null) return {};
221
+ if (typeof body === "string") return { body };
222
+ if (body instanceof globalThis.URLSearchParams) {
223
+ if (!headers.has("content-type")) headers.set("content-type", "application/x-www-form-urlencoded;charset=UTF-8");
224
+ return { body: body.toString() };
225
+ }
226
+ if (body instanceof ArrayBuffer) return { bodyBase64: toBase64(new Uint8Array(body)) };
227
+ if (ArrayBuffer.isView(body)) {
228
+ return { bodyBase64: toBase64(new Uint8Array(body.buffer, body.byteOffset, body.byteLength)) };
229
+ }
230
+ throw new TypeError(
231
+ "quickjs executor: fetch body must be a string, Uint8Array, ArrayBuffer or URLSearchParams (JSON.stringify objects first)",
232
+ );
233
+ };
168
234
  const toRequest = (input, init) => {
169
235
  const options = init || {};
170
236
  const url = typeof input === "string" ? input : input && (input.href || input.url) ? input.href || input.url : String(input);
171
- if (options.body != null && typeof options.body !== "string") {
172
- throw new TypeError("quickjs executor: fetch body must be a string (JSON.stringify it first)");
173
- }
174
- const headers = options.headers == null ? undefined : Array.from(new Headers(options.headers).entries());
175
- const request = { url, method: options.method, headers, body: options.body == null ? undefined : options.body };
237
+ const headerMap = new Headers(options.headers);
238
+ const body = encodeBody(options.body, headerMap);
239
+ const headerList = Array.from(headerMap.entries());
240
+ const headers = options.headers == null && headerList.length === 0 ? undefined : headerList;
241
+ const request = { url, method: options.method, headers, ...body };
176
242
  if (options.retries !== undefined) request.retries = options.retries;
177
243
  if (options.timeoutMs !== undefined) request.timeoutMs = options.timeoutMs;
178
244
  return request;
@@ -223,6 +289,9 @@ const define = (name, value) =>
223
289
  define("console", console);
224
290
  define("setTimeout", setTimeout);
225
291
  define("clearTimeout", clearTimeout);
292
+ define("setInterval", setInterval);
293
+ define("clearInterval", clearInterval);
294
+ define("crypto", crypto);
226
295
  define("fetch", fetch);
227
296
  define("Headers", Headers);
228
297
  define("Response", Response);
@@ -13,6 +13,7 @@ import { table } from "../stdlib/table";
13
13
  import { SwarmConfig } from "../swarm-config";
14
14
  import { bundleForQuickJS, USER_SCRIPT_FILE } from "./quickjs-bundle";
15
15
  import { SANDBOX_EPILOGUE, SANDBOX_PRELUDE } from "./quickjs-prelude";
16
+ import { SANDBOX_WEB_SHIMS } from "./quickjs-web-shims";
16
17
  import type { SourceMapReader } from "./sourcemap";
17
18
  import type {
18
19
  ExecutorOutput,
@@ -37,6 +38,13 @@ const BUNDLE_FILE = USER_SCRIPT_FILE.replace(/\.ts$/, ".js");
37
38
  const MAX_CONCURRENT_HOST_CALLS = 16;
38
39
  // Host calls and timers that one run can have in flight or queued.
39
40
  const MAX_PENDING_HOST_CALLS = 1024;
41
+ // A binary fetch body crosses the bridge as base64 and is decoded on the host.
42
+ // One body decodes to at most this many bytes, and the binary bodies a run has
43
+ // queued or in flight total at most MAX_PENDING_BINARY_BODY_BYTES. Both are
44
+ // checked before decoding, so a rejected request never reaches fetch.
45
+ const MAX_BINARY_BODY_BYTES = 16 * 1024 * 1024;
46
+ const MAX_BINARY_BODY_BASE64_LENGTH = Math.ceil(MAX_BINARY_BODY_BYTES / 3) * 4;
47
+ const MAX_PENDING_BINARY_BODY_BYTES = 64 * 1024 * 1024;
40
48
  // After a run ends, its host calls are aborted. Wait this long for them to settle.
41
49
  const HOST_CALL_SETTLE_MS = 1_000;
42
50
 
@@ -46,6 +54,9 @@ export type QuickJSJobResult = {
46
54
  leakedHostCalls: number;
47
55
  };
48
56
  const MAX_STACK_BYTES = 1024 * 1024;
57
+ // Bun's getRandomValues has no 64 KiB quota, so neither does the sandbox. This
58
+ // bound only keeps one sync host call's JSON reply small.
59
+ const MAX_RANDOM_BYTES = 1024 * 1024;
49
60
  const TIMEOUT_EXIT_CODE = 124;
50
61
 
51
62
  type ErrorPayload = { name: string; message: string; stack: string };
@@ -126,12 +137,51 @@ type SandboxRequest = {
126
137
  method?: string;
127
138
  headers?: [string, string][];
128
139
  body?: string;
140
+ /** Binary body, base64-encoded by the sandbox. */
141
+ bodyBase64?: string;
129
142
  retries?: number;
130
143
  timeoutMs?: number;
131
144
  };
132
145
 
146
+ const FETCH_PATHS = new Set(["fetch", "stdlib.fetch", "stdlib.fetchJson"]);
147
+ const BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
148
+
149
+ /**
150
+ * Checks a sandbox fetch request's base64 body without decoding it and returns
151
+ * its decoded size (0 when there is none). The sandbox controls the value, so
152
+ * the type, alphabet and size are checked before the host allocates anything.
153
+ */
154
+ function binaryBodySize(request: unknown): number {
155
+ if (!request || typeof request !== "object") return 0;
156
+ const encoded = (request as { bodyBase64?: unknown }).bodyBase64;
157
+ if (encoded === undefined) return 0;
158
+ if (typeof encoded !== "string") throw new TypeError("fetch bodyBase64 must be a string");
159
+ if (encoded.length > MAX_BINARY_BODY_BASE64_LENGTH) {
160
+ throw new RangeError(
161
+ `fetch binary body exceeds the ${MAX_BINARY_BODY_BYTES}-byte limit of the quickjs executor`,
162
+ );
163
+ }
164
+ if (encoded.length % 4 !== 0 || !BASE64_RE.test(encoded)) {
165
+ throw new TypeError("fetch bodyBase64 is not valid base64");
166
+ }
167
+ const padding = encoded.endsWith("==") ? 2 : encoded.endsWith("=") ? 1 : 0;
168
+ const size = (encoded.length / 4) * 3 - padding;
169
+ if (size > MAX_BINARY_BODY_BYTES) {
170
+ throw new RangeError(
171
+ `fetch binary body exceeds the ${MAX_BINARY_BODY_BYTES}-byte limit of the quickjs executor`,
172
+ );
173
+ }
174
+ return size;
175
+ }
176
+
133
177
  function requestInit(request: SandboxRequest): RequestInit {
134
- return { method: request.method, headers: request.headers, body: request.body };
178
+ if (request.bodyBase64 === undefined) {
179
+ return { method: request.method, headers: request.headers, body: request.body };
180
+ }
181
+ // Already checked by binaryBodySize when the call was accepted.
182
+ const bytes = Buffer.from(request.bodyBase64, "base64");
183
+ const body = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
184
+ return { method: request.method, headers: request.headers, body };
135
185
  }
136
186
 
137
187
  async function serializeResponse(response: Response, operation: string) {
@@ -223,6 +273,16 @@ function dispatchSync(config: SwarmConfig, op: string, value: unknown): unknown
223
273
  return Redacted.meta(redactedFor(config, value));
224
274
  case "config.has":
225
275
  return typeof value === "string" && config.get(value) !== undefined;
276
+ case "crypto.random": {
277
+ if (
278
+ !Number.isInteger(value) ||
279
+ (value as number) < 0 ||
280
+ (value as number) > MAX_RANDOM_BYTES
281
+ ) {
282
+ throw new Error(`crypto.random needs a byte count between 0 and ${MAX_RANDOM_BYTES}`);
283
+ }
284
+ return Array.from(crypto.getRandomValues(new Uint8Array(value as number)));
285
+ }
226
286
  case "table":
227
287
  return table(value as Array<Record<string, unknown>>);
228
288
  default:
@@ -287,6 +347,7 @@ class HostCallScope {
287
347
  private readonly waiters: Array<() => void> = [];
288
348
  private readonly outerFetch = globalThis.fetch;
289
349
  private active = 0;
350
+ private bodyBytes = 0;
290
351
 
291
352
  constructor() {
292
353
  const outer = this.outerFetch;
@@ -306,8 +367,11 @@ class HostCallScope {
306
367
  return this.controller.signal;
307
368
  }
308
369
 
309
- /** Track a host call. `limited` calls share the per-run concurrency budget. */
310
- call<T>(run: () => Promise<T>, limited: boolean): Promise<T> {
370
+ /**
371
+ * Track a host call. `limited` calls share the per-run concurrency budget.
372
+ * `bodyBytes` counts against the per-run binary body budget until it settles.
373
+ */
374
+ call<T>(run: () => Promise<T>, limited: boolean, bodyBytes = 0): Promise<T> {
311
375
  if (this.ops.size >= MAX_PENDING_HOST_CALLS) {
312
376
  return Promise.reject(
313
377
  new Error(
@@ -315,12 +379,21 @@ class HostCallScope {
315
379
  ),
316
380
  );
317
381
  }
382
+ if (this.bodyBytes + bodyBytes > MAX_PENDING_BINARY_BODY_BYTES) {
383
+ return Promise.reject(
384
+ new RangeError(
385
+ `Pending fetch binary bodies in one script run exceed ${MAX_PENDING_BINARY_BODY_BYTES} bytes`,
386
+ ),
387
+ );
388
+ }
389
+ this.bodyBytes += bodyBytes;
318
390
  const op = limited ? this.limited(run) : run();
319
391
  this.ops.add(op);
320
- void op.then(
321
- () => this.ops.delete(op),
322
- () => this.ops.delete(op),
323
- );
392
+ const settle = () => {
393
+ this.ops.delete(op);
394
+ this.bodyBytes -= bodyBytes;
395
+ };
396
+ void op.then(settle, settle);
324
397
  return op;
325
398
  }
326
399
 
@@ -491,7 +564,10 @@ async function evaluateJob(
491
564
  define("__host_call", (pathHandle, argsHandle) => {
492
565
  const path = ctx.getString(pathHandle);
493
566
  const callArgs = JSON.parse(ctx.getString(argsHandle)) as unknown[];
494
- return bridge(() => host.call(() => dispatchCall(hostCtx, path, callArgs, host.signal), true));
567
+ return bridge(async () => {
568
+ const bodyBytes = FETCH_PATHS.has(path) ? binaryBodySize(callArgs?.[0]) : 0;
569
+ return host.call(() => dispatchCall(hostCtx, path, callArgs, host.signal), true, bodyBytes);
570
+ });
495
571
  });
496
572
  define("__host_sync", (opHandle, valueHandle) => {
497
573
  let envelope: unknown;
@@ -538,6 +614,7 @@ async function evaluateJob(
538
614
  const init = `globalThis.__swarm_shape = ${JSON.stringify(shape)};\nglobalThis.__swarm_args = ${JSON.stringify(args)};`;
539
615
  const ok =
540
616
  evaluate(init, "swarm-init.js") &&
617
+ evaluate(SANDBOX_WEB_SHIMS, "swarm-web-shims.js") &&
541
618
  evaluate(SANDBOX_PRELUDE, "swarm-prelude.js") &&
542
619
  evaluate(bundle.code, BUNDLE_FILE) &&
543
620
  evaluate(SANDBOX_EPILOGUE, "swarm-epilogue.js");