@desplega.ai/agent-swarm 1.167.0 → 1.169.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 (184) hide show
  1. package/dist/{acp-adapter-zpvy6321.js → acp-adapter-m47zfx4a.js} +2 -2
  2. package/dist/{actions-fq2zrwtf.js → actions-ws7k3tvq.js} +25 -19
  3. package/dist/{amp-adapter-5gbk7501.js → amp-adapter-nvv8k8j5.js} +1 -1
  4. package/dist/{app-nctetn1e.js → app-80w6qspx.js} +5 -5
  5. package/dist/{assistant-0aq8gjvj.js → assistant-jtq439kw.js} +29 -20
  6. package/dist/{boot-reembed-1ze5853n.js → boot-reembed-3nc0aaed.js} +4 -4
  7. package/dist/{boot-reembed-fmp5ww69.js → boot-reembed-feda8wn8.js} +4 -4
  8. package/dist/{boot-scrub-sweep-cbp8xd4e.js → boot-scrub-sweep-grsa2er0.js} +5 -5
  9. package/dist/{boot-seeds-22tx8a53.js → boot-seeds-gcfgq44f.js} +7 -7
  10. package/dist/{claude-adapter-0jhgeh1d.js → claude-adapter-pjv2q7t8.js} +7 -7
  11. package/dist/{claude-managed-adapter-tzwxy803.js → claude-managed-adapter-0wa8se82.js} +2 -2
  12. package/dist/{claude-managed-setup-nrhfx5b0.js → claude-managed-setup-zxa9p9tx.js} +1 -1
  13. package/dist/{claude-sdk-session-wh77rh83.js → claude-sdk-session-c9j6k9kx.js} +8772 -8663
  14. package/dist/{cli-c2tpw9ck.js → cli-3ry5gx4f.js} +1 -1
  15. package/dist/{cli-wydz3feg.js → cli-61tbnjpf.js} +214 -2472
  16. package/dist/{cli-cexctgn5.js → cli-7vz49g7h.js} +101 -34
  17. package/dist/{cli-6qxmjpem.js → cli-8xjst8q2.js} +34 -22
  18. package/dist/{cli-tx2zvn23.js → cli-91r17hb1.js} +3 -3
  19. package/dist/{cli-577bxkf6.js → cli-a8vps458.js} +182 -2208
  20. package/dist/{cli-tc5zec7v.js → cli-b7y8113e.js} +1 -1
  21. package/dist/{cli-69egj654.js → cli-bqrs5ycj.js} +1 -1
  22. package/dist/{cli-cf2vgn9d.js → cli-dv76vq5d.js} +30 -28
  23. package/dist/{cli-g5erf0zt.js → cli-e2prcx5m.js} +31 -0
  24. package/dist/{cli-c8r99akr.js → cli-e5qk3m2a.js} +209 -31
  25. package/dist/{cli-z1dqhc9y.js → cli-eqfbsxsn.js} +3 -3
  26. package/dist/{cli-kw0jcfje.js → cli-fyeft04q.js} +1 -1
  27. package/dist/{cli-5qkcdv8c.js → cli-gmdajfw5.js} +3 -3
  28. package/dist/{cli-ypgba1a8.js → cli-jkfxpg1b.js} +1 -1
  29. package/dist/{cli-mxvnnv5q.js → cli-jv36y8wz.js} +1 -1
  30. package/dist/{cli-m7vmeepg.js → cli-km8559dm.js} +6 -6
  31. package/dist/{cli-ykt8f7va.js → cli-kp3vfknv.js} +1 -1
  32. package/dist/{cli-ymmc1zxg.js → cli-m23r88re.js} +8 -5
  33. package/dist/{cli-v1pwxreq.js → cli-n4z21pt8.js} +1 -1
  34. package/dist/{cli-mxry91sa.js → cli-qg2zhjfc.js} +73 -4
  35. package/dist/{cli-1yrcfa5s.js → cli-rbr91whd.js} +4 -4
  36. package/dist/{cli-mkdsy1nq.js → cli-rg1c8ccw.js} +93 -7
  37. package/dist/cli-rh44xx46.js +2659 -0
  38. package/dist/cli-s0k2j7q9.js +2084 -0
  39. package/dist/{cli-ty4bzkqr.js → cli-sajr098b.js} +1 -1
  40. package/dist/{cli-6vfz44sr.js → cli-swrz3mj8.js} +1 -1
  41. package/dist/{cli-p8rt8spp.js → cli-vejds37t.js} +2 -2
  42. package/dist/{cli-jbzbr36n.js → cli-w6xa5nf5.js} +1 -1
  43. package/dist/{cli-9q3wt23f.js → cli-x6z90nq9.js} +1 -1
  44. package/dist/{cli-qzmzwegq.js → cli-ysnkvdx6.js} +102 -9
  45. package/dist/{cli-y0kjpfm9.js → cli-yvxfaem2.js} +2 -2
  46. package/dist/{cli-pkj1w46r.js → cli-z48ggfyq.js} +1 -1
  47. package/dist/{cli-6bpvv8h4.js → cli-zzev32jw.js} +1 -1
  48. package/dist/cli.js +20 -17
  49. package/dist/{codex-adapter-cgesnjkr.js → codex-adapter-0kpg09ca.js} +3 -3
  50. package/dist/{codex-hook-15dgc4cb.js → codex-hook-295jpbfa.js} +2 -2
  51. package/dist/{codex-session-runner-8kd7ddhr.js → codex-session-runner-8n672hxh.js} +3 -3
  52. package/dist/{commands-h6zaaxym.js → commands-mhmg5n0x.js} +4 -4
  53. package/dist/{cursor-adapter-v8mm6bp5.js → cursor-adapter-wn8n5zzk.js} +2 -2
  54. package/dist/{db-qg3myn53.js → db-wa6s1mwv.js} +4 -4
  55. package/dist/{dispatcher-7vnwj9qw.js → dispatcher-nhs2pkr8.js} +16 -14
  56. package/dist/{dsh-adapter-tz6hfp5r.js → dsh-adapter-5423p70m.js} +1 -1
  57. package/dist/{grok-adapter-jdt2dz2s.js → grok-adapter-28pkdt80.js} +3 -3
  58. package/dist/{handlers-2qtkpncr.js → handlers-5rtf9nng.js} +19 -17
  59. package/dist/{hook-51yfcgsv.js → hook-krjfkpzy.js} +4 -4
  60. package/dist/{hook-qnbdgwvd.js → hook-qkm73zk6.js} +5 -5
  61. package/dist/{http-jsmtag08.js → http-a397xcn4.js} +4845 -3484
  62. package/dist/{index-2t9kesn1.js → index-ac0v8v9d.js} +4 -4
  63. package/dist/{index-ekywyv2v.js → index-kqre45fr.js} +16 -14
  64. package/dist/{index-bt8wkv1w.js → index-rtxg15ah.js} +17 -15
  65. package/dist/{index-t4g06w93.js → index-vvsq7v45.js} +26 -23
  66. package/dist/{index-cx1sb9bq.js → index-ytdhtdry.js} +50 -11
  67. package/dist/{keepalive-4aezx4bj.js → keepalive-psfh8ysx.js} +6 -6
  68. package/dist/{lead-r44bkpm5.js → lead-m6rppjn7.js} +29 -27
  69. package/dist/{maintenance-grz5jrzh.js → maintenance-41f837ke.js} +4 -4
  70. package/dist/{node-qm9eb6z0.js → node-fkajaq96.js} +1 -1
  71. package/dist/{oauth-refresh-sweep-wm4ctjfj.js → oauth-refresh-sweep-6hr55hm8.js} +5 -5
  72. package/dist/{onboard-v60zgqf0.js → onboard-ktrtd7cz.js} +2 -2
  73. package/dist/{otel-impl-ghh4dyf8.js → otel-impl-dq4f2k3m.js} +1 -1
  74. package/dist/{pi-mono-adapter-0eg8kw2c.js → pi-mono-adapter-e5v9w0hv.js} +3 -2
  75. package/dist/{pricing-refresh-mjrqvnvw.js → pricing-refresh-kmk7j1ra.js} +6 -6
  76. package/dist/{rbac-roles-fd5tskj9.js → rbac-roles-9w1dkkxd.js} +4 -4
  77. package/dist/{rbac-roles-axv4d74x.js → rbac-roles-9ykx9tc5.js} +5 -5
  78. package/dist/{render-v2-k50j33qd.js → render-v2-ycqm53np.js} +5 -5
  79. package/dist/resume-cq2pkp8q.js +70 -0
  80. package/dist/{setup-4syh0pd2.js → setup-0dcmdvbn.js} +2 -2
  81. package/dist/{virtual-modules-ftr9kwe8.js → virtual-modules-1x9k4jcs.js} +1 -1
  82. package/dist/{work-objects-jcdbg8rr.js → work-objects-x01x2fh8.js} +19 -17
  83. package/dist/{worker-x8j8c9sx.js → worker-py1ssb1h.js} +29 -27
  84. package/openapi.json +15622 -15042
  85. package/package.json +8 -5
  86. package/src/apps/store.ts +1 -0
  87. package/src/artifact-sdk/feedback-overlay.ts +528 -0
  88. package/src/be/db/workflow-runs.ts +95 -0
  89. package/src/be/db-queries/slack-inbound.ts +489 -0
  90. package/src/be/db.ts +9 -2
  91. package/src/be/extensions/assets.ts +1 -1
  92. package/src/be/migrations/203_workflow_run_parent_step.sql +10 -0
  93. package/src/be/migrations/204_slack_inbound_receipts.sql +51 -0
  94. package/src/be/script-connections.ts +41 -7
  95. package/src/be/scripts/run-saved.ts +40 -20
  96. package/src/be/swarm-config-guard.ts +4 -0
  97. package/src/extensions/catalog.generated.json +17 -0
  98. package/src/extensions/catalog.ts +1 -1
  99. package/src/extensions/dispatcher.ts +3 -3
  100. package/src/heartbeat/heartbeat.ts +152 -2
  101. package/src/heartbeat/templates.ts +16 -0
  102. package/src/http/all-routes.ts +1 -0
  103. package/src/http/index.ts +2 -0
  104. package/src/http/integrations.ts +3 -1
  105. package/src/http/mcp-bridge.ts +1 -0
  106. package/src/http/pages-public.ts +7 -2
  107. package/src/http/pages.ts +178 -0
  108. package/src/http/script-connections.ts +35 -0
  109. package/src/http/slack-inbound.ts +107 -0
  110. package/src/http/tasks.ts +45 -27
  111. package/src/http/users.ts +1 -1
  112. package/src/http/webhooks.ts +20 -20
  113. package/src/prompts/session-templates.ts +32 -0
  114. package/src/providers/acp-targets.ts +1 -1
  115. package/src/providers/amp-adapter.ts +2 -2
  116. package/src/providers/claude-adapter.ts +6 -0
  117. package/src/providers/claude-background-keepalive.ts +100 -0
  118. package/src/providers/claude-managed-adapter.ts +2 -11
  119. package/src/providers/claude-sdk-session.ts +7 -1
  120. package/src/providers/dsh-adapter.ts +1 -1
  121. package/src/providers/opencode-adapter.ts +1 -2
  122. package/src/providers/pi-mono-adapter.ts +3 -3
  123. package/src/realtime/browser.generated.txt +8 -8
  124. package/src/realtime/browser.ts +3 -3
  125. package/src/scheduler/scheduler.ts +82 -13
  126. package/src/scripts-runtime/executors/quickjs-pool-config.ts +26 -0
  127. package/src/scripts-runtime/executors/quickjs.ts +9 -2
  128. package/src/scripts-runtime/executors/registry.ts +1 -1
  129. package/src/server.ts +1 -0
  130. package/src/slack/actions.ts +6 -2
  131. package/src/slack/app.ts +5 -0
  132. package/src/slack/assistant.ts +15 -5
  133. package/src/slack/egress-scrub.ts +3 -2
  134. package/src/slack/handlers.ts +36 -18
  135. package/src/slack/inbound-dispatch.ts +549 -0
  136. package/src/slack/inbound-files.ts +2 -0
  137. package/src/telemetry-context.ts +1 -0
  138. package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
  139. package/src/tests/claude-background-keepalive.fixture.ts +162 -0
  140. package/src/tests/claude-background-keepalive.test.ts +139 -0
  141. package/src/tests/codex-oauth-device.test.ts +1 -1
  142. package/src/tests/connection-embedded-auth.test.ts +51 -0
  143. package/src/tests/docs-index-footprint.test.js +3 -3
  144. package/src/tests/dsh-adapter.test.ts +1 -1
  145. package/src/tests/extensions-catalog.test.ts +1 -0
  146. package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
  147. package/src/tests/heartbeat-checklist.test.ts +243 -0
  148. package/src/tests/pages-feedback.test.ts +218 -0
  149. package/src/tests/schedule-target-type.test.ts +136 -0
  150. package/src/tests/script-connections-http.test.ts +45 -0
  151. package/src/tests/script-connections-mcp.test.ts +41 -0
  152. package/src/tests/script-connections.test.ts +2 -0
  153. package/src/tests/script-executor-quickjs-pool.test.ts +106 -0
  154. package/src/tests/script-executor-registry.test.ts +56 -0
  155. package/src/tests/send-task-self-send.test.ts +107 -0
  156. package/src/tests/slack-inbound-diagnostics-auth.test.ts +93 -0
  157. package/src/tests/slack-inbound-receipts.test.ts +684 -0
  158. package/src/tests/slack-session-status.test.ts +4 -2
  159. package/src/tests/ui-e2e-ingest-payload.test.ts +5 -1
  160. package/src/tests/user-token-routes.test.ts +1 -1
  161. package/src/tests/workflow-executors.test.ts +3 -2
  162. package/src/tests/workflow-sub-workflow.test.ts +363 -0
  163. package/src/tools/script-connections/tool.ts +35 -3
  164. package/src/tools/send-task.ts +9 -1
  165. package/src/utils/internal-ai/complete-structured.ts +6 -3
  166. package/src/utils/runtime-model-catalog.ts +1 -1
  167. package/src/workflows/engine.ts +17 -1
  168. package/src/workflows/executors/agent-task.ts +2 -2
  169. package/src/workflows/executors/human-in-the-loop.ts +3 -3
  170. package/src/workflows/executors/registry.ts +2 -0
  171. package/src/workflows/executors/script.ts +1 -1
  172. package/src/workflows/executors/sub-workflow.ts +135 -0
  173. package/src/workflows/executors/wait.ts +3 -3
  174. package/src/workflows/recovery.ts +11 -1
  175. package/src/workflows/resume.ts +77 -0
  176. package/src/workflows/wait-filter.ts +5 -7
  177. package/templates/extensions/README.md +1 -0
  178. package/templates/extensions/slack-reply-gate/README.md +98 -0
  179. package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
  180. package/templates/extensions/slack-reply-gate/manifest.json +8 -0
  181. package/templates/skills/heartbeat-runbook/content.md +3 -1
  182. package/templates/skills/pages/content.md +25 -0
  183. package/templates/skills/swarm-scripts/SKILL.md +10 -0
  184. package/templates/skills/swarm-scripts/content.md +10 -0
