@desplega.ai/agent-swarm 1.165.0 → 1.166.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 (188) hide show
  1. package/dist/{acp-adapter-w6zws6me.js → acp-adapter-yzfxn8vj.js} +4 -4
  2. package/dist/{actions-jna0b3wm.js → actions-qz3m692w.js} +20 -24
  3. package/dist/{amp-adapter-0eve81pz.js → amp-adapter-j1xbmsqz.js} +4 -4
  4. package/dist/{app-75hm5cfa.js → app-43vrdmv8.js} +6 -7
  5. package/dist/{assistant-yzd51j33.js → assistant-ertffmka.js} +21 -25
  6. package/dist/{boot-reembed-z158xq32.js → boot-reembed-998jtpdg.js} +9 -12
  7. package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-s7k7c6v2.js} +10 -12
  8. package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
  9. package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-vvzs1zhk.js} +8 -8
  10. package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-2zvmekrp.js} +9 -11
  11. package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-s49wbk8t.js} +3 -3
  12. package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-cvhqq8qn.js} +4 -4
  13. package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-kkbxtw5q.js} +2618 -2600
  14. package/dist/{cli-k6xx3pve.js → cli-0nhdwxd5.js} +4 -4
  15. package/dist/{cli-n9ehc28t.js → cli-0qjcsxgz.js} +4 -4
  16. package/dist/{cli-zn2v3t8v.js → cli-3ddybbbx.js} +1 -1
  17. package/dist/{cli-5anb0ev3.js → cli-3qn4r334.js} +71 -14
  18. package/dist/{cli-1kzfvd1z.js → cli-4r1sjfc4.js} +9 -5
  19. package/dist/{cli-ba66wyhy.js → cli-66zh26sp.js} +6 -6
  20. package/dist/{cli-9xb89xca.js → cli-68r1j4sg.js} +6 -6
  21. package/dist/{cli-xnzrzets.js → cli-8kgvmkak.js} +2 -2
  22. package/dist/{cli-28smfvbn.js → cli-8t229cmr.js} +5 -5
  23. package/dist/{cli-9866bd07.js → cli-aqcey741.js} +1 -1
  24. package/dist/{cli-90v942q1.js → cli-cpfmqz3n.js} +8 -8
  25. package/dist/{cli-50eg13rj.js → cli-df56tqdw.js} +459 -282
  26. package/dist/{cli-8gpbwy09.js → cli-dw2q9f7g.js} +5 -5
  27. package/dist/{cli-0n4fasz0.js → cli-ecknp7p1.js} +15 -15
  28. package/dist/{cli-xb3c6aat.js → cli-f2n17zhp.js} +1 -1
  29. package/dist/{cli-ftry7975.js → cli-feh3dkah.js} +1 -1
  30. package/dist/{cli-amwhs8h2.js → cli-fn697bnt.js} +2 -2
  31. package/dist/{cli-prt9emct.js → cli-gt8azyet.js} +3 -3
  32. package/dist/{cli-59jn5twa.js → cli-h3x613ze.js} +2 -2
  33. package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
  34. package/dist/cli-j2jbr8af.js +516 -0
  35. package/dist/{cli-wfajtmf7.js → cli-jhm6hzjn.js} +593 -10
  36. package/dist/{cli-xtgtyafv.js → cli-jvj6a0cv.js} +190 -439
  37. package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
  38. package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
  39. package/dist/{cli-pjxnr1jn.js → cli-mwegs4af.js} +3 -3
  40. package/dist/{cli-5b2fjjzh.js → cli-p4ztb80h.js} +2 -2
  41. package/dist/{cli-e7t4snpa.js → cli-rggdq8wq.js} +4 -4
  42. package/dist/{cli-bdv39kgj.js → cli-rsy933yz.js} +5 -5
  43. package/dist/{cli-gh5yqzpe.js → cli-v2a8x45d.js} +2 -2
  44. package/dist/{cli-bns5c8k9.js → cli-vzc73wet.js} +43 -47
  45. package/dist/cli-wt7ccy4r.js +2654 -0
  46. package/dist/{cli-j0pdrmwg.js → cli-x7kpr72a.js} +3 -3
  47. package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
  48. package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
  49. package/dist/{cli-zvckv750.js → cli-y59h2pk9.js} +2 -2
  50. package/dist/{cli-qdv8kgz7.js → cli-yshaazs1.js} +13884 -3241
  51. package/dist/{cli-nfygw434.js → cli-ysnyhws9.js} +2 -2
  52. package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
  53. package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
  54. package/dist/cli.js +24 -20
  55. package/dist/{codex-adapter-3qnxshje.js → codex-adapter-dw1wxqt8.js} +7 -7
  56. package/dist/{codex-login-brkxhhdg.js → codex-login-terbdsqq.js} +2 -2
  57. package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-3cgb8y1g.js} +7 -7
  58. package/dist/{commands-2ybfcxkj.js → commands-mhxam6z2.js} +6 -6
  59. package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-2qda3st7.js} +3 -3
  60. package/dist/{db-f4ans0jj.js → db-sjks418k.js} +8 -6
  61. package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
  62. package/dist/{dispatcher-pk182apd.js → dispatcher-1es759rn.js} +18 -22
  63. package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-bek4cyw9.js} +3 -3
  64. package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
  65. package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
  66. package/dist/{grok-adapter-yt9pab3k.js → grok-adapter-ar9cw9jg.js} +5 -5
  67. package/dist/{handlers-g2wnr4t9.js → handlers-1gkbhktr.js} +21 -25
  68. package/dist/{hook-f7jt9vnm.js → hook-ds2zxq9v.js} +14 -16
  69. package/dist/{hook-bbdpnkb4.js → hook-vxa3tcsy.js} +8 -10
  70. package/dist/{http-w40q4w94.js → http-3h21z9md.js} +247 -164
  71. package/dist/{index-056j5kzn.js → index-9b65j1m5.js} +8 -8
  72. package/dist/{index-9bfssf54.js → index-bn6cy8ps.js} +18 -22
  73. package/dist/{index-cj1r9q83.js → index-dkfxpkmm.js} +19 -23
  74. package/dist/{index-7n70jwbb.js → index-f8xnqrye.js} +19 -21
  75. package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
  76. package/dist/{index-k8xc083e.js → index-n99fycq7.js} +18 -22
  77. package/dist/{keepalive-1tj10tf4.js → keepalive-3xspa9jw.js} +11 -8
  78. package/dist/{lead-st0bg7v4.js → lead-6qphc124.js} +34 -38
  79. package/dist/{maintenance-nz71zats.js → maintenance-0teem1bb.js} +7 -9
  80. package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
  81. package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-sh9q25xp.js} +10 -7
  82. package/dist/{onboard-kgpr9mbn.js → onboard-54qmnqxe.js} +2 -2
  83. package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-wjfa2wvd.js} +2 -2
  84. package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ycqzp2a1.js} +2 -2
  85. package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-e77e9zst.js} +4 -4
  86. package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-atnkngqs.js} +7 -7
  87. package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-9pjphh9q.js} +6 -6
  88. package/dist/{rbac-roles-790j3ga8.js → rbac-roles-j78wdksm.js} +6 -6
  89. package/dist/{render-v2-tch4nefj.js → render-v2-pxjs49fx.js} +6 -7
  90. package/dist/{setup-rcm303ns.js → setup-ymej0xtx.js} +2 -2
  91. package/dist/{work-objects-ctg7t70w.js → work-objects-3nh4ymzb.js} +22 -26
  92. package/dist/{worker-5dp0xm55.js → worker-qa9fkz5s.js} +34 -38
  93. package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
  94. package/openapi.json +5 -2
  95. package/package.json +9 -5
  96. package/src/be/automation-preflight.ts +9 -4
  97. package/src/be/boot-scrub-sweep.ts +302 -0
  98. package/src/be/crypto/key-bootstrap.ts +1 -1
  99. package/src/be/db/approvals.ts +15 -1
  100. package/src/be/db/codex-oauth-device-flows.ts +40 -0
  101. package/src/be/db/config-secrets.ts +31 -0
  102. package/src/be/db/runtime.ts +4 -0
  103. package/src/be/db/script-runs.ts +15 -0
  104. package/src/be/db/tasks/write.ts +1 -1
  105. package/src/be/db-queries/mcp-oauth.ts +5 -0
  106. package/src/be/db-queries/oauth.ts +77 -0
  107. package/src/be/db.ts +162 -59
  108. package/src/be/events.ts +17 -1
  109. package/src/be/memory/index-content.ts +2 -2
  110. package/src/be/memory/providers/sqlite-store.ts +63 -11
  111. package/src/be/memory/types.ts +14 -0
  112. package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
  113. package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
  114. package/src/be/scripts/db.ts +28 -4
  115. package/src/be/scripts/source-secrets.ts +37 -0
  116. package/src/be/scrub-json.ts +32 -0
  117. package/src/be/sealed-json.ts +59 -0
  118. package/src/be/secret-registry.ts +131 -0
  119. package/src/be/workflow-replay.ts +77 -0
  120. package/src/http/codex-oauth-device.ts +8 -10
  121. package/src/http/db-query-bounded.ts +3 -0
  122. package/src/http/db-query-shared.ts +107 -15
  123. package/src/http/db-query.ts +10 -2
  124. package/src/http/index.ts +23 -6
  125. package/src/http/script-runs.ts +9 -1
  126. package/src/http/scripts.ts +15 -0
  127. package/src/http/workflows.ts +3 -2
  128. package/src/oauth/mcp-wrapper.ts +1 -1
  129. package/src/providers/acp-adapter.ts +25 -11
  130. package/src/providers/acp-target-catalog.ts +35 -1
  131. package/src/providers/acp-targets.ts +69 -3
  132. package/src/providers/amp-adapter.ts +1 -1
  133. package/src/providers/cursor-adapter.ts +1 -1
  134. package/src/script-workflows/supervisor.ts +11 -2
  135. package/src/scripts-runtime/eval-harness.ts +2 -12
  136. package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
  137. package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
  138. package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
  139. package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
  140. package/src/scripts-runtime/executors/quickjs.ts +196 -0
  141. package/src/scripts-runtime/executors/registry.ts +2 -0
  142. package/src/scripts-runtime/executors/sourcemap.ts +79 -0
  143. package/src/scripts-runtime/script-error-hints.ts +14 -0
  144. package/src/server.ts +3 -1
  145. package/src/tests/acp-adapter.test.ts +127 -2
  146. package/src/tests/boot-scrub-sweep.test.ts +390 -0
  147. package/src/tests/check-text-columns.test.ts +145 -0
  148. package/src/tests/claude-sdk-process.fixture.ts +13 -2
  149. package/src/tests/codex-oauth-device.test.ts +23 -37
  150. package/src/tests/db-query-denylist.test.ts +202 -0
  151. package/src/tests/db-task-read-contract.test.ts +1 -0
  152. package/src/tests/db-task-write-contract.test.ts +1 -0
  153. package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
  154. package/src/tests/events-scrub.test.ts +88 -0
  155. package/src/tests/fixtures/secret-corpus.ts +320 -0
  156. package/src/tests/script-connections.test.ts +5 -0
  157. package/src/tests/script-executor-conformance.test.ts +3 -0
  158. package/src/tests/script-executor-quickjs.test.ts +442 -0
  159. package/src/tests/script-executor-registry.test.ts +6 -1
  160. package/src/tests/script-runs-http.test.ts +88 -1
  161. package/src/tests/script-runs-scrub.test.ts +275 -0
  162. package/src/tests/scripts-http.test.ts +82 -1
  163. package/src/tests/scrubbed-text-12e.test.ts +289 -0
  164. package/src/tests/sealed-key-guard.test.ts +166 -0
  165. package/src/tests/secret-canary-sweep.test.ts +958 -0
  166. package/src/tests/secret-registry.test.ts +355 -0
  167. package/src/tests/secret-rules.test.ts +230 -0
  168. package/src/tests/secret-scrubber.test.ts +3 -2
  169. package/src/tests/workflow-replay.test.ts +518 -0
  170. package/src/tests/workflow-scrub.test.ts +242 -0
  171. package/src/tools/db-query.ts +1 -1
  172. package/src/tools/inject-learning.ts +1 -1
  173. package/src/tools/workflows/get-workflow-run.ts +3 -2
  174. package/src/types.ts +1 -1
  175. package/src/utils/secret-rules.generated.ts +2055 -0
  176. package/src/utils/secret-scrubber.ts +363 -29
  177. package/src/workflows/engine.ts +39 -4
  178. package/src/workflows/recovery.ts +10 -1
  179. package/src/workflows/resume.ts +62 -3
  180. package/src/workflows/retry-poller.ts +3 -0
  181. package/dist/boot-scrub-logs-246yn076.js +0 -99
  182. package/dist/cli-3a4m1aws.js +0 -572
  183. package/dist/cli-a87b04k8.js +0 -951
  184. package/dist/cli-cvk6yzw5.js +0 -40
  185. package/dist/cli-h1spv8v4.js +0 -427
  186. package/dist/cli-jqt9hrnz.js +0 -168
  187. package/dist/cli-yhmwmv0g.js +0 -9281
  188. package/src/be/boot-scrub-logs.ts +0 -132
