@desplega.ai/agent-swarm 2.6.0 → 2.7.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 (213) hide show
  1. package/README.md +2 -0
  2. package/dist/{acp-adapter-dgfakfpw.js → acp-adapter-ma1js5n7.js} +6 -6
  3. package/dist/{actions-fbqma8aa.js → actions-1vhmtvg9.js} +18 -18
  4. package/dist/{amp-adapter-en0jy6hs.js → amp-adapter-sxqqvmdk.js} +3 -3
  5. package/dist/{app-k8a5cgpp.js → app-mwx1d5a9.js} +3 -3
  6. package/dist/{assistant-a16433gr.js → assistant-yx1p6g82.js} +20 -20
  7. package/dist/{boot-reembed-f4nn561n.js → boot-reembed-5h37a2c2.js} +1 -1
  8. package/dist/{boot-reembed-6c1sjfnr.js → boot-reembed-7h78vmvv.js} +2 -1
  9. package/dist/{boot-scrub-sweep-d4w9gajf.js → boot-scrub-sweep-8mawd4gw.js} +3 -3
  10. package/dist/{boot-seeds-9bmkspyw.js → boot-seeds-ca84dhnn.js} +4 -4
  11. package/dist/{claude-adapter-nys1kjxa.js → claude-adapter-wqt4hfkq.js} +7 -7
  12. package/dist/{claude-managed-adapter-55207973.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
  13. package/dist/{claude-managed-setup-3cm35gey.js → claude-managed-setup-rqv98sea.js} +2 -2
  14. package/dist/{claude-sdk-session-nw5yd8cp.js → claude-sdk-session-f29f3b2x.js} +7 -7
  15. package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
  16. package/dist/{cli-k21gx5ht.js → cli-1hhbzkbb.js} +6 -6
  17. package/dist/{cli-v49fkghp.js → cli-1m9x8dr5.js} +1 -1
  18. package/dist/{cli-24j1jeeb.js → cli-2pvzmas1.js} +3 -3
  19. package/dist/{cli-w2656ka1.js → cli-2y7v133v.js} +7 -7
  20. package/dist/{cli-g8ka5j4j.js → cli-48gxjmcz.js} +94 -36
  21. package/dist/{cli-2sjhpsns.js → cli-54sj64fc.js} +1 -1
  22. package/dist/{cli-aj8djyqt.js → cli-60sty27p.js} +2 -2
  23. package/dist/{cli-4hxrf0p4.js → cli-6a9qfa8b.js} +308 -47
  24. package/dist/{cli-n7asmce6.js → cli-6mgdba42.js} +3 -3
  25. package/dist/{cli-gf95q3bm.js → cli-6srv653c.js} +3 -3
  26. package/dist/{cli-9abdgn5t.js → cli-86ehn4s5.js} +3 -3
  27. package/dist/{cli-bqztfyve.js → cli-8mbc9za9.js} +1 -1
  28. package/dist/{cli-a2rr0fmg.js → cli-8wmj18cz.js} +1 -1
  29. package/dist/{cli-c2habvsf.js → cli-9gsm2rzp.js} +8 -8
  30. package/dist/{cli-4rf0f5vg.js → cli-9pshx6rd.js} +4 -2
  31. package/dist/{cli-abkm71vr.js → cli-a6ds2epe.js} +1 -1
  32. package/dist/{cli-esakngh8.js → cli-a89zdqz9.js} +1 -1
  33. package/dist/{cli-3ndmpxxv.js → cli-ay40dnj3.js} +2 -2
  34. package/dist/{cli-kf4jree9.js → cli-aypd09nf.js} +1 -1
  35. package/dist/{cli-6y51ryva.js → cli-b3yzyc57.js} +1 -1
  36. package/dist/{cli-ag2yrnt5.js → cli-c668vvcf.js} +2 -2
  37. package/dist/{cli-5sckpqcx.js → cli-d20qvpr2.js} +1 -1
  38. package/dist/{cli-a34pr3w1.js → cli-e1595bgx.js} +2 -2
  39. package/dist/{cli-m70bajzr.js → cli-f12mr20z.js} +1 -1
  40. package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
  41. package/dist/{cli-mvjz5an3.js → cli-f4dbh7a5.js} +7 -7
  42. package/dist/{cli-az25fn5f.js → cli-gsfk58nw.js} +2 -2
  43. package/dist/{cli-1tem6ns9.js → cli-gt4h8cxw.js} +3 -2
  44. package/dist/{cli-cz37jqss.js → cli-h5jeg46p.js} +2 -2
  45. package/dist/{cli-emhjcj2y.js → cli-he9gbdx0.js} +60 -10
  46. package/dist/{cli-3fwqy71j.js → cli-j0ppywdr.js} +38 -31
  47. package/dist/{cli-5rj83bam.js → cli-j3y0x2n4.js} +34 -33
  48. package/dist/{cli-3gfvmzht.js → cli-j9bpmykh.js} +73 -25
  49. package/dist/{cli-m5a9m0ak.js → cli-kv7sbc2c.js} +3 -3
  50. package/dist/{cli-g20jjwt4.js → cli-m30k9777.js} +12 -12
  51. package/dist/{cli-683y0b3a.js → cli-m5e802r7.js} +2 -2
  52. package/dist/{cli-nn6vx2mq.js → cli-ptfq202x.js} +5 -3
  53. package/dist/{cli-d2dpf95n.js → cli-qj442fws.js} +1 -1
  54. package/dist/{cli-03gg9pd1.js → cli-rd5m70gj.js} +4 -4
  55. package/dist/{cli-gnsb1zxa.js → cli-s09j9vp5.js} +1 -1
  56. package/dist/{cli-zqne1qcj.js → cli-s0mgx214.js} +2 -2
  57. package/dist/{cli-sqma6vw1.js → cli-sgqm2582.js} +5 -5
  58. package/dist/{cli-nq677csy.js → cli-sjctjr48.js} +1 -1
  59. package/dist/{cli-3aqd4jaa.js → cli-twctnkng.js} +3 -3
  60. package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
  61. package/dist/{cli-7enjwya9.js → cli-vz78s5wm.js} +1 -1
  62. package/dist/{cli-art7c1zh.js → cli-wqr7cea4.js} +4 -4
  63. package/dist/{cli-hfeb68z4.js → cli-x37r7qwf.js} +11 -3
  64. package/dist/{cli-ffvsj2qw.js → cli-yd2nreqw.js} +2 -2
  65. package/dist/{cli-ca6banwz.js → cli-ymjpkxgn.js} +6 -6
  66. package/dist/{cli-mtnj4fwa.js → cli-yxbsmx56.js} +5 -5
  67. package/dist/cli.js +18 -16
  68. package/dist/{codex-adapter-d95q4qsn.js → codex-adapter-zk59x2zh.js} +5 -5
  69. package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
  70. package/dist/{codex-session-runner-gdwd8h0c.js → codex-session-runner-kh6s4j3n.js} +5 -5
  71. package/dist/{commands-jqmgk54b.js → commands-68et1qv2.js} +2 -2
  72. package/dist/{controller-z3jq589f.js → controller-fyrbnzg4.js} +2 -2
  73. package/dist/{controllers-connect-r2za39ef.js → controllers-connect-nep9h2xv.js} +19 -19
  74. package/dist/{cursor-adapter-4mr4jgkw.js → cursor-adapter-yss3pg7t.js} +1 -1
  75. package/dist/{db-w36wwb5g.js → db-28gjyvxj.js} +2 -2
  76. package/dist/{db-ppv2tze7.js → db-8ftwvzba.js} +3 -3
  77. package/dist/{dispatcher-pv5h77vf.js → dispatcher-hayexmkr.js} +17 -17
  78. package/dist/{dsh-adapter-d4pdpb3r.js → dsh-adapter-46wawff9.js} +1 -1
  79. package/dist/{easter-egg-3d-4sp4dvmc.js → easter-egg-3d-8d1e0h4m.js} +1 -1
  80. package/dist/{execute-jev839y1.js → execute-cjrfxfnm.js} +1 -1
  81. package/dist/{external-channel-y6nkzhnn.js → external-channel-xbmxcg09.js} +2 -2
  82. package/dist/{grok-adapter-t08b9ntm.js → grok-adapter-ag4wks2a.js} +7 -7
  83. package/dist/handlers-tzmevqb5.js +41 -0
  84. package/dist/{heartbeat-vn31xwdk.js → heartbeat-psfvcgwp.js} +20 -20
  85. package/dist/{hook-6d0zbvvt.js → hook-8pz04697.js} +5 -5
  86. package/dist/{hook-cpjs8q51.js → hook-cyq4mryn.js} +6 -6
  87. package/dist/{http-9c6medda.js → http-haj1nd6k.js} +109 -114
  88. package/dist/{index-qhjf2htm.js → index-jwt2bj6f.js} +261 -96
  89. package/dist/{index-g6k9tctj.js → index-q52f47pe.js} +1 -1
  90. package/dist/{index-pkma1yf8.js → index-syvhnvtw.js} +1 -1
  91. package/dist/{index-yqgbfgaj.js → index-y1ajx04a.js} +20 -20
  92. package/dist/{index-k0yt1tvq.js → index-z5h22pes.js} +17 -17
  93. package/dist/{index-38t35h0n.js → index-zyq8ct52.js} +17 -17
  94. package/dist/{keepalive-em0117bp.js → keepalive-xdvsamvj.js} +3 -3
  95. package/dist/{lead-23e7z59t.js → lead-hqg57kdp.js} +35 -35
  96. package/dist/{maintenance-kbme3qy9.js → maintenance-4y2xzh3a.js} +1 -1
  97. package/dist/{oauth-refresh-sweep-xysj4yse.js → oauth-refresh-sweep-jd4r7zbs.js} +8 -3
  98. package/dist/{onboard-p6q0vz97.js → onboard-18447q9q.js} +5 -5
  99. package/dist/{opencode-adapter-esv3k9yn.js → opencode-adapter-26b2frdf.js} +5 -5
  100. package/dist/{otel-impl-54qhqh1z.js → otel-impl-f9tgr8yw.js} +1 -1
  101. package/dist/{pi-mono-adapter-mymbwhj0.js → pi-mono-adapter-qn6qbga1.js} +33 -12
  102. package/dist/{pricing-refresh-yb75wv33.js → pricing-refresh-dw2tgb53.js} +3 -3
  103. package/dist/{rbac-roles-1vp2mw4p.js → rbac-roles-aygm2760.js} +2 -2
  104. package/dist/{rbac-roles-dexhc9x8.js → rbac-roles-s4nwj2m3.js} +1 -1
  105. package/dist/{render-v2-tjay5hhf.js → render-v2-yfvdfr0x.js} +3 -3
  106. package/dist/{resume-8ayf37g5.js → resume-1bmvjbf3.js} +7 -7
  107. package/dist/{runtime-hqe5cqcd.js → runtime-f7tpzedy.js} +1 -1
  108. package/dist/{session-access-fdhp987s.js → session-access-r5garej6.js} +4 -4
  109. package/dist/{session-source-backfill-qgtc86z1.js → session-source-backfill-424wzv0r.js} +2 -2
  110. package/dist/{setup-4rrmqcq3.js → setup-8tygvxkq.js} +1 -1
  111. package/dist/{virtual-modules-52km00d6.js → virtual-modules-xsw323b3.js} +2 -2
  112. package/dist/{work-objects-p28fgd70.js → work-objects-ggnghpgf.js} +20 -20
  113. package/dist/{worker-e3w7vye1.js → worker-sqjd1nqf.js} +35 -35
  114. package/openapi.json +69 -1
  115. package/package.json +5 -3
  116. package/src/be/automation-preflight.ts +2 -0
  117. package/src/be/db/api-keys.ts +9 -0
  118. package/src/be/db/workflow-runs.ts +36 -5
  119. package/src/be/db.ts +80 -22
  120. package/src/be/memory/task-learning.ts +155 -0
  121. package/src/be/migrations/245_script_executor.sql +3 -0
  122. package/src/be/oauth-refresh-sweep.ts +2 -1
  123. package/src/be/scripts/boot-reembed.ts +2 -0
  124. package/src/be/scripts/db.ts +14 -4
  125. package/src/be/scripts/embeddings.ts +2 -0
  126. package/src/be/scripts/run-saved.ts +1 -0
  127. package/src/be/scripts/typecheck.ts +9 -3
  128. package/src/be/seed-skills/index.ts +7 -0
  129. package/src/commands/onboard/compose-generator.ts +5 -3
  130. package/src/commands/provider-credentials.ts +2 -1
  131. package/src/heartbeat/heartbeat.ts +14 -1
  132. package/src/heartbeat/script-runner-probe.ts +47 -0
  133. package/src/http/api-keys.ts +24 -9
  134. package/src/http/idempotency-key.ts +1 -1
  135. package/src/http/index.ts +3 -3
  136. package/src/http/pages-public.ts +38 -5
  137. package/src/http/scripts.ts +27 -0
  138. package/src/http/workflows.ts +10 -0
  139. package/src/http/x.ts +5 -0
  140. package/src/prompts/base-prompt.ts +21 -1
  141. package/src/prompts/session-templates.ts +17 -3
  142. package/src/providers/amp-adapter.ts +1 -1
  143. package/src/providers/dsh-adapter.ts +1 -1
  144. package/src/providers/pi-mono-adapter.ts +41 -11
  145. package/src/scheduler/scheduler.ts +2 -1
  146. package/src/scripts-runtime/eval-harness.ts +8 -1
  147. package/src/scripts-runtime/executors/native.ts +22 -16
  148. package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
  149. package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
  150. package/src/scripts-runtime/executors/quickjs.ts +7 -0
  151. package/src/scripts-runtime/executors/registry.ts +32 -0
  152. package/src/scripts-runtime/executors/types.ts +7 -0
  153. package/src/scripts-runtime/host-validated-args.ts +133 -0
  154. package/src/scripts-runtime/loader.ts +30 -3
  155. package/src/scripts-runtime/swarm-sdk.ts +9 -2
  156. package/src/scripts-runtime/types/stdlib.d.ts +9 -1
  157. package/src/scripts-runtime/types/swarm-sdk.d.ts +9 -1
  158. package/src/tests/api-key-status-by-suffix.test.ts +242 -0
  159. package/src/tests/base-prompt.test.ts +70 -2
  160. package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
  161. package/src/tests/credential-check.test.ts +4 -2
  162. package/src/tests/docs-index.test.ts +135 -0
  163. package/src/tests/dsh-adapter.test.ts +1 -1
  164. package/src/tests/fetch-resolved-env.test.ts +16 -2
  165. package/src/tests/heartbeat-checklist.test.ts +89 -0
  166. package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
  167. package/src/tests/oauth-refresh-sweep.test.ts +34 -1
  168. package/src/tests/onboard-compose.test.ts +20 -0
  169. package/src/tests/opencode-adapter.test.ts +2 -1
  170. package/src/tests/pages-frame-ancestors.test.ts +162 -0
  171. package/src/tests/pi-codemode.test.ts +2 -1
  172. package/src/tests/pi-mono-adapter.test.ts +54 -0
  173. package/src/tests/pi-native-mcp.test.ts +13 -1
  174. package/src/tests/pi-tool-exposure.test.ts +8 -8
  175. package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
  176. package/src/tests/prompt-template-session.test.ts +4 -3
  177. package/src/tests/request-composition-derive.test.ts +329 -0
  178. package/src/tests/script-connections.test.ts +2 -0
  179. package/src/tests/script-executor-conformance.test.ts +36 -1
  180. package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
  181. package/src/tests/script-executor-quickjs.test.ts +140 -0
  182. package/src/tests/script-host-validated-args-native.test.ts +196 -0
  183. package/src/tests/script-per-script-executor.test.ts +415 -0
  184. package/src/tests/scripts-external-api.test.ts +45 -0
  185. package/src/tests/sdk-allowlist.test.ts +3 -2
  186. package/src/tests/self-improvement.test.ts +4 -4
  187. package/src/tests/store-progress-learning.test.ts +288 -0
  188. package/src/tests/ui-logs-parser.test.ts +180 -0
  189. package/src/tests/workflow-engine-v2.test.ts +116 -1
  190. package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
  191. package/src/tests/workflow-run-finished-event.test.ts +263 -0
  192. package/src/tests/workflow-swarm-script.test.ts +34 -0
  193. package/src/tools/script-common.ts +1 -0
  194. package/src/tools/script-run.ts +7 -0
  195. package/src/tools/script-upsert.ts +7 -0
  196. package/src/tools/store-progress.ts +67 -1
  197. package/src/tools/workflows/trigger-workflow.ts +104 -10
  198. package/src/types.ts +12 -1
  199. package/src/utils/credentials.ts +5 -2
  200. package/src/workflows/engine.ts +18 -16
  201. package/src/workflows/executors/swarm-script.ts +5 -0
  202. package/src/workflows/foreach-driver.ts +5 -11
  203. package/src/workflows/node-timeout.ts +30 -0
  204. package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
  205. package/templates/community/linear-delivery-loop/README.md +38 -0
  206. package/templates/community/linear-delivery-loop/config.example.json +31 -0
  207. package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
  208. package/templates/community/linear-delivery-loop/schedule.json +10 -0
  209. package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
  210. package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
  211. package/templates/skills/swarm-docs/config.json +14 -0
  212. package/templates/skills/swarm-docs/content.md +144 -0
  213. package/dist/handlers-eqsnmv95.js +0 -41
@@ -173,23 +173,21 @@ export class NativeScriptExecutor implements ScriptExecutor {
173
173
  let removeAbortListener: (() => void) | undefined;
174
174
  let proc: Bun.Subprocess<"pipe", "pipe", "pipe"> | undefined;
175
175
 
176
- try {
177
- if (killed) {
178
- return {
179
- result: undefined,
180
- stdout: "",
181
- stderr: "",
182
- truncated: { stdout: false, stderr: false },
183
- durationMs: Date.now() - start,
184
- exitCode: 1,
185
- error: "killed",
186
- };
187
- }
176
+ const killedOutput = (): ExecutorOutput => ({
177
+ result: undefined,
178
+ stdout: "",
179
+ stderr: "",
180
+ truncated: { stdout: false, stderr: false },
181
+ durationMs: Date.now() - start,
182
+ exitCode: 1,
183
+ error: "killed",
184
+ });
188
185
 
189
- await Bun.write(argsFile, JSON.stringify(input.args ?? null));
190
- await Bun.write(sourceFile, input.source);
191
- await writeBareImportShims(tmpdir);
186
+ try {
187
+ if (killed) return killedOutput();
192
188
 
189
+ // Listen before the first await: an abort during the file writes below
190
+ // must stop the spawn, not land between the writes and the listener.
193
191
  const onExternalAbort = () => {
194
192
  killed = true;
195
193
  if (proc) void terminateProcessGroup(proc.pid);
@@ -197,12 +195,19 @@ export class NativeScriptExecutor implements ScriptExecutor {
197
195
  input.signal?.addEventListener("abort", onExternalAbort, { once: true });
198
196
  removeAbortListener = () => input.signal?.removeEventListener("abort", onExternalAbort);
199
197
 
198
+ await Bun.write(argsFile, JSON.stringify(input.args ?? null));
199
+ await Bun.write(sourceFile, input.source);
200
+ await writeBareImportShims(tmpdir);
201
+
202
+ // No await between this check and the spawn below.
203
+ if (killed) return killedOutput();
204
+
200
205
  const timeout = setTimeout(() => {
201
206
  timedOut = true;
202
207
  if (proc) void terminateProcessGroup(proc.pid);
203
208
  }, input.resources.wallClockMs);
204
209
 
205
- const harnessEnv = {
210
+ const harnessEnv: Record<string, string> = {
206
211
  PATH: process.env.PATH ?? "/usr/bin:/bin",
207
212
  HOME: process.env.HOME ?? "/tmp",
208
213
  LANG: process.env.LANG ?? "C.UTF-8",
@@ -214,6 +219,7 @@ export class NativeScriptExecutor implements ScriptExecutor {
214
219
  SWARM_SCRIPT_RESULT_FILE: resultFile,
215
220
  SWARM_SCRIPT_ERROR_FILE: errorFile,
216
221
  SWARM_SCRIPT_STARTED_FILE: startedFile,
222
+ ...(input.hostValidatedArgs ? { SWARM_SCRIPT_HOST_VALIDATED_ARGS: "1" } : {}),
217
223
  };
218
224
 
219
225
  proc = registerProcessGroup(
@@ -308,14 +308,116 @@ define("__swarm_ctx", ctx);
308
308
  * zod `argsSchema` check, the same contract as `eval-harness.ts`.
309
309
  */
310
310
  export const SANDBOX_EPILOGUE = String.raw`(async () => {
311
+ const isStructuralSchemaNode = (schema) => {
312
+ if (!schema || typeof schema !== "object") return false;
313
+ const def = schema._zod && schema._zod.def;
314
+ if (!def || typeof def !== "object") return false;
315
+ if (def.coerce) return false;
316
+ if (Array.isArray(def.checks) && def.checks.length > 0) return false;
317
+ switch (def.type) {
318
+ case "string":
319
+ case "number":
320
+ return !def.format;
321
+ case "boolean":
322
+ case "literal":
323
+ case "enum":
324
+ return true;
325
+ case "optional":
326
+ case "nullable":
327
+ return isStructuralSchemaNode(def.innerType);
328
+ case "array":
329
+ return isStructuralSchemaNode(def.element);
330
+ case "object": {
331
+ const rawShape = typeof def.shape === "function" ? def.shape() : def.shape;
332
+ if (!rawShape || typeof rawShape !== "object") return false;
333
+ return Object.values(rawShape).every(isStructuralSchemaNode);
334
+ }
335
+ default:
336
+ return false;
337
+ }
338
+ };
339
+ const isHostValidationEquivalent = (schema, value) => {
340
+ if (!schema || typeof schema !== "object") return false;
341
+ const def = schema._zod && schema._zod.def;
342
+ if (!def || typeof def !== "object") return false;
343
+ if (def.coerce) return false;
344
+ if (Array.isArray(def.checks) && def.checks.length > 0) return false;
345
+ switch (def.type) {
346
+ case "string":
347
+ return typeof value === "string" && !def.format;
348
+ case "number":
349
+ return typeof value === "number" && Number.isFinite(value) && !def.format;
350
+ case "boolean":
351
+ return typeof value === "boolean";
352
+ case "literal": {
353
+ const rawValues = def.values;
354
+ const values = Array.isArray(rawValues)
355
+ ? rawValues
356
+ : rawValues instanceof Set
357
+ ? Array.from(rawValues)
358
+ : [];
359
+ return (
360
+ values.length > 0 &&
361
+ values.some((v) => v === value && typeof v !== "bigint" && typeof v !== "symbol")
362
+ );
363
+ }
364
+ case "enum": {
365
+ if (schema._zod && schema._zod.values instanceof Set) {
366
+ return schema._zod.values.has(value);
367
+ }
368
+ const entriesObj = def.entries && typeof def.entries === "object" ? def.entries : null;
369
+ if (!entriesObj) return false;
370
+ const entries = Object.values(entriesObj).filter(
371
+ (v) => !(typeof v === "string" && typeof entriesObj[v] === "number"),
372
+ );
373
+ return entries.length > 0 && entries.includes(value);
374
+ }
375
+ case "optional":
376
+ return value === undefined
377
+ ? isStructuralSchemaNode(def.innerType)
378
+ : isHostValidationEquivalent(def.innerType, value);
379
+ case "nullable":
380
+ return value === null
381
+ ? isStructuralSchemaNode(def.innerType)
382
+ : isHostValidationEquivalent(def.innerType, value);
383
+ case "array":
384
+ return (
385
+ Array.isArray(value) &&
386
+ isStructuralSchemaNode(def.element) &&
387
+ value.every((item) => isHostValidationEquivalent(def.element, item))
388
+ );
389
+ case "object": {
390
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
391
+ const shape = typeof def.shape === "function" ? def.shape() : def.shape;
392
+ if (!shape || typeof shape !== "object") return false;
393
+ for (const key of Object.keys(value)) {
394
+ if (!Object.hasOwn(shape, key)) return false;
395
+ }
396
+ for (const [key, propSchema] of Object.entries(shape)) {
397
+ if (!Object.hasOwn(value, key) && key in value) return false;
398
+ const propValue = value[key];
399
+ if (!isHostValidationEquivalent(propSchema, propValue)) return false;
400
+ }
401
+ return true;
402
+ }
403
+ default:
404
+ return false;
405
+ }
406
+ };
311
407
  const mod = globalThis.__swarm_module;
312
408
  if (!mod || typeof mod.default !== "function") {
313
409
  throw new Error(
314
410
  "Swarm script must export a default function. Script must export default async function (args, ctx): args FIRST, ctx second.",
315
411
  );
316
412
  }
413
+ const hostValidatedArgs = globalThis.__swarm_host_validated_args === true;
317
414
  let args = globalThis.__swarm_args;
318
- if (mod.argsSchema && typeof mod.argsSchema === "object" && "parse" in mod.argsSchema) {
415
+ if (
416
+ mod.argsSchema &&
417
+ typeof mod.argsSchema === "object" &&
418
+ "parse" in mod.argsSchema &&
419
+ !(hostValidatedArgs && isHostValidationEquivalent(mod.argsSchema, args))
420
+ ) {
319
421
  try {
320
422
  args = mod.argsSchema.parse(args);
321
423
  } catch (error) {
@@ -29,6 +29,7 @@ import type {
29
29
  export type QuickJSJob = {
30
30
  source: string;
31
31
  args: unknown;
32
+ hostValidatedArgs?: boolean;
32
33
  configPayload: SwarmConfigPayload;
33
34
  resources: ScriptResourcePolicy;
34
35
  network: ExecutorInput["network"];
@@ -656,7 +657,7 @@ async function evaluateJob(
656
657
 
657
658
  let timer: ReturnType<typeof setTimeout> | undefined;
658
659
  try {
659
- const init = `globalThis.__swarm_shape = ${JSON.stringify(shape)};\nglobalThis.__swarm_args = ${JSON.stringify(args)};`;
660
+ const init = `globalThis.__swarm_shape = ${JSON.stringify(shape)};\nglobalThis.__swarm_args = ${JSON.stringify(args)};\nglobalThis.__swarm_host_validated_args = ${Boolean(job.hostValidatedArgs)};`;
660
661
  const ok =
661
662
  evaluate(init, "swarm-init.js") &&
662
663
  evaluate(SANDBOX_WEB_SHIMS, "swarm-web-shims.js") &&
@@ -146,6 +146,12 @@ export class QuickJSScriptExecutor implements ScriptExecutor {
146
146
  `quickjs worker failed to start: ${error instanceof Error ? error.message : String(error)}`,
147
147
  );
148
148
  }
149
+ // An abort while the worker booted fired before the listener below
150
+ // exists. From here to postMessage nothing awaits.
151
+ if (input.signal?.aborted) {
152
+ pool.release(slot);
153
+ return emptyOutput("killed");
154
+ }
149
155
 
150
156
  const id = ++requestSeq;
151
157
  const start = performance.now();
@@ -196,6 +202,7 @@ export class QuickJSScriptExecutor implements ScriptExecutor {
196
202
  job: {
197
203
  source: input.source,
198
204
  args: input.args,
205
+ ...(input.hostValidatedArgs ? { hostValidatedArgs: true } : {}),
199
206
  configPayload: input.configPayload,
200
207
  resources: input.resources,
201
208
  network: input.network,
@@ -7,6 +7,27 @@ const EXECUTORS: Record<string, () => ScriptExecutor> = {
7
7
  quickjs: () => new QuickJSScriptExecutor(),
8
8
  };
9
9
 
10
+ export function getDeploymentScriptExecutorName(env: NodeJS.ProcessEnv = process.env): string {
11
+ return (env.SCRIPT_EXECUTOR ?? "native").trim();
12
+ }
13
+
14
+ /**
15
+ * Operator sandbox ceiling: when the deployment sets `SCRIPT_EXECUTOR=quickjs`,
16
+ * individual scripts or agent calls may not escalate to the less-restricted
17
+ * `native` executor. When the deployment runs `native` (the default), scripts
18
+ * may opt down into `quickjs`.
19
+ */
20
+ export function isExecutorAllowedByDeployment(
21
+ requested: string | null | undefined,
22
+ env: NodeJS.ProcessEnv = process.env,
23
+ ): boolean {
24
+ if (!requested) return true;
25
+ if (getDeploymentScriptExecutorName(env) === "quickjs" && requested.trim() === "native") {
26
+ return false;
27
+ }
28
+ return true;
29
+ }
30
+
10
31
  export function getScriptExecutor(name = process.env.SCRIPT_EXECUTOR ?? "native"): ScriptExecutor {
11
32
  const factory = EXECUTORS[name.trim()];
12
33
  if (!factory) {
@@ -16,3 +37,14 @@ export function getScriptExecutor(name = process.env.SCRIPT_EXECUTOR ?? "native"
16
37
  }
17
38
  return factory();
18
39
  }
40
+
41
+ export function resolveScriptExecutor(
42
+ requested?: string | null,
43
+ env: NodeJS.ProcessEnv = process.env,
44
+ ): ScriptExecutor {
45
+ if (!isExecutorAllowedByDeployment(requested, env)) {
46
+ throw new Error('Executor "native" is not permitted when deployment SCRIPT_EXECUTOR="quickjs"');
47
+ }
48
+ const effective = requested ?? env.SCRIPT_EXECUTOR ?? "native";
49
+ return getScriptExecutor(effective);
50
+ }
@@ -3,6 +3,7 @@ import type { ScriptApiConnectionDescriptor, ScriptMcpConnectionDescriptor } fro
3
3
  import type { FailedCredentialBinding, ResolvedCredentialBinding } from "../credential-broker";
4
4
 
5
5
  export type ScriptFsMode = "none" | "workspace-rw";
6
+ export type ScriptExecutorName = "native" | "quickjs";
6
7
 
7
8
  export type EgressSecretEntry = ResolvedCredentialBinding;
8
9
 
@@ -49,6 +50,12 @@ export type ScriptResourcePolicy = {
49
50
  export type ExecutorInput = {
50
51
  source: string;
51
52
  args: unknown;
53
+ /**
54
+ * Trusted host-only flag indicating `args` already passed host JSON Schema
55
+ * validation for this run. Executors may skip `argsSchema.parse(args)` only
56
+ * when this flag is true AND the runtime `argsSchema` is purely structural.
57
+ */
58
+ hostValidatedArgs?: boolean;
52
59
  configPayload: SwarmConfigPayload;
53
60
  resources: ScriptResourcePolicy;
54
61
  fsMode: ScriptFsMode;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Unforgeable host-only capability symbol set on `RunScriptInput` strictly at
3
+ * the host boundary (`POST /api/x/script/{endpointId}`) after `validateJsonSchema`
4
+ * succeeds. Because `Symbol` keys cannot be produced by JSON deserialization,
5
+ * callers that forward request-shaped JSON fields into `RunScriptInput` can
6
+ * never opt out of `argsSchema` validation.
7
+ */
8
+ export const HOST_VALIDATED_ARGS = Symbol("swarm.hostValidatedArgs");
9
+
10
+ /**
11
+ * Checks whether a runtime `argsSchema` (Zod v4 / `zod/mini` schema) and its
12
+ * candidate `value` are semantically equivalent to the host-side JSON Schema
13
+ * validation performed by `validateJsonSchema` in `src/http/x.ts`.
14
+ *
15
+ * Returns `true` only when `schema.parse(value)` is guaranteed to succeed and
16
+ * return a value deeply identical to `value` without running any defaults,
17
+ * coercions, transforms, refinements, or unknown-key stripping.
18
+ */
19
+ export function isHostValidationEquivalent(schema: unknown, value: unknown): boolean {
20
+ if (!schema || typeof schema !== "object") return false;
21
+ const zodInternal = (schema as { _zod?: { def?: Record<string, unknown>; values?: unknown } })
22
+ ._zod;
23
+ const def = zodInternal?.def;
24
+ if (!def || typeof def !== "object") return false;
25
+
26
+ if (def.coerce) return false;
27
+ if (Array.isArray(def.checks) && def.checks.length > 0) return false;
28
+
29
+ switch (def.type) {
30
+ case "string":
31
+ return typeof value === "string" && !def.format;
32
+ case "number":
33
+ return typeof value === "number" && Number.isFinite(value) && !def.format;
34
+ case "boolean":
35
+ return typeof value === "boolean";
36
+ case "literal": {
37
+ const rawValues = def.values;
38
+ const values = Array.isArray(rawValues)
39
+ ? rawValues
40
+ : rawValues instanceof Set
41
+ ? Array.from(rawValues)
42
+ : [];
43
+ return (
44
+ values.length > 0 &&
45
+ values.some((v) => v === value && typeof v !== "bigint" && typeof v !== "symbol")
46
+ );
47
+ }
48
+ case "enum": {
49
+ if (zodInternal?.values instanceof Set) {
50
+ return zodInternal.values.has(value);
51
+ }
52
+ const entriesObj =
53
+ def.entries && typeof def.entries === "object"
54
+ ? (def.entries as Record<string, unknown>)
55
+ : null;
56
+ if (!entriesObj) return false;
57
+ const entries = Object.values(entriesObj).filter(
58
+ (v) => !(typeof v === "string" && typeof entriesObj[v] === "number"),
59
+ );
60
+ return entries.length > 0 && entries.includes(value);
61
+ }
62
+ case "optional":
63
+ return value === undefined
64
+ ? isStructuralSchemaNode(def.innerType)
65
+ : isHostValidationEquivalent(def.innerType, value);
66
+ case "nullable":
67
+ return value === null
68
+ ? isStructuralSchemaNode(def.innerType)
69
+ : isHostValidationEquivalent(def.innerType, value);
70
+ case "array":
71
+ return (
72
+ Array.isArray(value) &&
73
+ isStructuralSchemaNode(def.element) &&
74
+ value.every((item) => isHostValidationEquivalent(def.element, item))
75
+ );
76
+ case "object": {
77
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
78
+ const rawShape = typeof def.shape === "function" ? def.shape() : def.shape;
79
+ if (!rawShape || typeof rawShape !== "object") return false;
80
+ const shape = rawShape as Record<string, unknown>;
81
+ const obj = value as Record<string, unknown>;
82
+
83
+ // If `value` carries extra keys not declared in `shape`, let `.parse()`
84
+ // handle key stripping (`z.object`), passthrough (`z.looseObject`), or
85
+ // rejection (`z.strictObject`).
86
+ for (const key of Object.keys(obj)) {
87
+ if (!Object.hasOwn(shape, key)) return false;
88
+ }
89
+
90
+ for (const [key, propSchema] of Object.entries(shape)) {
91
+ // Retain Zod's property lookup semantics: if `key` is absent as an own
92
+ // property on `obj` but resolves via `Object.prototype` (e.g. `constructor`,
93
+ // `toString`), fall back to `.parse()` so Zod validates/rejects the
94
+ // inherited prototype value identically to `main`.
95
+ if (!Object.hasOwn(obj, key) && key in obj) return false;
96
+ const propValue = obj[key];
97
+ if (!isHostValidationEquivalent(propSchema, propValue)) return false;
98
+ }
99
+ return true;
100
+ }
101
+ default:
102
+ return false;
103
+ }
104
+ }
105
+
106
+ function isStructuralSchemaNode(schema: unknown): boolean {
107
+ if (!schema || typeof schema !== "object") return false;
108
+ const def = (schema as { _zod?: { def?: Record<string, unknown> } })._zod?.def;
109
+ if (!def || typeof def !== "object") return false;
110
+ if (def.coerce) return false;
111
+ if (Array.isArray(def.checks) && def.checks.length > 0) return false;
112
+ switch (def.type) {
113
+ case "string":
114
+ case "number":
115
+ return !def.format;
116
+ case "boolean":
117
+ case "literal":
118
+ case "enum":
119
+ return true;
120
+ case "optional":
121
+ case "nullable":
122
+ return isStructuralSchemaNode(def.innerType);
123
+ case "array":
124
+ return isStructuralSchemaNode(def.element);
125
+ case "object": {
126
+ const rawShape = typeof def.shape === "function" ? def.shape() : def.shape;
127
+ if (!rawShape || typeof rawShape !== "object") return false;
128
+ return Object.values(rawShape as Record<string, unknown>).every(isStructuralSchemaNode);
129
+ }
130
+ default:
131
+ return false;
132
+ }
133
+ }
@@ -6,16 +6,18 @@ import { createValueScrubber, scrubObject, scrubSecrets } from "../utils/secret-
6
6
  import type { ScriptApiConnectionDescriptor, ScriptMcpConnectionDescriptor } from "./api-types";
7
7
  import type { FailedCredentialBinding } from "./credential-broker";
8
8
  import { buildEgressSecrets } from "./egress-secrets";
9
- import { getScriptExecutor } from "./executors/registry";
9
+ import { isExecutorAllowedByDeployment, resolveScriptExecutor } from "./executors/registry";
10
10
  import {
11
11
  DEFAULT_SCRIPT_RESOURCES,
12
12
  type EgressSecretEntry,
13
13
  type ExecutorOutput,
14
14
  type ScriptAgentFsDescriptor,
15
+ type ScriptExecutorName,
15
16
  type ScriptFsMode,
16
17
  type ScriptResourcePolicy,
17
18
  type SwarmConfigPayload,
18
19
  } from "./executors/types";
20
+ import { HOST_VALIDATED_ARGS } from "./host-validated-args";
19
21
  import { validateScriptImports } from "./import-allowlist";
20
22
 
21
23
  /**
@@ -38,7 +40,19 @@ const CAPACITY_EXCEEDED_RETRY_DELAYS_MS = [200, 750];
38
40
  export type RunScriptInput = {
39
41
  source: string;
40
42
  args?: unknown;
43
+ /**
44
+ * Unforgeable host-only capability symbol set only when the host has already
45
+ * validated `args` against the script's `argsJsonSchema` (e.g. `POST /api/x/script/{endpointId}`).
46
+ * Keyed by a `Symbol` so request-shaped JSON inputs forwarded into `RunScriptInput`
47
+ * can never opt out of `argsSchema` validation.
48
+ */
49
+ [HOST_VALIDATED_ARGS]?: boolean;
41
50
  fsMode?: ScriptFsMode;
51
+ /**
52
+ * Optional per-script executor override ("native" | "quickjs"). When null or
53
+ * undefined, `getScriptExecutor` falls back to `SCRIPT_EXECUTOR ?? "native"`.
54
+ */
55
+ executor?: ScriptExecutorName | null;
42
56
  agentId: string;
43
57
  /**
44
58
  * Bearer for the run's `ctx.swarm` SDK calls. Api-side callers pass a
@@ -118,6 +132,18 @@ export async function runScript(input: RunScriptInput): Promise<RunScriptOutput>
118
132
  };
119
133
  }
120
134
 
135
+ if (!isExecutorAllowedByDeployment(input.executor)) {
136
+ return {
137
+ result: undefined,
138
+ stdout: "",
139
+ stderr: 'Executor "native" is not permitted when deployment SCRIPT_EXECUTOR="quickjs"',
140
+ truncated: { stdout: false, stderr: false },
141
+ durationMs: 0,
142
+ exitCode: 1,
143
+ error: "executor_error",
144
+ };
145
+ }
146
+
121
147
  // The global scrubber skips values under its length floor, so this run's own
122
148
  // secret config is redacted here at any length before the global pass. Built
123
149
  // before the import check: its diagnostic quotes source, which can hold a value.
@@ -150,6 +176,7 @@ export async function runScript(input: RunScriptInput): Promise<RunScriptOutput>
150
176
  const executorInput = {
151
177
  source: input.source,
152
178
  args: input.args ?? null,
179
+ ...(input[HOST_VALIDATED_ARGS] === true ? { hostValidatedArgs: true } : {}),
153
180
  configPayload,
154
181
  resources,
155
182
  fsMode: input.fsMode ?? "none",
@@ -157,11 +184,11 @@ export async function runScript(input: RunScriptInput): Promise<RunScriptOutput>
157
184
  signal: input.signal,
158
185
  };
159
186
 
160
- let output = await getScriptExecutor().run(executorInput);
187
+ let output = await resolveScriptExecutor(input.executor).run(executorInput);
161
188
  for (const delayMs of CAPACITY_EXCEEDED_RETRY_DELAYS_MS) {
162
189
  if (output.error !== "capacity_exceeded" || input.signal?.aborted) break;
163
190
  await Bun.sleep(delayMs);
164
- output = await getScriptExecutor().run(executorInput);
191
+ output = await resolveScriptExecutor(input.executor).run(executorInput);
165
192
  }
166
193
 
167
194
  return {
@@ -8,6 +8,7 @@ type BridgeRequest = {
8
8
  method: string;
9
9
  path: string;
10
10
  body?: unknown;
11
+ headers?: Record<string, string>;
11
12
  };
12
13
 
13
14
  function headers(config: SwarmConfig): Record<string, string> {
@@ -362,10 +363,16 @@ function bridgeRequestFor(name: string, args: unknown): BridgeRequest | null {
362
363
  case "workflow_trigger": {
363
364
  const id = typeof body.id === "string" ? body.id : undefined;
364
365
  if (!id) throw new Error("workflow_trigger requires string `id`");
366
+ const path = `/api/workflows/${encodeURIComponent(id)}/trigger`;
365
367
  return {
366
368
  method: "POST",
367
- path: `/api/workflows/${encodeURIComponent(id)}/trigger`,
369
+ path: body.wait === false ? `${path}?wait=false` : path,
368
370
  body: body.triggerData ?? {},
371
+ // The route's own Idempotency-Key: recorded in the run's transaction,
372
+ // so a retry after a kill replays the run instead of starting another.
373
+ ...(typeof body.idempotencyKey === "string"
374
+ ? { headers: { "Idempotency-Key": body.idempotencyKey } }
375
+ : {}),
369
376
  };
370
377
  }
371
378
  case "workflow_retryRun": {
@@ -491,7 +498,7 @@ async function callBridgeApi(
491
498
 
492
499
  const res = await fetch(`${baseUrl}${request.path}`, {
493
500
  method: request.method,
494
- headers: headers(config),
501
+ headers: { ...headers(config), ...request.headers },
495
502
  body: request.body === undefined ? undefined : JSON.stringify(request.body),
496
503
  });
497
504
  const data = await readScriptSdkJsonResponse(res, `ctx.swarm.${name}`);
@@ -26,6 +26,7 @@ declare module "swarm-sdk" {
26
26
  | { [key: string]: JsonValue };
27
27
  export type ScriptScope = "agent" | "global";
28
28
  export type ScriptFsMode = "none" | "workspace-rw";
29
+ export type ScriptExecutorName = "native" | "quickjs";
29
30
  export type ScriptApiRawOptions = { raw: true };
30
31
  export type ScriptApiDefaultOptions = { raw?: false };
31
32
  export type RoutingReason =
@@ -304,6 +305,7 @@ declare module "swarm-sdk" {
304
305
  intent?: string;
305
306
  scope?: ScriptScope;
306
307
  fsMode?: ScriptFsMode;
308
+ executor?: ScriptExecutorName | null;
307
309
  idempotencyKey?: string;
308
310
  }): Promise<unknown>;
309
311
  // --- swarm / agent ---
@@ -535,7 +537,12 @@ declare module "swarm-sdk" {
535
537
  workflow_patch(args: Record<string, unknown>): Promise<unknown>;
536
538
  workflow_patchNode(args: Record<string, unknown>): Promise<unknown>;
537
539
  workflow_delete(args: { id: string }): Promise<unknown>;
538
- workflow_trigger(args: { id: string; triggerData?: Record<string, unknown> }): Promise<unknown>;
540
+ workflow_trigger(args: {
541
+ id: string;
542
+ triggerData?: Record<string, unknown>;
543
+ wait?: boolean;
544
+ idempotencyKey?: string;
545
+ }): Promise<unknown>;
539
546
  workflow_retryRun(args: { id: string }): Promise<unknown>;
540
547
  workflow_cancelRun(args: { id: string }): Promise<unknown>;
541
548
 
@@ -552,6 +559,7 @@ declare module "swarm-sdk" {
552
559
  intent?: string;
553
560
  scope?: ScriptScope;
554
561
  fsMode?: ScriptFsMode;
562
+ executor?: ScriptExecutorName | null;
555
563
  }): Promise<unknown>;
556
564
  script_delete(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
557
565
  script_queryTypes(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
@@ -8,6 +8,7 @@ declare module "swarm-sdk" {
8
8
  | { [key: string]: JsonValue };
9
9
  export type ScriptScope = "agent" | "global";
10
10
  export type ScriptFsMode = "none" | "workspace-rw";
11
+ export type ScriptExecutorName = "native" | "quickjs";
11
12
  export type ScriptApiRawOptions = { raw: true };
12
13
  export type ScriptApiDefaultOptions = { raw?: false };
13
14
  export type RoutingReason =
@@ -286,6 +287,7 @@ declare module "swarm-sdk" {
286
287
  intent?: string;
287
288
  scope?: ScriptScope;
288
289
  fsMode?: ScriptFsMode;
290
+ executor?: ScriptExecutorName | null;
289
291
  idempotencyKey?: string;
290
292
  }): Promise<unknown>;
291
293
  // --- swarm / agent ---
@@ -517,7 +519,12 @@ declare module "swarm-sdk" {
517
519
  workflow_patch(args: Record<string, unknown>): Promise<unknown>;
518
520
  workflow_patchNode(args: Record<string, unknown>): Promise<unknown>;
519
521
  workflow_delete(args: { id: string }): Promise<unknown>;
520
- workflow_trigger(args: { id: string; triggerData?: Record<string, unknown> }): Promise<unknown>;
522
+ workflow_trigger(args: {
523
+ id: string;
524
+ triggerData?: Record<string, unknown>;
525
+ wait?: boolean;
526
+ idempotencyKey?: string;
527
+ }): Promise<unknown>;
521
528
  workflow_retryRun(args: { id: string }): Promise<unknown>;
522
529
  workflow_cancelRun(args: { id: string }): Promise<unknown>;
523
530
 
@@ -534,6 +541,7 @@ declare module "swarm-sdk" {
534
541
  intent?: string;
535
542
  scope?: ScriptScope;
536
543
  fsMode?: ScriptFsMode;
544
+ executor?: ScriptExecutorName | null;
537
545
  }): Promise<unknown>;
538
546
  script_delete(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
539
547
  script_queryTypes(args: { name: string; scope?: ScriptScope }): Promise<unknown>;