@@ -254,6 +254,51 @@ describe("/api/script-connections HTTP", () => {
254
254
  expect(await res.json()).toEqual({ error: "Only the lead can manage script connections." });
255
255
  });
256
256
 
257
+ test("DELETE removes a connection, its managed binding, and its inline secret", async () => {
258
+ const created = await dispatch("/api/script-connections", {
259
+ method: "POST",
260
+ agentId: leadAgentId,
261
+ body: {
262
+ kind: "openapi",
263
+ slug: "deleteHttpVendor",
264
+ baseUrl: "https://api.vendor.test",
265
+ allowedHosts: ["api.vendor.test"],
266
+ auth: { type: "bearer", secret: "example-http-delete-tok" },
267
+ openapiSpecJson: inlineOpenApiSpec(),
268
+ },
269
+ });
270
+ expect(created.status).toBe(200);
271
+ const { connection } = (await created.json()) as { connection: { id: string } };
272
+
273
+ const forbidden = await dispatch(`/api/script-connections/${connection.id}`, {
274
+ method: "DELETE",
275
+ agentId: workerAgentId,
276
+ });
277
+ expect(forbidden.status).toBe(403);
278
+
279
+ const res = await dispatch(`/api/script-connections/${connection.id}`, {
280
+ method: "DELETE",
281
+ agentId: leadAgentId,
282
+ });
283
+ expect(res.status).toBe(200);
284
+ expect(await res.json()).toEqual({ deleted: true, id: connection.id });
285
+
286
+ const counts = await getDbClient().get<{ conns: number; bindings: number; secrets: number }>(
287
+ `SELECT
288
+ (SELECT COUNT(*) FROM script_connections WHERE id = ?) AS conns,
289
+ (SELECT COUNT(*) FROM script_credential_bindings WHERE managed_by_connection_id = ?) AS bindings,
290
+ (SELECT COUNT(*) FROM swarm_config WHERE key = 'connection.deleteHttpVendor.secret') AS secrets`,
291
+ [connection.id, connection.id],
292
+ );
293
+ expect(counts).toEqual({ conns: 0, bindings: 0, secrets: 0 });
294
+
295
+ const missing = await dispatch(`/api/script-connections/${connection.id}`, {
296
+ method: "DELETE",
297
+ agentId: leadAgentId,
298
+ });
299
+ expect(missing.status).toBe(404);
300
+ });
301
+
257
302
  test("list returns connections without secrets", async () => {
258
303
  await upsertSwarmConfig({
259
304
  scope: "global",
@@ -907,3 +907,44 @@ describe("script MCP connections", () => {
907
907
  }
908
908
  });
909
909
  });
910
+
911
+ describe("script-connections MCP delete", () => {
912
+ test("lead deletes a connection and its inline secret; unknown id and non-lead fail", async () => {
913
+ const lead = await createAgent({ name: "mcp-delete-lead", isLead: true, status: "idle" });
914
+ const worker = await createAgent({ name: "mcp-delete-worker", isLead: false, status: "idle" });
915
+ const connection = await upsertScriptConnection({
916
+ slug: "mcpDeleteVendor",
917
+ kind: "graphql",
918
+ baseUrl: "https://api.vendor.test/graphql",
919
+ allowedHosts: ["api.vendor.test"],
920
+ auth: { type: "bearer", secret: "example-mcp-delete-tok" },
921
+ });
922
+
923
+ const denied = (await scriptConnectionsTool().handler(
924
+ { action: "delete", id: connection.id },
925
+ meta(worker.id),
926
+ )) as ToolResult;
927
+ expect(denied.structuredContent.success).toBe(false);
928
+ expect(await getScriptConnectionById(connection.id)).not.toBeNull();
929
+
930
+ const result = (await scriptConnectionsTool().handler(
931
+ { action: "delete", id: connection.id },
932
+ meta(lead.id),
933
+ )) as ToolResult;
934
+ expect(result.structuredContent.success).toBe(true);
935
+ expect(result.structuredContent.connections.map((c) => c.id)).not.toContain(connection.id);
936
+ expect(await getScriptConnectionById(connection.id)).toBeNull();
937
+ const secretRow = await getDbClient().get<{ key: string }>(
938
+ "SELECT key FROM swarm_config WHERE key = ?",
939
+ ["connection.mcpDeleteVendor.secret"],
940
+ );
941
+ expect(secretRow).toBeNull();
942
+
943
+ const again = (await scriptConnectionsTool().handler(
944
+ { action: "delete", id: connection.id },
945
+ meta(lead.id),
946
+ )) as ToolResult;
947
+ expect(again.structuredContent.success).toBe(false);
948
+ expect(again.structuredContent.message).toContain("not found");
949
+ });
950
+ });
@@ -615,6 +615,8 @@ describe("script connections", () => {
615
615
  markMigrationApplied(database, "201_workflow_replay_columns.sql");
616
616
  // 202 moves rows out of kv_entries, which this migration-112-only fixture does not create.
617
617
  markMigrationApplied(database, "202_codex_oauth_device_flows.sql");
618
+ // 203 alters workflow_runs, which this migration-112-only fixture does not create.
619
+ markMigrationApplied(database, "203_workflow_run_parent_step.sql");
618
620
 
619
621
  const id = crypto.randomUUID();
620
622
  const now = new Date().toISOString();
@@ -0,0 +1,106 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { CHILD_PROCESS_TEST_BUDGET_MS, expectChildOk, runChild } from "./test-proc";
3
+
4
+ // Each child gets a fresh shared pool. Hold host calls open so queued runs
5
+ // cannot reuse an idle worker and hide the configured concurrency limit.
6
+ const probe = `
7
+ import { QuickJSScriptExecutor, quickJSWorkerCount } from "./src/scripts-runtime/executors/quickjs";
8
+ import { DEFAULT_SCRIPT_RESOURCES } from "./src/scripts-runtime/executors/types";
9
+ let release;
10
+ const gate = new Promise(resolve => { release = resolve; });
11
+ let started = 0;
12
+ let inFlight = 0;
13
+ let peak = 0;
14
+ const server = Bun.serve({ port: 0, async fetch() {
15
+ started++;
16
+ peak = Math.max(peak, ++inFlight);
17
+ await gate;
18
+ inFlight--;
19
+ return new Response("ok");
20
+ }});
21
+ try {
22
+ const executor = new QuickJSScriptExecutor();
23
+ const input = {
24
+ source: 'export default async (args) => (await fetch(args.url)).text();',
25
+ args: { url: "http://127.0.0.1:" + server.port },
26
+ configPayload: { system: {
27
+ apiKey: { value: "pool-test", isSecret: true },
28
+ agentId: { value: crypto.randomUUID(), isSecret: false },
29
+ mcpBaseUrl: { value: "http://127.0.0.1:" + server.port, isSecret: false },
30
+ }, user: {} },
31
+ resources: DEFAULT_SCRIPT_RESOURCES,
32
+ fsMode: "none", network: "open",
33
+ };
34
+ const jobs = Array.from({ length: 10 }, () => executor.run(input));
35
+ const workers = quickJSWorkerCount();
36
+ const deadline = Date.now() + 10_000;
37
+ while (started < workers && Date.now() < deadline) await Bun.sleep(10);
38
+ const heldPeak = peak;
39
+ release();
40
+ const outputs = await Promise.all(jobs);
41
+ process.env.SCRIPT_QUICKJS_POOL_SIZE = "16";
42
+ const later = await executor.run(input);
43
+ console.log(JSON.stringify({ workers, heldPeak, results: outputs.map(o => o.result),
44
+ errors: outputs.map(o => o.error), stderr: outputs[0].stderr,
45
+ retainedWorkers: quickJSWorkerCount(), laterResult: later.result }));
46
+ } finally {
47
+ release();
48
+ server.stop(true);
49
+ }
50
+ `;
51
+
52
+ async function inspectPool(size?: string) {
53
+ const env = { ...process.env };
54
+ if (size === undefined) delete env.SCRIPT_QUICKJS_POOL_SIZE;
55
+ else env.SCRIPT_QUICKJS_POOL_SIZE = size;
56
+ const result = expectChildOk(
57
+ await runChild([process.execPath, "--eval", probe], { env }),
58
+ "QuickJS pool probe",
59
+ );
60
+ return JSON.parse(result.stdout.trim()) as {
61
+ workers: number;
62
+ heldPeak: number;
63
+ results: string[];
64
+ errors: Array<string | null>;
65
+ stderr: string;
66
+ retainedWorkers: number;
67
+ laterResult: string;
68
+ };
69
+ }
70
+
71
+ describe("QuickJS pool configuration", () => {
72
+ for (const [size, expected] of [
73
+ [undefined, 4],
74
+ ["1", 1],
75
+ ["2", 2],
76
+ ["8", 8],
77
+ ] as const) {
78
+ test(
79
+ `pool size ${size ?? "unset"} controls workers and queues excess runs`,
80
+ async () => {
81
+ const result = await inspectPool(size);
82
+ expect(result.workers).toBe(expected);
83
+ expect(result.heldPeak).toBe(expected);
84
+ expect(result.results).toEqual(Array(10).fill("ok"));
85
+ expect(result.errors).toEqual(Array(10).fill(null));
86
+ expect(result.retainedWorkers).toBe(expected);
87
+ expect(result.laterResult).toBe("ok");
88
+ },
89
+ CHILD_PROCESS_TEST_BUDGET_MS,
90
+ );
91
+ }
92
+
93
+ for (const size of ["0", "33"]) {
94
+ test(
95
+ `invalid pool size ${size} fails before spawning workers`,
96
+ async () => {
97
+ const result = await inspectPool(size);
98
+ expect(result.workers).toBe(0);
99
+ expect(result.errors).toEqual(Array(10).fill("executor_error"));
100
+ expect(result.stderr).toContain("SCRIPT_QUICKJS_POOL_SIZE");
101
+ expect(result.stderr).toContain("integer between 1 and 32");
102
+ },
103
+ CHILD_PROCESS_TEST_BUDGET_MS,
104
+ );
105
+ }
106
+ });
@@ -1,6 +1,8 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { validateConfigValue } from "../be/swarm-config-guard";
2
3
  import { NativeScriptExecutor } from "../scripts-runtime/executors/native";
3
4
  import { QuickJSScriptExecutor } from "../scripts-runtime/executors/quickjs";
5
+ import { resolveQuickJSPoolSize } from "../scripts-runtime/executors/quickjs-pool-config";
4
6
  import { getScriptExecutor } from "../scripts-runtime/executors/registry";
5
7
 
6
8
  describe("getScriptExecutor", () => {
@@ -20,3 +22,57 @@ describe("getScriptExecutor", () => {
20
22
  expect(() => getScriptExecutor("e2b")).toThrow("Available: native, quickjs");
21
23
  });
22
24
  });
25
+
26
+ describe("SCRIPT_EXECUTOR configuration", () => {
27
+ test("every accepted value resolves to its executor", () => {
28
+ for (const value of ["native", "quickjs", " native ", " quickjs "]) {
29
+ expect(validateConfigValue("SCRIPT_EXECUTOR", value)).toBeNull();
30
+ expect(getScriptExecutor(value).name).toBe(value.trim());
31
+ }
32
+ });
33
+
34
+ test("rejects unsupported names, blank values and non-string values", () => {
35
+ for (const value of ["e2b", "Native", "QUICKJS", "", " ", null, undefined, 1, true]) {
36
+ expect(validateConfigValue("SCRIPT_EXECUTOR", value)).toBe(
37
+ "Invalid SCRIPT_EXECUTOR value (must be one of: native, quickjs)",
38
+ );
39
+ }
40
+ });
41
+ });
42
+
43
+ describe("SCRIPT_QUICKJS_POOL_SIZE configuration", () => {
44
+ test("defaults to four and accepts bounded whole numbers", () => {
45
+ expect(resolveQuickJSPoolSize(undefined)).toBe(4);
46
+ for (const value of ["1", "4", "32", " 2 ", 1, 32]) {
47
+ expect(validateConfigValue("SCRIPT_QUICKJS_POOL_SIZE", value)).toBeNull();
48
+ expect(resolveQuickJSPoolSize(String(value))).toBe(Number(value));
49
+ }
50
+ });
51
+
52
+ test("config writes and deployment env reject invalid sizes", () => {
53
+ for (const value of [
54
+ 0,
55
+ 33,
56
+ 1.5,
57
+ "0",
58
+ "33",
59
+ "1.5",
60
+ "1e1",
61
+ "0x10",
62
+ "NaN",
63
+ "",
64
+ " ",
65
+ "99999999999999999",
66
+ ]) {
67
+ expect(validateConfigValue("SCRIPT_QUICKJS_POOL_SIZE", value)).toContain(
68
+ "integer between 1 and 32",
69
+ );
70
+ expect(() => resolveQuickJSPoolSize(String(value))).toThrow("integer between 1 and 32");
71
+ }
72
+ for (const value of [null, undefined, true, [2], {}, { toString: null }]) {
73
+ expect(validateConfigValue("SCRIPT_QUICKJS_POOL_SIZE", value)).toContain(
74
+ "integer between 1 and 32",
75
+ );
76
+ }
77
+ });
78
+ });
@@ -0,0 +1,107 @@
1
+ // send-task refuses an agent session that targets itself, but a script runs as its owner (a
2
+ // scheduled script as the schedule's creator), so a Lead-owned script must be able to file
3
+ // work for the Lead. Script calls reach the tool through /api/mcp-bridge as `script-sdk`.
4
+
5
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
6
+ import { unlinkSync } from "node:fs";
7
+ import {
8
+ createServer as createHttpServer,
9
+ type IncomingMessage,
10
+ type Server,
11
+ type ServerResponse,
12
+ } from "node:http";
13
+ import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
14
+ import { handleMcpBridge } from "../http/mcp-bridge";
15
+ import { getPathSegments } from "../http/utils";
16
+ import { sendTaskHandler } from "../tools/send-task";
17
+ import { ownerCtx } from "../tools/task-tool-ctx";
18
+ import { getRequestInfo } from "../tools/utils";
19
+
20
+ const TEST_DB_PATH = "./test-send-task-self-send.sqlite";
21
+
22
+ let server: Server;
23
+ let baseUrl: string;
24
+ let leadId = "";
25
+
26
+ const selfSendArgs = (agentId: string) => ({
27
+ task: "x mention to triage",
28
+ agentId,
29
+ routingReason: "skill" as const,
30
+ routingNote: "Lead triages mentions itself",
31
+ offerMode: false,
32
+ allowDuplicate: true,
33
+ });
34
+
35
+ async function tasksFor(agentId: string): Promise<number> {
36
+ return (await getDbClient().get<{ c: number }>(
37
+ "SELECT COUNT(*) AS c FROM agent_tasks WHERE agentId = ?",
38
+ [agentId],
39
+ ))!.c;
40
+ }
41
+
42
+ beforeAll(async () => {
43
+ initDb(TEST_DB_PATH);
44
+ leadId = (
45
+ await createAgent({ name: "self-send-lead", isLead: true, status: "idle", maxTasks: 5 })
46
+ ).id;
47
+ server = createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
48
+ const myAgentId = req.headers["x-agent-id"] as string | undefined;
49
+ const ok = await handleMcpBridge(
50
+ req,
51
+ res,
52
+ getPathSegments(req.url || ""),
53
+ undefined,
54
+ myAgentId,
55
+ );
56
+ if (!ok) {
57
+ res.writeHead(404);
58
+ res.end("Not Found");
59
+ }
60
+ });
61
+ await new Promise<void>((resolve) => server.listen(0, resolve));
62
+ const addr = server.address();
63
+ if (!addr || typeof addr === "string") throw new Error("no port");
64
+ baseUrl = `http://127.0.0.1:${addr.port}`;
65
+ });
66
+
67
+ afterAll(() => {
68
+ server.close();
69
+ closeDb();
70
+ for (const suffix of ["", "-wal", "-shm"]) {
71
+ try {
72
+ unlinkSync(TEST_DB_PATH + suffix);
73
+ } catch {
74
+ // ignore
75
+ }
76
+ }
77
+ });
78
+
79
+ describe("send-task self-send guard", () => {
80
+ test("a script sending a task to its own agent succeeds", async () => {
81
+ const before = await tasksFor(leadId);
82
+ const res = await fetch(`${baseUrl}/api/mcp-bridge`, {
83
+ method: "POST",
84
+ headers: { "Content-Type": "application/json", "x-agent-id": leadId },
85
+ body: JSON.stringify({ tool: "send-task", args: selfSendArgs(leadId) }),
86
+ });
87
+ expect(res.status).toBe(200);
88
+ const body = (await res.json()) as { success?: boolean; message?: string };
89
+ expect(body.message).not.toContain("Cannot send a task to yourself");
90
+ expect(body.success).toBe(true);
91
+ expect(await tasksFor(leadId)).toBe(before + 1);
92
+ });
93
+
94
+ test("an agent session sending a task to itself is still refused", async () => {
95
+ const before = await tasksFor(leadId);
96
+ // An MCP session's meta carries no origin mark, so getRequestInfo reports `mcp`.
97
+ const info = getRequestInfo({
98
+ sessionId: "session-1",
99
+ requestInfo: { headers: { "x-agent-id": leadId } },
100
+ } as unknown as Parameters<typeof getRequestInfo>[0]);
101
+ expect(info.callOrigin).toBe("mcp");
102
+ const result = await sendTaskHandler(ownerCtx(info), selfSendArgs(leadId));
103
+ expect(result.ok).toBe(false);
104
+ expect(result.message).toContain("Cannot send a task to yourself");
105
+ expect(await tasksFor(leadId)).toBe(before);
106
+ });
107
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * GET /api/slack/inbound/diagnostics is operator-only. `auth.apiKey` admits
3
+ * user `aswt_` tokens and verb-less GETs pass RBAC admission, so the handler
4
+ * gates on the principal kind. Runs the real handleCore auth path.
5
+ */
6
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7
+ import { unlink } from "node:fs/promises";
8
+ import {
9
+ createServer as createHttpServer,
10
+ type IncomingMessage,
11
+ type Server,
12
+ type ServerResponse,
13
+ } from "node:http";
14
+ import { closeDb, createUser, initDb } from "../be/db";
15
+ import { ensureRbacSeedsSynced } from "../be/rbac-roles";
16
+ import { type IdentityActor, mintToken } from "../be/users";
17
+ import { handleCore } from "../http/core";
18
+ import { handleSlackInbound } from "../http/slack-inbound";
19
+ import { getPathSegments } from "../http/utils";
20
+ import { listenOnFreePort } from "./test-net";
21
+
22
+ const TEST_DB_PATH = "./test-slack-inbound-diagnostics-auth.sqlite";
23
+ const API_KEY = "example-test-api-key";
24
+ const ACTOR: IdentityActor = { kind: "operator", id: "op:test" };
25
+ const PATH = "/api/slack/inbound/diagnostics";
26
+
27
+ let server: Server;
28
+ let port = 0;
29
+ let savedRbac: string | undefined;
30
+
31
+ async function removeDbFiles() {
32
+ for (const suffix of ["", "-wal", "-shm"]) {
33
+ try {
34
+ await unlink(TEST_DB_PATH + suffix);
35
+ } catch {
36
+ // File does not exist.
37
+ }
38
+ }
39
+ }
40
+
41
+ async function get(bearer: string): Promise<{ status: number; body: unknown }> {
42
+ const res = await fetch(`http://localhost:${port}${PATH}`, {
43
+ headers: { Authorization: `Bearer ${bearer}` },
44
+ });
45
+ return { status: res.status, body: await res.json() };
46
+ }
47
+
48
+ beforeAll(async () => {
49
+ savedRbac = process.env.RBAC_ENABLED;
50
+ await removeDbFiles();
51
+ initDb(TEST_DB_PATH);
52
+ ensureRbacSeedsSynced({ quiet: true });
53
+ server = createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
54
+ if (await handleCore(req, res, undefined, API_KEY)) return;
55
+ if (await handleSlackInbound(req, res, getPathSegments(req.url || ""))) return;
56
+ res.writeHead(404, { "Content-Type": "application/json" });
57
+ res.end(JSON.stringify({ error: "Not Found" }));
58
+ });
59
+ port = await listenOnFreePort(server);
60
+ });
61
+
62
+ afterAll(async () => {
63
+ await new Promise<void>((resolve) => server.close(() => resolve()));
64
+ closeDb();
65
+ await removeDbFiles();
66
+ if (savedRbac === undefined) delete process.env.RBAC_ENABLED;
67
+ else process.env.RBAC_ENABLED = savedRbac;
68
+ });
69
+
70
+ describe("GET /api/slack/inbound/diagnostics authorization", () => {
71
+ test("operator key reads diagnostics", async () => {
72
+ const res = await get(API_KEY);
73
+ expect(res.status).toBe(200);
74
+ expect(res.body).toHaveProperty("receipts.counts");
75
+ });
76
+
77
+ test("active user token is refused with 403 whether RBAC admission is on or off", async () => {
78
+ const user = await createUser({ name: "Diagnostics User" });
79
+ const { plaintext } = await mintToken(user.id, "diagnostics", ACTOR);
80
+
81
+ for (const flag of ["true", "false"]) {
82
+ process.env.RBAC_ENABLED = flag;
83
+ const res = await get(plaintext);
84
+ expect(res.status).toBe(403);
85
+ expect(res.body).toEqual({ error: "Operator access required" });
86
+ }
87
+ });
88
+
89
+ test("unknown bearer is refused with 401", async () => {
90
+ const res = await get("aswt_not-a-real-token");
91
+ expect(res.status).toBe(401);
92
+ });
93
+ });