@@ -0,0 +1,442 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { QuickJSScriptExecutor, quickJSWorkerCount } from "../scripts-runtime/executors/quickjs";
3
+ import type { ExecutorInput } from "../scripts-runtime/executors/types";
4
+ import { DEFAULT_SCRIPT_RESOURCES } from "../scripts-runtime/executors/types";
5
+
6
+ // Behavior specific to the quickjs executor. The shared contract lives in
7
+ // script-executor-conformance.test.ts. No test here spawns a process.
8
+
9
+ const requests: Array<{ method: string; path: string; auth: string | null; agent: string | null }> =
10
+ [];
11
+ const kv = new Map<string, unknown>();
12
+ // Requests to /hang (or kv key "hang") never get a response. The test reads
13
+ // how many of them the client aborted.
14
+ const hang = { started: 0, aborted: 0 };
15
+ const slow = { inFlight: 0, maxInFlight: 0 };
16
+ function hangUntilAborted(req: Request): Promise<Response> {
17
+ hang.started++;
18
+ return new Promise((resolve) => {
19
+ req.signal.addEventListener("abort", () => {
20
+ hang.aborted++;
21
+ resolve(new Response("aborted"));
22
+ });
23
+ });
24
+ }
25
+ async function waitFor(check: () => boolean, timeoutMs = 3_000): Promise<boolean> {
26
+ const until = Date.now() + timeoutMs;
27
+ while (!check()) {
28
+ if (Date.now() > until) return false;
29
+ await Bun.sleep(10);
30
+ }
31
+ return true;
32
+ }
33
+ const server = Bun.serve({
34
+ port: 0,
35
+ async fetch(req) {
36
+ const url = new URL(req.url);
37
+ requests.push({
38
+ method: req.method,
39
+ path: url.pathname,
40
+ auth: req.headers.get("authorization"),
41
+ agent: req.headers.get("x-agent-id"),
42
+ });
43
+ if (url.pathname === "/hang" || url.pathname === "/api/kv/hang") return hangUntilAborted(req);
44
+ if (url.pathname === "/slow") {
45
+ slow.inFlight++;
46
+ slow.maxInFlight = Math.max(slow.maxInFlight, slow.inFlight);
47
+ await Bun.sleep(30);
48
+ slow.inFlight--;
49
+ return Response.json({ ok: true });
50
+ }
51
+ if (url.pathname.startsWith("/api/kv/")) {
52
+ const key = decodeURIComponent(url.pathname.slice("/api/kv/".length));
53
+ if (req.method === "PUT") {
54
+ const body = (await req.json()) as { value: unknown };
55
+ kv.set(key, body.value);
56
+ return Response.json({ key, value: body.value });
57
+ }
58
+ return Response.json({ key, value: kv.get(key) ?? null });
59
+ }
60
+ if (url.pathname === "/echo") {
61
+ return Response.json(
62
+ { method: req.method, body: await req.text(), custom: req.headers.get("x-custom") },
63
+ { headers: { "x-reply": "yes" } },
64
+ );
65
+ }
66
+ return Response.json({ error: "not found" }, { status: 404 });
67
+ },
68
+ });
69
+ afterAll(() => server.stop(true));
70
+
71
+ const baseUrl = `http://127.0.0.1:${server.port}`;
72
+ const executor = new QuickJSScriptExecutor();
73
+
74
+ function input(source: string, overrides: Partial<ExecutorInput> = {}): ExecutorInput {
75
+ return {
76
+ source,
77
+ args: null,
78
+ configPayload: {
79
+ system: {
80
+ apiKey: { value: "quickjs-test-secret", isSecret: true },
81
+ agentId: { value: "agent-q", isSecret: false },
82
+ mcpBaseUrl: { value: baseUrl, isSecret: false },
83
+ },
84
+ user: { REGION: { value: "eu", isSecret: false } },
85
+ },
86
+ resources: { ...DEFAULT_SCRIPT_RESOURCES, wallClockMs: 5_000, ...overrides.resources },
87
+ fsMode: "none",
88
+ network: "open",
89
+ ...overrides,
90
+ };
91
+ }
92
+
93
+ describe("quickjs executor", () => {
94
+ test("runs TypeScript with a tree-shaken zod argsSchema", async () => {
95
+ const source = `import { z } from "zod";
96
+ export const argsSchema = z.object({ email: z.string().email(), n: z.number().int() });
97
+ type Args = z.infer<typeof argsSchema>;
98
+ export default async function (args: Args) {
99
+ return { domain: args.email.split("@")[1], double: args.n * 2 };
100
+ }`;
101
+ const ok = await executor.run({ ...input(source), args: { email: "a@b.co", n: 3 } });
102
+ expect(ok.error).toBeUndefined();
103
+ expect(ok.result).toEqual({ domain: "b.co", double: 6 });
104
+
105
+ const bad = await executor.run({ ...input(source), args: { email: "nope", n: 1.5 } });
106
+ expect(bad.error).toBe("eval_error");
107
+ expect(bad.runtimeError?.message).toContain("argsSchema validation failed:");
108
+ expect(bad.runtimeError?.message).toContain("email");
109
+ });
110
+
111
+ test("accepts args that arrive as a JSON string", async () => {
112
+ const output = await executor.run({
113
+ ...input("export default async (args) => args.x + 1;"),
114
+ args: JSON.stringify({ x: 41 }),
115
+ });
116
+ expect(output.result).toBe(42);
117
+ });
118
+
119
+ test("maps runtime errors back to the original TypeScript line", async () => {
120
+ const source = `type T = { a: number };
121
+
122
+ function explode(value: T): never {
123
+ throw new Error("boom " + value.a);
124
+ }
125
+
126
+ export default async (args: T) => explode(args);
127
+ `;
128
+ const output = await executor.run({ ...input(source), args: { a: 7 } });
129
+ expect(output.error).toBe("eval_error");
130
+ expect(output.runtimeError?.message).toBe("boom 7");
131
+ expect(output.runtimeError?.userScriptLine).toBe(4);
132
+ expect(output.runtimeError?.userFrames[0]?.file).toBe("user-script.ts");
133
+ expect(output.stderr).toContain("user-script.ts:4:");
134
+ });
135
+
136
+ test("adds the (args, ctx) hint when ctx members are read off undefined", async () => {
137
+ const output = await executor.run(
138
+ input("export default async (ctx) => { const { log } = ctx.api; return log; };"),
139
+ );
140
+ expect(output.error).toBe("eval_error");
141
+ expect((output.runtimeError as { ctxSignatureHint?: string }).ctxSignatureHint).toContain(
142
+ "args comes first",
143
+ );
144
+ });
145
+
146
+ test("reports syntax errors as eval_error with the build diagnostic", async () => {
147
+ const output = await executor.run(input("export default async () => { return (; };"));
148
+ expect(output.error).toBe("eval_error");
149
+ expect(output.runtimeError?.name).toBe("BuildError");
150
+ expect(output.stderr.length).toBeGreaterThan(0);
151
+ });
152
+
153
+ test("interrupts a synchronous busy loop and keeps the worker usable", async () => {
154
+ const looped = await executor.run(
155
+ input("export default async () => { while (true) {} };", {
156
+ resources: { ...DEFAULT_SCRIPT_RESOURCES, wallClockMs: 200 },
157
+ }),
158
+ );
159
+ expect(looped.error).toBe("timeout");
160
+ expect(looped.exitCode).toBe(124);
161
+
162
+ const after = await executor.run(input("export default async () => 'alive';"));
163
+ expect(after.result).toBe("alive");
164
+ });
165
+
166
+ test("times out a promise that never settles", async () => {
167
+ const output = await executor.run(
168
+ input("export default async () => new Promise(() => {});", {
169
+ resources: { ...DEFAULT_SCRIPT_RESOURCES, wallClockMs: 150 },
170
+ }),
171
+ );
172
+ expect(output.error).toBe("timeout");
173
+ });
174
+
175
+ test("enforces the heap limit as oom", async () => {
176
+ const output = await executor.run(
177
+ // Growing one array hits the limit in ~100 ms. Many small strings make
178
+ // QuickJS's GC thrash near the limit, which takes seconds to fail.
179
+ input(
180
+ "export default async () => { const a = []; for (let i = 0; i < 1e9; i++) a.push(i); };",
181
+ {
182
+ resources: { ...DEFAULT_SCRIPT_RESOURCES, memoryMb: 16, wallClockMs: 10_000 },
183
+ },
184
+ ),
185
+ );
186
+ expect(output.error).toBe("oom");
187
+ });
188
+
189
+ test("an abort kills a busy script and the pool recovers", async () => {
190
+ const controller = new AbortController();
191
+ setTimeout(() => controller.abort(), 100);
192
+ const output = await executor.run(
193
+ input("export default async () => { while (true) {} };", {
194
+ signal: controller.signal,
195
+ resources: { ...DEFAULT_SCRIPT_RESOURCES, wallClockMs: 10_000 },
196
+ }),
197
+ );
198
+ expect(output.error).toBe("killed");
199
+
200
+ const after = await executor.run(input("export default async () => 'recovered';"));
201
+ expect(after.result).toBe("recovered");
202
+ });
203
+
204
+ test("exposes no process, filesystem, or module globals", async () => {
205
+ // Read off globalThis: Bun.build constant-folds a bare `typeof require` to "function".
206
+ const output = await executor.run(
207
+ input(`export default async () => ({
208
+ process: typeof globalThis.process,
209
+ bun: typeof globalThis.Bun,
210
+ require: typeof globalThis.require,
211
+ })`),
212
+ );
213
+ expect(output.result).toEqual({ process: "undefined", bun: "undefined", require: "undefined" });
214
+ });
215
+
216
+ test("glob and grep fail with a clear message", async () => {
217
+ const output = await executor.run(
218
+ input("export default async (_args, ctx) => ctx.stdlib.glob('*.ts');"),
219
+ );
220
+ expect(output.error).toBe("eval_error");
221
+ expect(output.runtimeError?.message).toContain("not available in the quickjs executor");
222
+ });
223
+
224
+ test("routes console output to stdout and stderr", async () => {
225
+ const output = await executor.run(
226
+ input(
227
+ "export default async (_args, ctx) => { console.log('hello', { a: 1 }); ctx.logger.error('bad'); return 1; };",
228
+ ),
229
+ );
230
+ expect(output.stdout).toBe('hello {"a":1}\n');
231
+ expect(output.stderr).toBe("bad\n");
232
+ });
233
+
234
+ test("setTimeout runs callbacks through the host clock", async () => {
235
+ const output = await executor.run(
236
+ input(`export default async () => {
237
+ const order: string[] = [];
238
+ const cancelled = setTimeout(() => order.push("cancelled"), 5);
239
+ clearTimeout(cancelled);
240
+ await new Promise((resolve) => setTimeout(() => { order.push("fired"); resolve(null); }, 10));
241
+ return order;
242
+ };`),
243
+ );
244
+ expect(output.result).toEqual(["fired"]);
245
+ });
246
+
247
+ test("ctx.swarm calls go through the host with the swarm identity", async () => {
248
+ requests.length = 0;
249
+ const output = await executor.run(
250
+ input(`export default async (_args, ctx) => {
251
+ await ctx.swarm.kv_set({ key: "greeting", value: "hi" });
252
+ const got = await ctx.swarm.kv_get({ key: "greeting" });
253
+ return got.data.value;
254
+ };`),
255
+ );
256
+ expect(output.error).toBeUndefined();
257
+ expect(output.result).toBe("hi");
258
+ expect(requests.map((r) => r.method)).toEqual(["PUT", "GET"]);
259
+ expect(requests.every((r) => r.auth === "Bearer quickjs-test-secret")).toBe(true);
260
+ expect(requests.every((r) => r.agent === "agent-q")).toBe(true);
261
+ });
262
+
263
+ test("fetch returns a Response-like object with headers and a text body", async () => {
264
+ const output = await executor.run(
265
+ input(`export default async () => {
266
+ const response = await fetch("${baseUrl}/echo", {
267
+ method: "POST",
268
+ headers: { "X-Custom": "c1" },
269
+ body: JSON.stringify({ n: 1 }),
270
+ });
271
+ return { status: response.status, ok: response.ok, reply: response.headers.get("x-reply"), body: await response.json() };
272
+ };`),
273
+ );
274
+ expect(output.result).toEqual({
275
+ status: 200,
276
+ ok: true,
277
+ reply: "yes",
278
+ body: { method: "POST", body: '{"n":1}', custom: "c1" },
279
+ });
280
+ });
281
+
282
+ test("fetch rejects non-string bodies", async () => {
283
+ const output = await executor.run(
284
+ input(
285
+ `export default async () => fetch("${baseUrl}/echo", { method: "POST", body: { n: 1 } });`,
286
+ ),
287
+ );
288
+ expect(output.error).toBe("eval_error");
289
+ expect(output.runtimeError?.message).toContain("fetch body must be a string");
290
+ });
291
+
292
+ test("user config values stay redacted until unwrapped", async () => {
293
+ const output = await executor.run(
294
+ input(`export default async (_args, ctx) => {
295
+ const region = ctx.swarm.config.get("REGION");
296
+ return {
297
+ wrapped: region,
298
+ text: String(region),
299
+ value: ctx.stdlib.Redacted.value(region),
300
+ meta: ctx.stdlib.Redacted.meta(region),
301
+ missing: ctx.swarm.config.get("NOPE") === undefined,
302
+ };
303
+ };`),
304
+ );
305
+ expect(output.result).toEqual({
306
+ wrapped: "<redacted>",
307
+ text: "<redacted>",
308
+ value: "eu",
309
+ meta: { type: "user", isSecret: false },
310
+ missing: true,
311
+ });
312
+ });
313
+
314
+ test("a non-serializable result is an eval_error", async () => {
315
+ const output = await executor.run(
316
+ input("export default async () => { const a: any = {}; a.self = a; return a; };"),
317
+ );
318
+ expect(output.error).toBe("eval_error");
319
+ expect(output.runtimeError?.message).toContain("not JSON-serializable");
320
+ });
321
+
322
+ test("recycles a worker whose WASM memory grew past the threshold", async () => {
323
+ await executor.run(input("export default async () => 1;"));
324
+ const before = quickJSWorkerCount();
325
+ const big = await executor.run(
326
+ input(
327
+ "export default async () => { const a = []; for (let i = 0; i < 3e6; i++) a.push({ i }); return a.length; };",
328
+ ),
329
+ );
330
+ expect(big.result).toBe(3e6);
331
+ expect(quickJSWorkerCount()).toBe(before - 1);
332
+
333
+ const after = await executor.run(input("export default async () => 'fresh';"));
334
+ expect(after.result).toBe("fresh");
335
+ });
336
+
337
+ test("host calls only reach own api/mcp/room members", async () => {
338
+ const output = await executor.run(
339
+ input(`export default async () => {
340
+ const call = (path: string) => (globalThis as any).__host_call(path, "[]");
341
+ return [
342
+ JSON.parse(await call("swarm.room.constructor")).error?.message,
343
+ JSON.parse(await call("swarm.constructor")).error?.message,
344
+ JSON.parse(await call("api.__proto__.x")).error?.message,
345
+ ];
346
+ };`),
347
+ );
348
+ expect(output.result).toEqual([
349
+ "ctx.swarm.room.constructor is not a function",
350
+ "ctx.swarm.constructor is not a function",
351
+ "ctx.api.__proto__.x is not a function",
352
+ ]);
353
+ });
354
+
355
+ test("aborts a host fetch the script did not await when the run returns", async () => {
356
+ await executor.run(input("export default async () => 0;"));
357
+ const workers = quickJSWorkerCount();
358
+ const before = { ...hang };
359
+ const output = await executor.run(
360
+ // The 100 ms wait lets the request reach the server before the run ends.
361
+ input(`export default async () => {
362
+ void fetch("${baseUrl}/hang");
363
+ await new Promise((resolve) => setTimeout(resolve, 100));
364
+ return 1;
365
+ };`),
366
+ );
367
+ expect(output.result).toBe(1);
368
+ expect(hang.started).toBe(before.started + 1);
369
+ expect(await waitFor(() => hang.aborted > before.aborted)).toBe(true);
370
+ // The run aborted and settled the fetch itself, so the pool kept the worker.
371
+ // Terminating the worker is only the fallback for calls that do not settle.
372
+ expect(quickJSWorkerCount()).toBe(workers);
373
+ });
374
+
375
+ test("aborts a pending host fetch when the run times out", async () => {
376
+ const before = hang.aborted;
377
+ const output = await executor.run(
378
+ input(`export default async () => (await fetch("${baseUrl}/hang")).text();`, {
379
+ resources: { ...DEFAULT_SCRIPT_RESOURCES, wallClockMs: 200 },
380
+ }),
381
+ );
382
+ expect(output.error).toBe("timeout");
383
+ expect(await waitFor(() => hang.aborted > before)).toBe(true);
384
+
385
+ const after = await executor.run(input("export default async () => 'next';"));
386
+ expect(after.result).toBe("next");
387
+ });
388
+
389
+ test("aborts ctx.swarm host calls too", async () => {
390
+ await executor.run(input("export default async () => 0;"));
391
+ const workers = quickJSWorkerCount();
392
+ const before = { ...hang };
393
+ const output = await executor.run(
394
+ input(
395
+ `export default async (_args, ctx) => {
396
+ void ctx.swarm.kv_get({ key: "hang" });
397
+ await new Promise((resolve) => setTimeout(resolve, 100));
398
+ return 1;
399
+ };`,
400
+ ),
401
+ );
402
+ expect(output.result).toBe(1);
403
+ expect(hang.started).toBe(before.started + 1);
404
+ expect(await waitFor(() => hang.aborted > before.aborted)).toBe(true);
405
+ expect(quickJSWorkerCount()).toBe(workers);
406
+ });
407
+
408
+ test("caps concurrent host calls per run and queues the rest", async () => {
409
+ slow.maxInFlight = 0;
410
+ const output = await executor.run(
411
+ input(`export default async () => {
412
+ const replies = await Promise.all(
413
+ Array.from({ length: 40 }, () => fetch("${baseUrl}/slow").then((r) => r.json())),
414
+ );
415
+ return replies.filter((r: any) => r.ok).length;
416
+ };`),
417
+ );
418
+ expect(output.result).toBe(40);
419
+ expect(slow.maxInFlight).toBeGreaterThan(1);
420
+ expect(slow.maxInFlight).toBeLessThanOrEqual(16);
421
+ });
422
+
423
+ test("rejects host calls beyond the per-run pending limit", async () => {
424
+ const output = await executor.run(
425
+ input(`export default async () => {
426
+ const calls = Array.from({ length: 1100 }, () => fetch("${baseUrl}/slow").then(() => "ok", (e) => e.message));
427
+ const results = await Promise.all(calls);
428
+ return results.filter((r) => r !== "ok").length > 0 ? results.find((r) => r !== "ok") : "none";
429
+ };`),
430
+ );
431
+ expect(output.result).toContain("Too many pending host calls");
432
+ });
433
+
434
+ test("queues runs beyond the pool size", async () => {
435
+ const outputs = await Promise.all(
436
+ Array.from({ length: 12 }, (_, i) =>
437
+ executor.run({ ...input("export default async (args) => args.i * 2;"), args: { i } }),
438
+ ),
439
+ );
440
+ expect(outputs.map((o) => o.result)).toEqual(Array.from({ length: 12 }, (_, i) => i * 2));
441
+ });
442
+ });
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { NativeScriptExecutor } from "../scripts-runtime/executors/native";
3
+ import { QuickJSScriptExecutor } from "../scripts-runtime/executors/quickjs";
3
4
  import { getScriptExecutor } from "../scripts-runtime/executors/registry";
