@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
@@ -0,0 +1,47 @@
1
+ import { runScriptWithRunCredential } from "../be/scripts/run-credential";
2
+ import { scrubSecrets } from "../utils/secret-scrubber";
3
+
4
+ const PROBE_TIMEOUT_MS = 10_000;
5
+
6
+ /** Exercise the same executor and per-run credential as the gate, with a deadline even if the runner stalls. */
7
+ export async function probeScriptRunner(
8
+ agentId: string,
9
+ runner: typeof runScriptWithRunCredential = runScriptWithRunCredential,
10
+ ): Promise<{ quiet: false; reason: string } | null> {
11
+ const controller = new AbortController();
12
+ let timer: ReturnType<typeof setTimeout> | undefined;
13
+ try {
14
+ const deadline = new Promise<never>((_, reject) => {
15
+ timer = setTimeout(() => {
16
+ controller.abort();
17
+ reject(new Error("timeout after 10000ms"));
18
+ }, PROBE_TIMEOUT_MS);
19
+ });
20
+ const output = await Promise.race([
21
+ runner({
22
+ source: "export default async function () { return true; }",
23
+ agentId,
24
+ fsMode: "none",
25
+ timeoutMs: PROBE_TIMEOUT_MS,
26
+ signal: controller.signal,
27
+ }),
28
+ deadline,
29
+ ]);
30
+ if (output.exitCode !== 0 || output.error || output.runtimeError) {
31
+ throw new Error(
32
+ output.runtimeError?.message ??
33
+ output.error ??
34
+ `script exited with code ${output.exitCode}`,
35
+ );
36
+ }
37
+ return null;
38
+ } catch (error) {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ return {
41
+ quiet: false,
42
+ reason: `script-runner-unhealthy: ${scrubSecrets(message)}`.slice(0, 300),
43
+ };
44
+ } finally {
45
+ clearTimeout(timer);
46
+ }
47
+ }
@@ -243,14 +243,28 @@ const getAvailable = route({
243
243
  pattern: ["api", "keys", "available"],
244
244
  summary: "Get available (non-rate-limited) key indices for a credential type",
245
245
  tags: ["API Keys"],
246
- query: z.object({
247
- keyType: z.string(),
248
- totalKeys: z.coerce.number().int().min(1),
249
- scope: z.string().optional(),
250
- scopeId: z.string().optional(),
251
- /** Model family the caller is about to run. Filters out keys with an active weekly window block for that family. */
252
- model: z.enum(["fable", "opus", "sonnet", "haiku"]).optional(),
253
- }),
246
+ query: z
247
+ .object({
248
+ keyType: z.string(),
249
+ totalKeys: z.coerce.number().int().min(1),
250
+ scope: z.string().optional(),
251
+ scopeId: z.string().optional(),
252
+ /** Model family the caller is about to run. Filters out keys with an active weekly window block for that family. */
253
+ model: z.enum(["fable", "opus", "sonnet", "haiku"]).optional(),
254
+ /**
255
+ * Comma-separated key suffixes of the caller's pool, in pool order. When set,
256
+ * key status is matched by suffix across all scopes (not by stored index or
257
+ * `scope`), and returned indices are positions in this list.
258
+ */
259
+ keySuffixes: z
260
+ .string()
261
+ .transform((value) => value.split(","))
262
+ .optional(),
263
+ })
264
+ .refine((query) => !query.keySuffixes || query.keySuffixes.length === query.totalKeys, {
265
+ message: "keySuffixes must list exactly totalKeys suffixes",
266
+ path: ["keySuffixes"],
267
+ }),
254
268
  responses: {
255
269
  200: {
256
270
  description: "List of available key indices",
@@ -690,7 +704,7 @@ export async function handleApiKeys(
690
704
  const parsed = await getAvailable.parse(req, res, pathSegments, queryParams);
691
705
  if (!parsed) return true;
692
706
 
693
- const { keyType, totalKeys, scope, scopeId, model } = parsed.query;
707
+ const { keyType, totalKeys, scope, scopeId, model, keySuffixes } = parsed.query;
694
708
  try {
695
709
  const result = await getAvailableKeyIndices(
696
710
  keyType,
@@ -698,6 +712,7 @@ export async function handleApiKeys(
698
712
  scope,
699
713
  scopeId ?? null,
700
714
  model,
715
+ keySuffixes,
701
716
  );
702
717
  getAvailable.respond(res, 200, {
703
718
  success: true,
@@ -8,7 +8,7 @@ import { getRequestAuth } from "../utils/request-auth-context";
8
8
  import { jsonError } from "./utils";
9
9
 
10
10
  /** Visible ASCII, at most 255 characters: room for a UUID, a ULID or a client's own scheme. */
11
- const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{1,255}$/;
11
+ export const IDEMPOTENCY_KEY_PATTERN = /^[\x21-\x7E]{1,255}$/;
12
12
 
13
13
  /** Set on a response answered from an earlier accepted request. */
14
14
  export const IDEMPOTENT_REPLAYED_HEADER = "Idempotent-Replayed";
package/src/http/index.ts CHANGED
@@ -517,7 +517,7 @@ async function shutdown() {
517
517
  await stopChatSdk();
518
518
 
519
519
  // Stop OAuth keepalive
520
- if (process.env.OAUTH_KEEPALIVE_DISABLE !== "true") {
520
+ if (!isEnvFlagEnabled("OAUTH_KEEPALIVE_DISABLE", false)) {
521
521
  const { stopOAuthKeepalive } = await import("../oauth/keepalive");
522
522
  await stopOAuthKeepalive();
523
523
  }
@@ -847,7 +847,7 @@ httpServer
847
847
  startRuntimeSupervisor();
848
848
 
849
849
  // Start OAuth token keepalive (proactive refresh to prevent expiry)
850
- if (process.env.OAUTH_KEEPALIVE_DISABLE !== "true") {
850
+ if (!isEnvFlagEnabled("OAUTH_KEEPALIVE_DISABLE", false)) {
851
851
  const { startOAuthKeepalive } = await import("../oauth/keepalive");
852
852
  startOAuthKeepalive();
853
853
  }
@@ -855,7 +855,7 @@ httpServer
855
855
  // Start generic OAuth token refresh sweep (all oauth_apps providers,
856
856
  // 15-min tick, first run ~1 min after boot). Complements the tracker-only
857
857
  // keepalive above — see src/be/oauth-refresh-sweep.ts.
858
- if (process.env.OAUTH_REFRESH_SWEEP_DISABLE !== "true") {
858
+ if (!isEnvFlagEnabled("OAUTH_REFRESH_SWEEP_DISABLE", false)) {
859
859
  const { startOAuthRefreshSweep } = await import("../be/oauth-refresh-sweep");
860
860
  startOAuthRefreshSweep();
861
861
  }
@@ -308,13 +308,45 @@ function getAppBaseUrl(): string {
308
308
  return getAppUrl(LOCAL_PAGE_APP_URL);
309
309
  }
310
310
 
311
+ // CSP host-source without a path: optional http(s) scheme, a host that may
312
+ // start with a `*.` wildcard label, and an optional port (digits or `*`).
313
+ // Bare `*`, keywords (`'none'`, `'self'`) and anything carrying `;` or a path
314
+ // are rejected so a bad env value cannot widen or break the policy.
315
+ const FRAME_ANCESTOR_SOURCE_RE =
316
+ /^(?:https?:\/\/)?(?:\*\.)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?::(?:\d{1,5}|\*))?$/i;
317
+
318
+ let cachedFrameAncestors: { raw: string; origins: string[] } | null = null;
319
+
320
+ /**
321
+ * Parse `PAGES_FRAME_ANCESTORS`: extra origins allowed to iframe PUBLIC pages
322
+ * (for example a marketing site embedding a chart). Comma or space separated.
323
+ * Invalid entries are dropped with one warning per distinct env value; the
324
+ * parse is cached on the raw string so a config reload picks up changes
325
+ * without re-warning on every request.
326
+ */
327
+ export function getPageFrameAncestors(): string[] {
328
+ const raw = process.env.PAGES_FRAME_ANCESTORS ?? "";
329
+ if (cachedFrameAncestors?.raw === raw) return cachedFrameAncestors.origins;
330
+ const origins: string[] = [];
331
+ for (const entry of raw.split(/[\s,]+/)) {
332
+ const value = entry.replace(/\/+$/, "");
333
+ if (!value) continue;
334
+ if (FRAME_ANCESTOR_SOURCE_RE.test(value)) origins.push(value);
335
+ else console.warn(`[pages] ignoring invalid PAGES_FRAME_ANCESTORS entry: ${entry}`);
336
+ }
337
+ cachedFrameAncestors = { raw, origins };
338
+ return origins;
339
+ }
340
+
311
341
  /**
312
342
  * Build the `Content-Security-Policy` for the served HTML. Allows inline
313
343
  * scripts (required for `BROWSER_SDK_JS`) but locks down everything else to
314
344
  * `'self'`. The SPA iframes the page in step-6 with `sandbox="allow-scripts
315
- * allow-forms"`; the CSP is a defence-in-depth layer.
345
+ * allow-forms"`; the CSP is a defence-in-depth layer. `PAGES_FRAME_ANCESTORS`
346
+ * applies only to public pages: authed and password pages stay embeddable by
347
+ * the app origins alone.
316
348
  */
317
- function buildCsp(): string {
349
+ function buildCsp(page: Page): string {
318
350
  // `frame-ancestors` lists every origin allowed to iframe `/p/:id`. We must
319
351
  // include the SPA origin(s). Configured app URLs may be comma-separated so
320
352
  // portless dev (`https://ui.swarm.localhost`), a Vite port (`http://localhost:5274`),
@@ -333,7 +365,8 @@ function buildCsp(): string {
333
365
  "https://*.localhost",
334
366
  "http://*.localhost",
335
367
  ];
336
- const ancestors = Array.from(new Set([...configured, ...devFallbacks]));
368
+ const embedders = page.authMode === "public" ? getPageFrameAncestors() : [];
369
+ const ancestors = Array.from(new Set([...configured, ...devFallbacks, ...embedders]));
337
370
  // Allow Tailwind Play CDN (`cdn.tailwindcss.com`) for scripts, Google
338
371
  // Fonts (`fonts.googleapis.com` stylesheets + `fonts.gstatic.com` font
339
372
  // files) for the swarm default typography, and same-origin /@swarm/api/*
@@ -609,7 +642,7 @@ export async function handlePagesPublic(
609
642
  const headers: Record<string, string | string[]> = {
610
643
  "Content-Type": "text/html; charset=utf-8",
611
644
  "Cache-Control": "no-store",
612
- "Content-Security-Policy": buildCsp(),
645
+ "Content-Security-Policy": buildCsp(page),
613
646
  "X-Content-Type-Options": "nosniff",
614
647
  };
615
648
  if (inlineSetCookie.length > 0) headers["Set-Cookie"] = inlineSetCookie;
@@ -642,7 +675,7 @@ export async function handlePagesPublic(
642
675
  const headers: Record<string, string | string[]> = {
643
676
  "Content-Type": "text/html; charset=utf-8",
644
677
  "Cache-Control": "no-store",
645
- "Content-Security-Policy": buildCsp(),
678
+ "Content-Security-Policy": buildCsp(page),
646
679
  // Defence-in-depth: prevent MIME sniffing and clickjacking outside the SPA.
647
680
  "X-Content-Type-Options": "nosniff",
648
681
  };
@@ -40,12 +40,14 @@ import {
40
40
  typecheckScript,
41
41
  } from "../be/scripts/typecheck";
42
42
  import { can } from "../rbac";
43
+ import { isExecutorAllowedByDeployment } from "../scripts-runtime/executors/registry";
43
44
  import { extractScriptSignature, type ScriptSignature } from "../scripts-runtime/extract-signature";
44
45
  import { checkQuickJSCompat, quickJSCompatWarnings } from "../scripts-runtime/quickjs-compat";
45
46
  import {
46
47
  ScriptApiAuthModeSchema,
47
48
  ScriptApiRecordSchema,
48
49
  type ScriptDetail,
50
+ ScriptExecutorNameSchema,
49
51
  ScriptFsModeSchema,
50
52
  type ScriptListItem,
51
53
  type ScriptRecord,
@@ -67,6 +69,7 @@ const upsertBodySchema = z.object({
67
69
  intent: z.string().default(""),
68
70
  scope: ScriptScopeSchema.default("agent"),
69
71
  fsMode: ScriptFsModeSchema.default("none"),
72
+ executor: ScriptExecutorNameSchema.nullable().optional(),
70
73
  });
71
74
 
72
75
  const runBodySchema = z
@@ -77,6 +80,7 @@ const runBodySchema = z
77
80
  intent: z.string().default(""),
78
81
  scope: ScriptScopeSchema.optional(),
79
82
  fsMode: ScriptFsModeSchema.default("none"),
83
+ executor: ScriptExecutorNameSchema.nullable().optional(),
80
84
  idempotencyKey: z.string().max(200).optional(),
81
85
  })
82
86
  .refine((body) => Boolean(body.name) !== Boolean(body.source), {
@@ -671,6 +675,15 @@ export async function handleScripts(
671
675
  }
672
676
  }
673
677
 
678
+ if (parsed.body.executor && !isExecutorAllowedByDeployment(parsed.body.executor)) {
679
+ jsonError(
680
+ res,
681
+ 'Executor "native" is not permitted when deployment SCRIPT_EXECUTOR="quickjs"',
682
+ 400,
683
+ );
684
+ return true;
685
+ }
686
+
674
687
  const secretRefusal = refuseSourceWithSecrets(parsed.body.source);
675
688
  if (secretRefusal) {
676
689
  json(res, secretRefusal, 400);
@@ -708,6 +721,7 @@ export async function handleScripts(
708
721
  signatureJson: signatureJsonFor(parsed.body.source),
709
722
  argsJsonSchema,
710
723
  fsMode: parsed.body.fsMode,
724
+ executor: parsed.body.executor,
711
725
  agentId: agent.id,
712
726
  isScratch: false,
713
727
  typeChecked: true,
@@ -758,6 +772,7 @@ export async function handleScripts(
758
772
 
759
773
  let source = parsed.body.source;
760
774
  let fsMode = parsed.body.fsMode;
775
+ let executor = parsed.body.executor ?? null;
761
776
  let namedScript: ScriptRecord | null = null;
762
777
  if (parsed.body.name) {
763
778
  const script = await resolveScript(parsed.body.name, agent.id, parsed.body.scope);
@@ -768,6 +783,7 @@ export async function handleScripts(
768
783
  namedScript = script;
769
784
  source = script.source;
770
785
  fsMode = script.fsMode;
786
+ executor = parsed.body.executor !== undefined ? parsed.body.executor : script.executor;
771
787
  }
772
788
 
773
789
  if (fsMode === "workspace-rw") {
@@ -775,6 +791,15 @@ export async function handleScripts(
775
791
  return true;
776
792
  }
777
793
 
794
+ if (executor && !isExecutorAllowedByDeployment(executor)) {
795
+ jsonError(
796
+ res,
797
+ 'Executor "native" is not permitted when deployment SCRIPT_EXECUTOR="quickjs"',
798
+ 400,
799
+ );
800
+ return true;
801
+ }
802
+
778
803
  const startedAt = new Date().toISOString();
779
804
  // Touch before executing so an already-stale scratch script isn't reaped
780
805
  // by the retention sweep while this run is still in flight.
@@ -786,6 +811,7 @@ export async function handleScripts(
786
811
  source: source as string,
787
812
  args: parsed.body.args,
788
813
  fsMode,
814
+ executor,
789
815
  agentId: agent.id,
790
816
  runtimeInstanceId,
791
817
  egressSecrets: credentials.egressSecrets,
@@ -954,6 +980,7 @@ export async function handleScripts(
954
980
  isScratch: script.isScratch,
955
981
  typeChecked: script.typeChecked,
956
982
  fsMode: script.fsMode,
983
+ executor: script.executor,
957
984
  createdByAgentId: script.createdByAgentId,
958
985
  createdAt: script.createdAt,
959
986
  updatedAt: script.updatedAt,
@@ -307,6 +307,15 @@ const triggerWorkflowRoute = route({
307
307
  "With an `Idempotency-Key`, a retry returns the accepted run's `runId` and `skipped` without starting, resuming or waiting for anything, even after the workflow was disabled or edited and whatever state the run is in now.",
308
308
  tags: ["Workflows"],
309
309
  params: z.object({ id: z.string() }),
310
+ query: z.object({
311
+ wait: z
312
+ .enum(["true", "false"])
313
+ .optional()
314
+ .describe(
315
+ "Default true: respond after the run's instant steps ran, when it finishes or parks at its first async step. " +
316
+ "false: respond as soon as the run is committed and walk it in the background.",
317
+ ),
318
+ }),
310
319
  headers: idempotencyKeyHeader("starts one run"),
311
320
  responses: {
312
321
  201: {
@@ -1002,6 +1011,7 @@ export async function handleWorkflows(
1002
1011
  runId = await startWorkflowExecution(workflow, body, getExecutorRegistry(), {
1003
1012
  triggerType: "api",
1004
1013
  requestedByUserId: (await resolveHttpAuditUserId(req, myAgentId)) ?? undefined,
1014
+ detach: parsed.query.wait === "false",
1005
1015
  // Recorded in the run's own transaction, before the walk starts.
1006
1016
  accept: claim
1007
1017
  ? async (run) => {
package/src/http/x.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  MAX_SCRIPT_WALL_CLOCK_MS,
21
21
  MIN_SCRIPT_WALL_CLOCK_MS,
22
22
  } from "../scripts-runtime/executors/types";
23
+ import { HOST_VALIDATED_ARGS } from "../scripts-runtime/host-validated-args";
23
24
  import type { RunScriptOutput } from "../scripts-runtime/loader";
24
25
  import { scrubObject, scrubSecrets } from "../utils/secret-scrubber";
25
26
  import { validateJsonSchema } from "../workflows/json-schema-validator";
@@ -214,6 +215,7 @@ export async function handleX(
214
215
  // Scripts predating the schema column, or where extraction failed, have a
215
216
  // null schema — we skip and let the in-subprocess Zod check surface as a
216
217
  // runtime_error instead.
218
+ let hostValidatedArgs = false;
217
219
  if (script.argsJsonSchema) {
218
220
  try {
219
221
  const schema = JSON.parse(script.argsJsonSchema) as Record<string, unknown>;
@@ -227,6 +229,7 @@ export async function handleX(
227
229
  });
228
230
  return true;
229
231
  }
232
+ hostValidatedArgs = true;
230
233
  } catch {
231
234
  // Malformed stored schema — don't block execution on it.
232
235
  }
@@ -245,7 +248,9 @@ export async function handleX(
245
248
  const output = await runScriptWithRunCredential({
246
249
  source: script.source,
247
250
  args: args ?? null,
251
+ [HOST_VALIDATED_ARGS]: hostValidatedArgs,
248
252
  fsMode: "none",
253
+ executor: script.executor,
249
254
  agentId: endpoint.agentId,
250
255
  // timeoutMs only raises the wall-clock limit (network-bound scripts can use
251
256
  // the full window); the CPU ulimit (`ulimit -t`) is intentionally left at
@@ -271,12 +271,22 @@ export const getBasePrompt = async (args: BasePromptArgs): Promise<string> => {
271
271
  renderToolsAndSkillsVars({
272
272
  skillsSummary: args.skillsSummary,
273
273
  mcpServers: args.mcpServers,
274
+ // pi defers installed MCP servers together with swarm tools.
275
+ mcpToolsDeferred: provider === "pi" && hasToolSearch,
274
276
  nativeSkillDiscovery,
275
277
  toolDiscovery: discoveryResult.text,
276
278
  hasLocalEnv,
277
279
  }),
278
280
  );
279
281
  prompt += toolsResult.text;
282
+
283
+ // Lead docs pointer: only when the lead has the `swarm-docs` skill. The
284
+ // skill list is unknown on claude-managed (the cloud sandbox owns skill
285
+ // delivery) and before the first skill load, so render it then.
286
+ if (role === "lead" && hasSwarmDocsSkill(args.skillsSummary)) {
287
+ const docsResult = await resolveTemplateAsync("system.agent.lead.docs", {});
288
+ prompt += docsResult.text;
289
+ }
280
290
  }
281
291
 
282
292
  // J. Agent notes (CLAUDE.md), only when it differs from the generated
@@ -339,6 +349,11 @@ export const getTaskContextPrompt = async (args: TaskContextPromptArgs): Promise
339
349
  return prompt.replace(/\n{3,}/g, "\n\n").trim();
340
350
  };
341
351
 
352
+ /** True when the skill list includes `swarm-docs`, or when the list is unknown. */
353
+ function hasSwarmDocsSkill(skills: { name: string }[] | undefined): boolean {
354
+ return skills === undefined || skills.some((skill) => skill.name === "swarm-docs");
355
+ }
356
+
342
357
  /**
343
358
  * Dynamic lines for `system.agent.tools_skills`. The static text lives in the
344
359
  * template so operators can override or skip the section; only the lists that
@@ -347,6 +362,7 @@ export const getTaskContextPrompt = async (args: TaskContextPromptArgs): Promise
347
362
  function renderToolsAndSkillsVars(input: {
348
363
  skillsSummary?: { name: string; description: string }[];
349
364
  mcpServers?: string[];
365
+ mcpToolsDeferred?: boolean;
350
366
  nativeSkillDiscovery: boolean;
351
367
  hasLocalEnv: boolean;
352
368
  toolDiscovery: string;
@@ -382,7 +398,11 @@ function renderToolsAndSkillsVars(input: {
382
398
  const servers = (input.mcpServers ?? []).filter((name) => name.trim().length > 0);
383
399
  const mcpLine =
384
400
  servers.length > 0
385
- ? `Connected MCP servers: ${servers.join(", ")}. Their tools are in your tool list.\n`
401
+ ? `Connected MCP servers: ${servers.join(", ")}. ${
402
+ input.mcpToolsDeferred
403
+ ? "Load their tools with your harness tool search."
404
+ : "Their tools are in your tool list."
405
+ }\n`
386
406
  : "";
387
407
 
388
408
  return {
@@ -15,8 +15,9 @@
15
15
  * G secrets system.agent.secrets
16
16
  *
17
17
  * Deployment-gated blocks (E outputs, H slack, steering) and the per-agent and
18
- * per-task sections (tools and skills, agent notes, repository, requester
19
- * profile) are appended by base-prompt.ts after the composite.
18
+ * per-task sections (tools and skills, the lead docs pointer, agent notes,
19
+ * repository, requester profile) are appended by base-prompt.ts after the
20
+ * composite.
20
21
  *
21
22
  * Writing rules for every body: sentence case headings, one idea per sentence,
22
23
  * active voice, plain words, no em dashes, a prohibition only where no positive
@@ -109,6 +110,19 @@ Your heartbeat runbook is the \`heartbeatMd\` profile field. Edit it with \`upda
109
110
  category: "system",
110
111
  });
111
112
 
113
+ // Lead docs pointer. base-prompt.ts appends it after the tools and skills
114
+ // section when the lead has the `swarm-docs` skill (or when the skill list is
115
+ // unknown). An operator override with an empty body switches it off.
116
+ registerTemplate({
117
+ eventType: "system.agent.lead.docs",
118
+ header: "",
119
+ defaultBody: `
120
+ Load the \`swarm-docs\` skill only when a person asks for something no skill, workflow, schedule or memory of yours covers, or asks where something is documented. Then follow its offer rules.
121
+ `,
122
+ variables: [],
123
+ category: "system",
124
+ });
125
+
112
126
  // Remote providers (no MCP, no container): the task arrives in the session
113
127
  // prompt and the final message is the output. No tools to name.
114
128
  registerTemplate({
@@ -168,7 +182,7 @@ registerTemplate({
168
182
  ## Memory
169
183
 
170
184
  Memories from past sessions are in your task message. Read them before you start. For a wider search, run the \`task-context-gathering\` script with the task ID and two to four queries.
171
- Store a learning with \`memory-store\` when you solve something that will come back: a fix, a pattern, a gotcha. Completed task outputs are stored for you.
185
+ When you learn something that would change how the next agent works (a fix, a pattern, a gotcha), pass it as \`learning\` on a \`store-progress\` call before the task ends. Completed task outputs are stored for you.
172
186
  You MUST use the \`memory\` skill before you store, edit, or delete a memory.
173
187
  `,
174
188
  variables: [],
@@ -45,7 +45,7 @@ import type {
45
45
  * default; the image installs exactly this version and the per-task settings
46
46
  * set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
47
47
  */
48
- export const AMP_PACKAGE = "@ampcode/cli@0.0.1792929650-g000512";
48
+ export const AMP_PACKAGE = "@ampcode/cli@0.0.1793025586-g9211b5";
49
49
 
50
50
  /** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
51
51
  const AMP_MCP_SERVER_NAME = "agent-swarm";
@@ -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-rc.3";
40
+ export const DSH_PACKAGE = "@deepseek-ai/dsh@0.2.1-rc.4";
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()
@@ -382,14 +382,15 @@ type ToolStructuredContent = NonNullable<AgentToolResult["structuredContent"]>;
382
382
  export const SWARM_TOOL_NAMESPACE = { name: "agent-swarm" } as const;
383
383
 
384
384
  /**
385
- * `PI_TOOL_DEFERRAL`: hide non-core swarm tools behind pi's `tool_search`.
386
- * Off by default until a pilot measures the prompt-cache cost of mid-session
387
- * tool-set changes. Read from `process.env` by both the adapter traits (which
388
- * pick the prompt's tool-discovery line) and `createSession`, so the prompt
389
- * and the session always agree.
385
+ * `PI_TOOL_DEFERRAL`: hide non-core swarm tools, and installed MCP server
386
+ * tools, behind pi's `tool_search`. On by default: the deferred schemas cost
387
+ * more on every request than the one partial cache miss a load causes.
388
+ * `false` declares every tool up front. Read from `process.env` by both the
389
+ * adapter traits (which pick the prompt's tool-discovery line) and
390
+ * `createSession`, so the prompt and the session always agree.
390
391
  */
391
392
  export function isPiToolDeferralEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
392
- return parseEnvFlag(env.PI_TOOL_DEFERRAL, false);
393
+ return parseEnvFlag(env.PI_TOOL_DEFERRAL, true);
393
394
  }
394
395
 
395
396
  /**
@@ -919,6 +920,28 @@ function requestContextTokens(message: unknown): number | undefined {
919
920
  return usage.input + cacheRead + cacheWrite;
920
921
  }
921
922
 
923
+ /**
924
+ * The session-log line for one LLM call's usage (token counts only, never
925
+ * content). The dashboard reads it as that call's cache-hit chip.
926
+ */
927
+ function callUsageLine(message: unknown): string | undefined {
928
+ const usage = (message as { usage?: Record<string, unknown> } | null)?.usage;
929
+ if (typeof usage?.input !== "number") return undefined;
930
+ const count = (value: unknown) =>
931
+ typeof value === "number" && Number.isFinite(value) ? value : 0;
932
+ return JSON.stringify({
933
+ type: "call_usage",
934
+ harness: "pi",
935
+ usage: {
936
+ input: count(usage.input),
937
+ cacheRead: count(usage.cacheRead),
938
+ cacheWrite: count(usage.cacheWrite),
939
+ output: count(usage.output),
940
+ reasoning: count(usage.reasoning),
941
+ },
942
+ });
943
+ }
944
+
922
945
  function describeAssistantTurn(message: unknown): AssistantTurnShape {
923
946
  const msg = (message ?? {}) as {
924
947
  content?: unknown;
@@ -1116,6 +1139,10 @@ export class PiMonoSession implements ProviderSession {
1116
1139
  if (endMsg.role === "assistant") {
1117
1140
  const turn = describeAssistantTurn(event.message);
1118
1141
  this.lastAssistantTurn = turn;
1142
+ // Ahead of the turn's text and tool lines: the chip rides on the
1143
+ // call's first row.
1144
+ const usageLine = callUsageLine(event.message);
1145
+ if (usageLine) this.emit({ type: "raw_log", content: usageLine });
1119
1146
  if (endMsg.stopReason === "error") {
1120
1147
  // Candidate terminal failure. May still be cleared by a successful
1121
1148
  // retry (auto_retry_end success / a later good message_end).
@@ -1684,12 +1711,15 @@ function escapeValues(values: unknown): Record<string, string> {
1684
1711
  /**
1685
1712
  * Map installed servers (`fetchInstalledMcpServers(..., "claude")` entries)
1686
1713
  * to pi MCP configs. pi speaks stdio and streamable HTTP; `sse` entries map to
1687
- * HTTP, which is how the previous pi client already reached them. Tools stay
1688
- * `direct`, as before: the prompt lists these servers as in the tool list.
1714
+ * HTTP, which is how the previous pi client already reached them. With
1715
+ * `deferred`, their tools load through `tool_search` like non-core swarm
1716
+ * tools; otherwise they are declared up front.
1689
1717
  */
1690
1718
  export function toPiMcpServers(
1691
1719
  installed: Record<string, Record<string, unknown>> | null,
1720
+ options: { deferred?: boolean } = {},
1692
1721
  ): Record<string, McpServerConfig> {
1722
+ const exposure = options.deferred ? "deferred" : "direct";
1693
1723
  const servers: Record<string, McpServerConfig> = {};
1694
1724
  for (const [name, entry] of Object.entries(installed ?? {})) {
1695
1725
  if (typeof entry.command === "string") {
@@ -1698,14 +1728,14 @@ export function toPiMcpServers(
1698
1728
  command: entry.command,
1699
1729
  args: Array.isArray(entry.args) ? entry.args.map(String) : [],
1700
1730
  env: escapeValues(entry.env),
1701
- exposure: "direct",
1731
+ exposure,
1702
1732
  };
1703
1733
  } else if (typeof entry.url === "string") {
1704
1734
  servers[name] = {
1705
1735
  type: "http",
1706
1736
  url: entry.url,
1707
1737
  headers: escapeValues(entry.headers),
1708
- exposure: "direct",
1738
+ exposure,
1709
1739
  };
1710
1740
  }
1711
1741
  }
@@ -1860,7 +1890,7 @@ export class PiMonoAdapter implements ProviderAdapter {
1860
1890
  config.apiUrl && config.apiKey && config.agentId
1861
1891
  ? await fetchInstalledMcpServers(config.apiUrl, config.apiKey, config.agentId, "claude")
1862
1892
  : null;
1863
- const piMcpServers = toPiMcpServers(installedMcpServers);
1893
+ const piMcpServers = toPiMcpServers(installedMcpServers, { deferred: deferTools });
1864
1894
  if (Object.keys(piMcpServers).length > 0) {
1865
1895
  console.log(
1866
1896
  `\x1b[2m[${config.role}]\x1b[0m Registering ${Object.keys(piMcpServers).length} installed MCP server(s) with pi`,
@@ -75,7 +75,7 @@ function resolveExecutorRegistry(): ExecutorRegistry | null {
75
75
  * scripts-runtime — no agent/LLM in the loop. Reuses the same `runScriptWithRunCredential()`
76
76
  * path as the `swarm-script` workflow executor.
77
77
  */
78
- async function executeScheduleScript(schedule: ScheduledTask): Promise<void> {
78
+ export async function executeScheduleScript(schedule: ScheduledTask): Promise<void> {
79
79
  if (!schedule.scriptName) {
80
80
  throw new Error(`Schedule "${schedule.name}" has no scriptName (targetType=script)`);
81
81
  }
@@ -106,6 +106,7 @@ async function executeScheduleScript(schedule: ScheduledTask): Promise<void> {
106
106
  source: script.source,
107
107
  args,
108
108
  fsMode: "none",
109
+ executor: script.executor,
109
110
  agentId,
110
111
  egressSecrets: credentials.egressSecrets,
111
112
  userConfig: credentials.userConfig,
@@ -1,6 +1,7 @@
1
1
  import { buildCtx } from "./ctx";
2
2
  import { patchFetchWithEgressSubstitution } from "./egress-secrets";
3
3
  import type { SwarmConfigPayload } from "./executors/types";
4
+ import { isHostValidationEquivalent } from "./host-validated-args";
4
5
  import { ctxSignatureHintFor } from "./script-error-hints";
5
6
  import { SwarmConfig } from "./swarm-config";
6
7
 
@@ -131,8 +132,14 @@ try {
131
132
  );
132
133
  }
133
134
 
135
+ const hostValidatedArgs = process.env.SWARM_SCRIPT_HOST_VALIDATED_ARGS === "1";
134
136
  let validatedArgs = parsedArgs;
135
- if (mod.argsSchema && typeof mod.argsSchema === "object" && "parse" in mod.argsSchema) {
137
+ if (
138
+ mod.argsSchema &&
139
+ typeof mod.argsSchema === "object" &&
140
+ "parse" in mod.argsSchema &&
141
+ !(hostValidatedArgs && isHostValidationEquivalent(mod.argsSchema, parsedArgs))
142
+ ) {
136
143
  try {
137
144
  // biome-ignore lint/suspicious/noExplicitAny: argsSchema is a Zod schema at runtime
138
145
  validatedArgs = (mod.argsSchema as any).parse(parsedArgs);