4
5
 
5
6
  describe("getScriptExecutor", () => {
@@ -11,7 +12,11 @@ describe("getScriptExecutor", () => {
11
12
  expect(getScriptExecutor("native")).toBeInstanceOf(NativeScriptExecutor);
12
13
  });
13
14
 
15
+ test("returns quickjs when requested", () => {
16
+ expect(getScriptExecutor("quickjs")).toBeInstanceOf(QuickJSScriptExecutor);
17
+ });
18
+
14
19
  test("throws for unknown executors", () => {
15
- expect(() => getScriptExecutor("e2b")).toThrow("Available: native");
20
+ expect(() => getScriptExecutor("e2b")).toThrow("Available: native, quickjs");
16
21
  });
17
22
  });
@@ -14,12 +14,17 @@ import {
14
14
  initDb,
15
15
  updateScriptRun,
16
16
  } from "../be/db";
17
+ import { configSecretName, registerStoredSecret } from "../be/secret-registry";
17
18
  import { handleCore } from "../http/core";
18
19
  import { handleScriptRuns } from "../http/script-runs";
19
20
  import { getPathSegments, parseQueryParams } from "../http/utils";
20
21
  import { localProcessScriptExecutor } from "../script-workflows/executor";
21
22
  import { setScriptRunExecutor } from "../script-workflows/supervisor";
22
- import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
23
+ import {
24
+ clearVolatileSecretsForTesting,
25
+ refreshSecretScrubberCache,
26
+ } from "../utils/secret-scrubber";
27
+ import { randomToken } from "./synthetic-secret-helpers";
23
28
 
24
29
  const TEST_DB_PATH = "./test-script-runs-http.sqlite";
25
30
  const API_KEY = "example-test-script-runs-http-key-1234567890";
@@ -194,6 +199,11 @@ describe("/api/script-runs HTTP", () => {
194
199
  body: createBody({ background: true }),
195
200
  });
196
201
  expect(created.status).toBe(201);
202
+ // The launch is fire-and-forget and reads the sealed args before it
203
+ // starts the executor, so it can land after the response.
204
+ for (let i = 0; i < 50 && spawnedWith.length === 0; i++) {
205
+ await Bun.sleep(20);
206
+ }
197
207
  expect(spawnedWith).toEqual(["http://swarm-internal.example.test:3013"]);
198
208
  } finally {
199
209
  setScriptRunExecutor(localProcessScriptExecutor);
@@ -303,6 +313,54 @@ describe("/api/script-runs HTTP", () => {
303
313
  });
304
314
  });
305
315
 
316
+ test("redacts journal config/error and run output/error posted by the harness", async () => {
317
+ const token = ["ghp", randomToken(36)].join("_");
318
+ const created = await dispatch("/api/script-runs", {
319
+ method: "POST",
320
+ agentId,
321
+ body: createBody(),
322
+ });
323
+ const { id } = (await created.json()) as { id: string };
324
+
325
+ const recorded = await dispatch(`/api/internal/script-runs/${id}/steps`, {
326
+ method: "POST",
327
+ agentId,
328
+ body: JSON.stringify({
329
+ stepKey: "deploy",
330
+ stepType: "agent-task",
331
+ config: { task: `deploy with ${token}` },
332
+ status: "failed",
333
+ error: `deploy failed: bad credential ${token}`,
334
+ }),
335
+ });
336
+ expect(recorded.status).toBe(201);
337
+ const step = await getDbClient().get<{ config: string; error: string }>(
338
+ "SELECT config, error FROM script_run_journal WHERE runId = ?",
339
+ [id],
340
+ );
341
+ for (const value of [step?.config, step?.error]) {
342
+ expect(value).toBeString();
343
+ expect(value!.includes(token)).toBe(false);
344
+ expect(value!).toContain("[REDACTED:");
345
+ }
346
+ expect(step!.error).toContain("deploy failed: bad credential");
347
+ expect((JSON.parse(step!.config) as { task: string }).task).toContain("deploy with");
348
+
349
+ await updateScriptRun(id, { status: "running" });
350
+ const status = await dispatch(`/api/internal/script-runs/${id}/status`, {
351
+ method: "POST",
352
+ agentId,
353
+ body: JSON.stringify({ status: "failed", error: `exit: ${token}` }),
354
+ });
355
+ expect(status.status).toBe(204);
356
+ const run = await getDbClient().get<{ error: string }>(
357
+ "SELECT error FROM script_runs WHERE id = ?",
358
+ [id],
359
+ );
360
+ expect(run!.error.includes(token)).toBe(false);
361
+ expect(run!.error).toContain("exit: [REDACTED:");
362
+ });
363
+
306
364
  test("aborts the run when the journal step cap is exceeded", async () => {
307
365
  process.env.SCRIPT_RUN_MAX_STEPS = "1";
308
366
  const created = await dispatch("/api/script-runs", {
@@ -574,3 +632,32 @@ describe("/api/script-runs HTTP", () => {
574
632
  expect(body.run.error).toBe("original failure");
575
633
  });
576
634
  });
635
+
636
+ describe("durable launch source with an embedded secret", () => {
637
+ test("is refused before the run row is written", async () => {
638
+ const value = `durable-${randomToken(32)}`;
639
+ const name = configSecretName("DURABLE_TOKEN");
640
+ registerStoredSecret(value, name);
641
+ try {
642
+ const before = await getDbClient().get<{ n: number }>(
643
+ "SELECT COUNT(*) AS n FROM script_runs",
644
+ );
645
+ const response = await dispatch("/api/script-runs", {
646
+ method: "POST",
647
+ agentId,
648
+ body: createBody({
649
+ source: `export default async function main() { return { auth: "${value}" }; }`,
650
+ }),
651
+ });
652
+ expect(response.status).toBe(400);
653
+ expect(response.text).not.toContain(value);
654
+ const body = (await response.json()) as { error: string; findings: { id: string }[] };
655
+ expect(body.error).toBe("source_contains_secret");
656
+ expect(body.findings.map((f) => f.id)).toEqual([name]);
657
+ const after = await getDbClient().get<{ n: number }>("SELECT COUNT(*) AS n FROM script_runs");
658
+ expect(after?.n).toBe(before?.n);
659
+ } finally {
660
+ clearVolatileSecretsForTesting();
661
+ }
662
+ });
663
+ });