@desplega.ai/agent-swarm 1.162.0 → 1.164.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 (240) hide show
  1. package/README.md +2 -1
  2. package/dist/{acp-adapter-74m73ae9.js → acp-adapter-8t22d18g.js} +2 -2
  3. package/dist/{actions-4sr14gnt.js → actions-49xrhww7.js} +32 -29
  4. package/dist/amp-adapter-p230tajt.js +34 -0
  5. package/dist/{anthropic-messages-8xxrzky9.js → anthropic-messages-hst62ws9.js} +3 -3
  6. package/dist/{app-pt68fbwp.js → app-tgzw3f3d.js} +11 -10
  7. package/dist/{assistant-cv3jytks.js → assistant-dgfjm07j.js} +33 -30
  8. package/dist/{azure-openai-responses-f5cqsyj8.js → azure-openai-responses-mh8tvf78.js} +9 -68
  9. package/dist/{boot-reembed-4ht4gqth.js → boot-reembed-0qzs614r.js} +13 -12
  10. package/dist/{boot-reembed-nxpcvpj9.js → boot-reembed-dgp6peyx.js} +12 -11
  11. package/dist/{boot-scrub-logs-k93kyw8k.js → boot-scrub-logs-fxnk021a.js} +10 -9
  12. package/dist/{boot-seeds-1skcrs0e.js → boot-seeds-xe7adjvm.js} +14 -13
  13. package/dist/{claude-adapter-r5g0tzge.js → claude-adapter-hx0n8a31.js} +22 -20
  14. package/dist/{claude-managed-adapter-yxkfxg8j.js → claude-managed-adapter-hskapeen.js} +3 -3
  15. package/dist/{claude-managed-setup-pkyrgshb.js → claude-managed-setup-p7mt6dfq.js} +1 -1
  16. package/dist/{claude-sdk-session-jxyxnrwd.js → claude-sdk-session-d61a9c8x.js} +22 -20
  17. package/dist/{cli-twrh04ap.js → cli-0y30kqz4.js} +3 -3
  18. package/dist/{cli-5xhpb3kv.js → cli-0z9dj97t.js} +9 -9
  19. package/dist/{cli-t845n9xk.js → cli-1nzb8ftr.js} +2 -2
  20. package/dist/{cli-kdkwd3yr.js → cli-1wd1n2ya.js} +38 -10
  21. package/dist/{cli-vxe6qa4h.js → cli-23az412e.js} +1 -1
  22. package/dist/{cli-bsky8g7w.js → cli-26x4d89j.js} +1 -1
  23. package/dist/{cli-swfmp1fx.js → cli-2ar0x7cz.js} +9 -9
  24. package/dist/cli-2ksgp01n.js +933 -0
  25. package/dist/{cli-rnyzn44p.js → cli-2zhhnz2h.js} +1 -1
  26. package/dist/{cli-vc9bp061.js → cli-3qaw32we.js} +5 -5
  27. package/dist/{cli-vy3ed88x.js → cli-402m8btc.js} +31 -3
  28. package/dist/{cli-5bcd7sfy.js → cli-43fh8fzw.js} +3 -3
  29. package/dist/{cli-5k6k8ta3.js → cli-4f7s6k3q.js} +13 -5
  30. package/dist/{cli-sqvfa090.js → cli-4ntg6tkp.js} +6 -5
  31. package/dist/{cli-kh04eqwe.js → cli-4y9bnpvy.js} +1 -1
  32. package/dist/{cli-qjjvzwzd.js → cli-5c2j5yvx.js} +1 -1
  33. package/dist/{cli-2zy95vhm.js → cli-6891ft0y.js} +2 -2
  34. package/dist/{cli-tre4bdgq.js → cli-6behs2bt.js} +1 -1
  35. package/dist/cli-7mm2r1kp.js +17 -0
  36. package/dist/cli-7xcgj20z.js +68 -0
  37. package/dist/{cli-4cwxn7gp.js → cli-812vt3ht.js} +2 -2
  38. package/dist/{cli-mbssg1xv.js → cli-8kt0bmgx.js} +1 -1
  39. package/dist/{cli-wjgs5wxe.js → cli-8rpq35c0.js} +7 -7
  40. package/dist/{cli-jxs2ygaw.js → cli-9m8fk2v7.js} +74 -29
  41. package/dist/{cli-pkd6s2wy.js → cli-9q3wt23f.js} +2 -2
  42. package/dist/{cli-s3hwpt9v.js → cli-9s6fsg07.js} +30 -10
  43. package/dist/{cli-xqpk3rp7.js → cli-9x3bwfd3.js} +28 -5
  44. package/dist/{cli-qrgt5bqf.js → cli-agamgm0p.js} +1 -1
  45. package/dist/{cli-13hw4z91.js → cli-b0f70z24.js} +1 -1
  46. package/dist/{cli-gpqpmns7.js → cli-cgrvsz2t.js} +2 -2
  47. package/dist/{cli-2hhfhfq5.js → cli-cn53e7vc.js} +8 -1
  48. package/dist/{cli-p0r8sxj5.js → cli-crjazzsg.js} +222 -4
  49. package/dist/{cli-tcen9p5w.js → cli-d37n8b7c.js} +5 -5
  50. package/dist/{cli-wxkkd0e0.js → cli-dgh781z9.js} +3 -3
  51. package/dist/cli-dsdg2wrk.js +516 -0
  52. package/dist/{cli-q24re23n.js → cli-e2xnc4m5.js} +4 -4
  53. package/dist/{cli-f7kvq46p.js → cli-erxeg64j.js} +20 -0
  54. package/dist/{cli-8ev9xqnx.js → cli-fajbc7sa.js} +17 -219
  55. package/dist/{cli-g739zqzt.js → cli-fj86xr3c.js} +2 -2
  56. package/dist/{cli-zvhd3f64.js → cli-fpbvcf55.js} +3 -3
  57. package/dist/{cli-kp42yek0.js → cli-fyra5y8k.js} +424 -71
  58. package/dist/{cli-jyy6pm2f.js → cli-g3qgrzck.js} +15 -3
  59. package/dist/{cli-eg0b3p0k.js → cli-g5erf0zt.js} +23 -14
  60. package/dist/{cli-cvatsam7.js → cli-gcvzvq3e.js} +4 -4
  61. package/dist/{cli-89s4ngw3.js → cli-gvcgmx69.js} +1 -1
  62. package/dist/{cli-7tre70zp.js → cli-j261c831.js} +138 -128
  63. package/dist/{cli-sptrj5f5.js → cli-j3me0jw2.js} +2 -2
  64. package/dist/{cli-zxqx25nz.js → cli-k6w44yvq.js} +6 -6
  65. package/dist/cli-ndx5yz5n.js +3299 -0
  66. package/dist/{cli-ykk5jaax.js → cli-nexwh8zg.js} +34 -29
  67. package/dist/{cli-rfjw9jw2.js → cli-ng97fjq7.js} +2 -2
  68. package/dist/{cli-4x0csdjq.js → cli-ngne1dng.js} +2 -2
  69. package/dist/{cli-4dcpaefp.js → cli-pfzrhb8n.js} +2 -2
  70. package/dist/{cli-hb5c2x5y.js → cli-q38n4bq0.js} +2 -2
  71. package/dist/{cli-cebmgwsc.js → cli-r109nq7n.js} +4 -4
  72. package/dist/{cli-nq7yqw7b.js → cli-t7g0vmtc.js} +4 -4
  73. package/dist/{cli-jf7wmv1r.js → cli-tsvj33y0.js} +58 -19
  74. package/dist/{cli-7af8yhav.js → cli-vbr859rz.js} +7 -4
  75. package/dist/{cli-nt8hecg9.js → cli-wk3v6h55.js} +67 -38
  76. package/dist/{cli-vyqs9ya8.js → cli-wwvk0tsy.js} +7 -7
  77. package/dist/{cli-rj0gqwwp.js → cli-y2de3bzp.js} +1 -1
  78. package/dist/{cli-hmsw1gpy.js → cli-y74170pb.js} +4 -4
  79. package/dist/cli-ydt3zkwv.js +70 -0
  80. package/dist/{cli-xk79303e.js → cli-yj790e19.js} +9 -1
  81. package/dist/cli-zt3w1cn1.js +137 -0
  82. package/dist/{cli-trc24wk2.js → cli-zxg80zs9.js} +1 -1
  83. package/dist/cli.js +23 -22
  84. package/dist/{cloudflare-workers-ai-system-one-k8ddqjkq.js → cloudflare-workers-ai-system-one-tt484t61.js} +2 -2
  85. package/dist/{codex-adapter-esvmnhn7.js → codex-adapter-e3pky8aj.js} +14 -13
  86. package/dist/{codex-hook-m73jmqgx.js → codex-hook-b1fb7c2k.js} +4 -3
  87. package/dist/{codex-session-runner-ban9excr.js → codex-session-runner-1gq27w2t.js} +14 -13
  88. package/dist/{commands-75413p2n.js → commands-q07ezjzn.js} +10 -9
  89. package/dist/cursor-adapter-50cz7yba.js +32 -0
  90. package/dist/{db-7f5pecwg.js → db-wwt0sq9q.js} +18 -9
  91. package/dist/{dispatcher-fh9at8nz.js → dispatcher-wrg35vdd.js} +31 -28
  92. package/dist/{dsh-adapter-wpze65d4.js → dsh-adapter-sa6rxfpc.js} +6 -6
  93. package/dist/{e2b-gjgdjp91.js → e2b-v5np73yj.js} +4 -4
  94. package/dist/{easter-egg-3d-3w5c2n4c.js → easter-egg-3d-h2hx9b64.js} +2 -2
  95. package/dist/{event-bus-sfx8dx3t.js → event-bus-vkbhzt4n.js} +1 -1
  96. package/dist/{execute-f50wg9nh.js → execute-6gczxy8v.js} +42 -13
  97. package/dist/{google-generative-ai-jxxpnegy.js → google-generative-ai-d6nphg3q.js} +3 -3
  98. package/dist/{google-vertex-x7q1angm.js → google-vertex-cpshkref.js} +3 -3
  99. package/dist/{handlers-w13787bb.js → handlers-rv3gxvsp.js} +34 -31
  100. package/dist/{hook-46g1jrvx.js → hook-bcf5m80y.js} +15 -13
  101. package/dist/{hook-4tpts2ry.js → hook-z8b1jm2d.js} +16 -14
  102. package/dist/{http-19pzbfrt.js → http-56f7yt97.js} +932 -134
  103. package/dist/{index-scptvdxg.js → index-0kncjnrm.js} +30 -23
  104. package/dist/{index-dqmp15bq.js → index-1e6cg96n.js} +31 -28
  105. package/dist/{index-qb4q1y3e.js → index-8xgdb3k8.js} +31 -28
  106. package/dist/index-adw4zr9j.js +1020 -0
  107. package/dist/{index-h5gs261z.js → index-etdrp7yp.js} +140 -140
  108. package/dist/{index-8fd4dm4h.js → index-f9zscmft.js} +32 -29
  109. package/dist/index-fdk1t4d6.js +91297 -0
  110. package/dist/{index-s75wc4wd.js → index-jvb37h77.js} +12 -11
  111. package/dist/{keepalive-xa5ts9cd.js → keepalive-rp8daj4s.js} +12 -11
  112. package/dist/{lead-qp4xkvjq.js → lead-dfp77erc.js} +45 -40
  113. package/dist/{maintenance-rm5885hm.js → maintenance-wtmw3jqv.js} +13 -12
  114. package/dist/{mistral-conversations-rw7fbt22.js → mistral-conversations-8445ddjn.js} +2 -2
  115. package/dist/{oauth-refresh-sweep-1csdfebf.js → oauth-refresh-sweep-yhzqt95v.js} +11 -10
  116. package/dist/{onboard-r6b78cs4.js → onboard-9z8rwbcy.js} +2 -2
  117. package/dist/{openai-codex-responses-efvrgrq3.js → openai-codex-responses-9ynm88zy.js} +3 -3
  118. package/dist/{openai-completions-bbbrrz4z.js → openai-completions-nhqwjmaw.js} +2 -2
  119. package/dist/{openai-responses-9jjyrcm1.js → openai-responses-xx1c4kgm.js} +3 -3
  120. package/dist/{opencode-adapter-kb3g6pkf.js → opencode-adapter-1nxb6555.js} +5 -5
  121. package/dist/{otel-impl-0y9knpky.js → otel-impl-b83gaxw2.js} +1 -1
  122. package/dist/{pi-mono-adapter-z6s1bme2.js → pi-mono-adapter-ddxz8pjp.js} +18 -17
  123. package/dist/{pricing-refresh-mth26tgp.js → pricing-refresh-tmjvazb6.js} +12 -11
  124. package/dist/{rbac-roles-kwntb23q.js → rbac-roles-pa0gzjws.js} +11 -10
  125. package/dist/{rbac-roles-vpk4ebm8.js → rbac-roles-rhexa9s2.js} +12 -11
  126. package/dist/{render-v2-hcm7h00y.js → render-v2-ask84n8y.js} +11 -10
  127. package/dist/{runtime-vfjsfsc5.js → runtime-ckjnj3vf.js} +4 -4
  128. package/dist/{setup-hbyzd65h.js → setup-5ga5gdsy.js} +2 -2
  129. package/dist/{typesafe-system-one-2ef30pec.js → typesafe-system-one-datfr82p.js} +2 -2
  130. package/dist/{virtual-modules-9bv7ry91.js → virtual-modules-bmp3zwjf.js} +12 -11
  131. package/dist/{work-objects-m09sxnhe.js → work-objects-snnh0t7j.js} +34 -31
  132. package/dist/{worker-0h35rvk4.js → worker-rxyymrhb.js} +45 -40
  133. package/openapi.json +997 -39
  134. package/package.json +6 -5
  135. package/src/be/db/active-sessions.ts +30 -0
  136. package/src/be/db/approvals.ts +54 -0
  137. package/src/be/db.ts +25 -4
  138. package/src/be/mcp-server-checked-update.ts +34 -0
  139. package/src/be/mcp-server-stdio-gate.ts +34 -0
  140. package/src/be/memory/key-browser.ts +304 -0
  141. package/src/be/memory/providers/sqlite-store.ts +16 -3
  142. package/src/be/migrations/194_memory_orphan_chunks.sql +15 -0
  143. package/src/be/migrations/195_approval_request_approvals.sql +6 -0
  144. package/src/be/model-validation.ts +12 -2
  145. package/src/be/multi-runtime.ts +4 -3
  146. package/src/be/pricing-normalize.ts +11 -0
  147. package/src/be/repo-checked-update.ts +37 -0
  148. package/src/be/repo-merge-policy.ts +15 -0
  149. package/src/be/seed-pricing.ts +48 -1
  150. package/src/be/swarm-config-guard.ts +1 -0
  151. package/src/commands/provider-credentials.ts +44 -2
  152. package/src/extensions/contract-types.generated.ts +1 -1
  153. package/src/extensions/contract.ts +100 -0
  154. package/src/extensions/dispatcher.ts +19 -0
  155. package/src/extensions/post-bridge.ts +191 -0
  156. package/src/github/handlers.ts +128 -2
  157. package/src/gitlab/workflow-action-name.ts +15 -0
  158. package/src/heartbeat/heartbeat.ts +4 -3
  159. package/src/http/active-sessions.ts +6 -5
  160. package/src/http/agents.ts +10 -1
  161. package/src/http/approval-requests.ts +271 -35
  162. package/src/http/index.ts +1 -1
  163. package/src/http/mcp-servers.ts +74 -50
  164. package/src/http/memory.ts +275 -6
  165. package/src/http/repos.ts +58 -18
  166. package/src/http/request-principal.ts +19 -0
  167. package/src/http/session-cost-recompute.ts +64 -0
  168. package/src/http/session-data.ts +11 -0
  169. package/src/http/tasks.ts +9 -1
  170. package/src/http/webhooks.ts +17 -5
  171. package/src/prompts/base-prompt.ts +9 -4
  172. package/src/prompts/resolver.ts +1 -1
  173. package/src/prompts/session-templates.ts +28 -0
  174. package/src/providers/amp-adapter.ts +1232 -0
  175. package/src/providers/cursor-adapter.ts +638 -0
  176. package/src/providers/index.ts +9 -1
  177. package/src/providers/pricing-sources.md +39 -0
  178. package/src/providers/reasoning-effort.ts +15 -1
  179. package/src/providers/types.ts +11 -1
  180. package/src/rbac/legacy-policy.ts +23 -8
  181. package/src/rbac/permissions.ts +27 -1
  182. package/src/scripts-runtime/types/swarm-extension.d.ts +119 -0
  183. package/src/tests/amp-adapter.test.ts +822 -0
  184. package/src/tests/amp-budget-export-failed.test.ts +133 -0
  185. package/src/tests/amp-pricing-helpers.ts +16 -0
  186. package/src/tests/amp-pricing.test.ts +218 -0
  187. package/src/tests/api-drain-worker-e2e.test.ts +18 -7
  188. package/src/tests/approval-respond-auth.test.ts +371 -0
  189. package/src/tests/base-prompt.test.ts +13 -0
  190. package/src/tests/claude-adapter-binary.test.ts +24 -0
  191. package/src/tests/claude-adapter.test.ts +8 -0
  192. package/src/tests/cursor-adapter.test.ts +690 -0
  193. package/src/tests/db-task-read-contract.test.ts +5 -0
  194. package/src/tests/db-task-write-contract.test.ts +5 -0
  195. package/src/tests/docs-index-footprint.test.js +3 -3
  196. package/src/tests/extensions-post-bridge.test.ts +669 -0
  197. package/src/tests/extensions-run-context.test.ts +61 -0
  198. package/src/tests/fixtures/fake-amp.ts +304 -0
  199. package/src/tests/github-handlers-inline-comments.test.ts +147 -1
  200. package/src/tests/gitlab-workflow-event-names.test.ts +136 -0
  201. package/src/tests/mcp-input-ergonomics.test.ts +15 -10
  202. package/src/tests/mcp-server-stdio-gate.test.ts +438 -0
  203. package/src/tests/memory-http-access.test.ts +221 -0
  204. package/src/tests/memory-keys-endpoint.test.ts +406 -0
  205. package/src/tests/memory-store.test.ts +166 -0
  206. package/src/tests/model-tier-resolution.test.ts +3 -3
  207. package/src/tests/pricing-refresh.test.ts +5 -4
  208. package/src/tests/prompt-template-session.test.ts +5 -3
  209. package/src/tests/provider-process-group.test.ts +41 -0
  210. package/src/tests/provider-registration.test.ts +3 -1
  211. package/src/tests/rbac-charact-misc-tools.test.ts +18 -3
  212. package/src/tests/rbac-engine.test.ts +15 -11
  213. package/src/tests/rbac-wire-e2e.test.ts +4 -9
  214. package/src/tests/repo-allow-merge-gate.test.ts +382 -0
  215. package/src/tests/script-connections.test.ts +4 -0
  216. package/src/tests/send-task-requester-gate.test.ts +260 -0
  217. package/src/tests/session-activity-liveness.test.ts +248 -0
  218. package/src/tests/session-costs-golden.test.ts +28 -0
  219. package/src/tests/webhooks-task-blocked.test.ts +4 -4
  220. package/src/tests/write-gate.ts +68 -0
  221. package/src/tests/x402-client.test.ts +124 -0
  222. package/src/tests/x402-spending-tracker.test.ts +66 -0
  223. package/src/tools/mcp-servers/mcp-server-create.ts +23 -1
  224. package/src/tools/mcp-servers/mcp-server-update.ts +46 -28
  225. package/src/tools/memory-delete.ts +3 -3
  226. package/src/tools/repos/update-repo.ts +26 -5
  227. package/src/tools/send-task.ts +24 -1
  228. package/src/tools/steer-task.ts +1 -1
  229. package/src/types.ts +37 -2
  230. package/src/utils/amp-models.ts +114 -0
  231. package/src/utils/credentials.ts +8 -0
  232. package/src/utils/process-group.ts +79 -0
  233. package/src/workflows/event-bus.ts +28 -0
  234. package/src/x402/client.ts +36 -14
  235. package/src/x402/spending-tracker.ts +64 -3
  236. package/templates/skills/agent-browser/SKILL.md +5 -0
  237. package/templates/skills/agent-browser/content.md +5 -0
  238. package/templates/skills/heartbeat-runbook/content.md +1 -0
  239. package/templates/skills/swarm-extensions/content.md +4 -0
  240. package/dist/cli-y89t4nca.js +0 -67
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Who may answer an approval request, and who the answer is recorded as.
3
+ *
4
+ * Drives the real `handleApprovalRequests` handler behind the same auth
5
+ * resolution `handleCore` performs (`resolveHttpRequestAuth` + `setRequestAuth`),
6
+ * so each case presents a real credential: the shared key, the shared key with
7
+ * an `X-Agent-ID`, an `aswt_` user token, an `aseph_` agent session token, or a
8
+ * page session.
9
+ */
10
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
11
+ import { unlink } from "node:fs/promises";
12
+ import { createServer, type Server } from "node:http";
13
+ import {
14
+ closeDb,
15
+ createAgent,
16
+ createApprovalRequest,
17
+ createPage,
18
+ createUser,
19
+ getApprovalRequestById,
20
+ initDb,
21
+ } from "../be/db";
22
+ import { type IdentityActor, mintSessionToken, mintToken } from "../be/users";
23
+ import { handleApprovalRequests } from "../http/approval-requests";
24
+ import { resolveHttpRequestAuth } from "../http/auth";
25
+ import { signPageSession } from "../utils/page-session";
26
+ import { setRequestAuth } from "../utils/request-auth-context";
27
+ import { listenOnFreePort } from "./test-net";
28
+
29
+ const TEST_DB_PATH = "./test-approval-respond-auth.sqlite";
30
+ const API_KEY = "approval-respond-auth-test-key";
31
+ const ACTOR: IdentityActor = { kind: "operator", id: "approval-respond-auth-test" };
32
+
33
+ let server: Server;
34
+ let baseUrl = "";
35
+ let workerId = "";
36
+ let leadId = "";
37
+ let alice: { id: string; email: string; token: string };
38
+ let bob: { id: string; email: string; token: string };
39
+ let carol: { id: string; token: string };
40
+ let workerSessionToken = "";
41
+ let pageId = "";
42
+
43
+ function createAuthedServer(): Server {
44
+ return createServer(async (req, res) => {
45
+ const auth = await resolveHttpRequestAuth(req, API_KEY);
46
+ if (!auth) {
47
+ res.writeHead(401, { "Content-Type": "application/json" });
48
+ res.end(JSON.stringify({ error: "Unauthorized" }));
49
+ return;
50
+ }
51
+ setRequestAuth(req, auth);
52
+ const url = new URL(req.url ?? "/", "http://localhost");
53
+ const handled = await handleApprovalRequests(
54
+ req,
55
+ res,
56
+ url.pathname.split("/").filter(Boolean),
57
+ url.searchParams,
58
+ );
59
+ if (!handled) {
60
+ res.writeHead(404, { "Content-Type": "application/json" });
61
+ res.end(JSON.stringify({ error: "Not found" }));
62
+ }
63
+ });
64
+ }
65
+
66
+ type Approvers = {
67
+ users?: string[];
68
+ roles?: string[];
69
+ policy: "any" | "all" | { min: number };
70
+ };
71
+
72
+ async function pendingRequest(approvers: Approvers = { policy: "any" }) {
73
+ return createApprovalRequest({
74
+ id: crypto.randomUUID(),
75
+ title: "Ship the release",
76
+ questions: [{ id: "q1", type: "approval", label: "Approve?", required: true }],
77
+ approvers,
78
+ });
79
+ }
80
+
81
+ async function respond(
82
+ id: string,
83
+ headers: Record<string, string>,
84
+ body: Record<string, unknown> = { responses: { q1: { approved: true } } },
85
+ ) {
86
+ const res = await fetch(`${baseUrl}/api/approval-requests/${id}/respond`, {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/json", ...headers },
89
+ body: JSON.stringify(body),
90
+ });
91
+ return { status: res.status, body: (await res.json()) as Record<string, any> };
92
+ }
93
+
94
+ const operator = () => ({ Authorization: `Bearer ${API_KEY}` });
95
+ const asUser = (token: string) => ({ Authorization: `Bearer ${token}` });
96
+
97
+ beforeAll(async () => {
98
+ try {
99
+ await unlink(TEST_DB_PATH);
100
+ } catch {}
101
+ initDb(TEST_DB_PATH);
102
+
103
+ workerId = crypto.randomUUID();
104
+ leadId = crypto.randomUUID();
105
+ await createAgent({ id: workerId, name: "respond-worker", isLead: false, status: "idle" });
106
+ await createAgent({ id: leadId, name: "respond-lead", isLead: true, status: "idle" });
107
+
108
+ const a = await createUser({
109
+ name: "Alice",
110
+ email: "alice@example.com",
111
+ role: "release-manager",
112
+ });
113
+ const b = await createUser({ name: "Bob", email: "bob@example.com" });
114
+ const c = await createUser({ name: "Carol", role: "release-manager" });
115
+ alice = {
116
+ id: a.id,
117
+ email: "alice@example.com",
118
+ token: (await mintToken(a.id, "alice", ACTOR)).plaintext,
119
+ };
120
+ bob = {
121
+ id: b.id,
122
+ email: "bob@example.com",
123
+ token: (await mintToken(b.id, "bob", ACTOR)).plaintext,
124
+ };
125
+ carol = { id: c.id, token: (await mintToken(c.id, "carol", ACTOR)).plaintext };
126
+
127
+ workerSessionToken = (await mintSessionToken(workerId, crypto.randomUUID(), 60_000)).plaintext;
128
+
129
+ const page = await createPage({
130
+ agentId: workerId,
131
+ slug: `respond-page-${crypto.randomUUID().slice(0, 8)}`,
132
+ title: "Respond page",
133
+ contentType: "text/html",
134
+ body: "<p>hi</p>",
135
+ });
136
+ pageId = page.id;
137
+
138
+ server = createAuthedServer();
139
+ const port = await listenOnFreePort(server);
140
+ baseUrl = `http://127.0.0.1:${port}`;
141
+ });
142
+
143
+ afterAll(async () => {
144
+ await new Promise<void>((resolve) => server.close(() => resolve()));
145
+ closeDb();
146
+ for (const suffix of ["", "-wal", "-shm"]) {
147
+ try {
148
+ await unlink(`${TEST_DB_PATH}${suffix}`);
149
+ } catch {}
150
+ }
151
+ });
152
+
153
+ describe("agent principals cannot answer an approval request", () => {
154
+ test("shared key with X-Agent-ID (worker) is refused and the request stays pending", async () => {
155
+ const request = await pendingRequest();
156
+ const res = await respond(
157
+ request.id,
158
+ { ...operator(), "X-Agent-ID": workerId },
159
+ {
160
+ responses: { q1: { approved: true } },
161
+ respondedBy: alice.email,
162
+ },
163
+ );
164
+ expect(res.status).toBe(403);
165
+ const stored = await getApprovalRequestById(request.id);
166
+ expect(stored?.status).toBe("pending");
167
+ expect(stored?.resolvedBy).toBeNull();
168
+ });
169
+
170
+ test("shared key with a lead's X-Agent-ID is refused", async () => {
171
+ const request = await pendingRequest();
172
+ const res = await respond(request.id, { ...operator(), "X-Agent-ID": leadId });
173
+ expect(res.status).toBe(403);
174
+ expect((await getApprovalRequestById(request.id))?.status).toBe("pending");
175
+ });
176
+
177
+ test("an aseph_ agent session token is refused", async () => {
178
+ const request = await pendingRequest();
179
+ const res = await respond(request.id, asUser(workerSessionToken));
180
+ expect(res.status).toBe(403);
181
+ expect((await getApprovalRequestById(request.id))?.status).toBe("pending");
182
+ });
183
+
184
+ test("a page session with no signed-in user is refused", async () => {
185
+ const request = await pendingRequest();
186
+ const session = await signPageSession({ pageId, exp: Math.floor(Date.now() / 1000) + 600 });
187
+ const res = await respond(request.id, {
188
+ ...operator(),
189
+ "X-Page-Id": pageId,
190
+ "X-Page-Session": session,
191
+ });
192
+ expect(res.status).toBe(403);
193
+ expect((await getApprovalRequestById(request.id))?.status).toBe("pending");
194
+ });
195
+ });
196
+
197
+ describe("the responder comes from the credential, not the body", () => {
198
+ test("shared key alone resolves as operator; respondedBy is kept only as a claim", async () => {
199
+ const request = await pendingRequest();
200
+ const res = await respond(request.id, operator(), {
201
+ responses: { q1: { approved: true } },
202
+ respondedBy: "someone-else@example.com",
203
+ });
204
+ expect(res.status).toBe(200);
205
+ expect(res.body.approvalRequest.status).toBe("approved");
206
+ expect(res.body.approvalRequest.resolvedBy).toBe("operator");
207
+ expect(res.body.approvalRequest.approvals).toEqual([
208
+ expect.objectContaining({
209
+ responder: "operator",
210
+ approved: true,
211
+ claimedRespondedBy: "someone-else@example.com",
212
+ }),
213
+ ]);
214
+ expect((await getApprovalRequestById(request.id))?.resolvedBy).toBe("operator");
215
+ });
216
+
217
+ test("a user token resolves as that user's id, whatever the body claims", async () => {
218
+ const request = await pendingRequest();
219
+ const res = await respond(request.id, asUser(bob.token), {
220
+ responses: { q1: { approved: false } },
221
+ respondedBy: alice.email,
222
+ });
223
+ expect(res.status).toBe(200);
224
+ expect(res.body.approvalRequest.status).toBe("rejected");
225
+ expect(res.body.approvalRequest.resolvedBy).toBe(bob.id);
226
+ });
227
+
228
+ test("a page session signed for a user resolves as that user", async () => {
229
+ const request = await pendingRequest();
230
+ const session = await signPageSession({
231
+ pageId,
232
+ exp: Math.floor(Date.now() / 1000) + 600,
233
+ uid: alice.id,
234
+ });
235
+ const res = await respond(request.id, {
236
+ ...operator(),
237
+ "X-Page-Id": pageId,
238
+ "X-Page-Session": session,
239
+ });
240
+ expect(res.status).toBe(200);
241
+ expect(res.body.approvalRequest.resolvedBy).toBe(alice.id);
242
+ });
243
+ });
244
+
245
+ describe("the approvers policy is enforced", () => {
246
+ test("a user outside approvers.users and approvers.roles is refused", async () => {
247
+ const request = await pendingRequest({ users: [alice.id], policy: "any" });
248
+ const res = await respond(request.id, asUser(bob.token));
249
+ expect(res.status).toBe(403);
250
+ expect((await getApprovalRequestById(request.id))?.status).toBe("pending");
251
+ });
252
+
253
+ test("a listed user may answer; a user may also be listed by email", async () => {
254
+ const byId = await pendingRequest({ users: [alice.id], policy: "any" });
255
+ expect((await respond(byId.id, asUser(alice.token))).body.approvalRequest.status).toBe(
256
+ "approved",
257
+ );
258
+ const byEmail = await pendingRequest({ users: [bob.email], policy: "any" });
259
+ expect((await respond(byEmail.id, asUser(bob.token))).body.approvalRequest.status).toBe(
260
+ "approved",
261
+ );
262
+ });
263
+
264
+ test("a user holding a listed role may answer", async () => {
265
+ const request = await pendingRequest({ roles: ["release-manager"], policy: "any" });
266
+ expect((await respond(request.id, asUser(bob.token))).status).toBe(403);
267
+ const res = await respond(request.id, asUser(carol.token));
268
+ expect(res.status).toBe(200);
269
+ expect(res.body.approvalRequest.resolvedBy).toBe(carol.id);
270
+ });
271
+
272
+ test("the operator key may answer a restricted request", async () => {
273
+ const request = await pendingRequest({ users: [alice.id], policy: "any" });
274
+ const res = await respond(request.id, operator());
275
+ expect(res.status).toBe(200);
276
+ expect(res.body.approvalRequest.resolvedBy).toBe("operator");
277
+ });
278
+
279
+ test("min: N distinct approvals are needed; a repeat answer from one responder is refused", async () => {
280
+ const request = await pendingRequest({ policy: { min: 2 } });
281
+
282
+ const first = await respond(request.id, asUser(alice.token));
283
+ expect(first.status).toBe(200);
284
+ expect(first.body.approvalRequest.status).toBe("pending");
285
+ expect(first.body.approvalRequest.resolvedBy).toBeNull();
286
+ expect(first.body.approvalRequest.approvalProgress).toEqual({ approved: 1, required: 2 });
287
+
288
+ const repeat = await respond(request.id, asUser(alice.token));
289
+ expect(repeat.status).toBe(409);
290
+
291
+ const second = await respond(request.id, asUser(bob.token));
292
+ expect(second.status).toBe(200);
293
+ expect(second.body.approvalRequest.status).toBe("approved");
294
+ expect(second.body.approvalRequest.resolvedBy).toBe(bob.id);
295
+ expect(second.body.approvalRequest.approvalProgress).toBeNull();
296
+ expect(
297
+ (second.body.approvalRequest.approvals as { responder: string }[]).map((a) => a.responder),
298
+ ).toEqual([alice.id, bob.id]);
299
+ });
300
+
301
+ test("concurrent answers to a min-2 request both land and resolve it", async () => {
302
+ const request = await pendingRequest({ policy: { min: 2 } });
303
+ const results = await Promise.all([
304
+ respond(request.id, asUser(alice.token)),
305
+ respond(request.id, asUser(bob.token)),
306
+ ]);
307
+ expect(results.map((r) => r.status)).toEqual([200, 200]);
308
+ const stored = await getApprovalRequestById(request.id);
309
+ expect(stored?.status).toBe("approved");
310
+ expect(stored?.approvals?.map((a) => a.responder).sort()).toEqual([alice.id, bob.id].sort());
311
+ });
312
+
313
+ test("concurrent repeat answers from one responder count once", async () => {
314
+ const request = await pendingRequest({ policy: { min: 2 } });
315
+ const results = await Promise.all([
316
+ respond(request.id, asUser(alice.token)),
317
+ respond(request.id, asUser(alice.token)),
318
+ ]);
319
+ expect(results.map((r) => r.status).sort()).toEqual([200, 409]);
320
+ const stored = await getApprovalRequestById(request.id);
321
+ expect(stored?.status).toBe("pending");
322
+ expect(stored?.approvals).toHaveLength(1);
323
+ });
324
+
325
+ test("all: every listed user must approve; the operator does not stand in for one", async () => {
326
+ const request = await pendingRequest({ users: [alice.id, bob.id], policy: "all" });
327
+
328
+ const byOperator = (await respond(request.id, operator())).body.approvalRequest;
329
+ expect(byOperator.status).toBe("pending");
330
+ expect(byOperator.approvalProgress).toEqual({ approved: 0, required: 2 });
331
+ const byAlice = (await respond(request.id, asUser(alice.token))).body.approvalRequest;
332
+ expect(byAlice.status).toBe("pending");
333
+ expect(byAlice.approvalProgress).toEqual({ approved: 1, required: 2 });
334
+ const last = await respond(request.id, asUser(bob.token));
335
+ expect(last.body.approvalRequest.status).toBe("approved");
336
+ });
337
+
338
+ test("the slim list and the detail read carry quorum progress for pending requests", async () => {
339
+ const request = await pendingRequest({ policy: { min: 3 } });
340
+ expect((await respond(request.id, asUser(alice.token))).status).toBe(200);
341
+
342
+ const list = await fetch(`${baseUrl}/api/approval-requests?fields=slim&status=pending`, {
343
+ headers: operator(),
344
+ });
345
+ const { approvalRequests } = (await list.json()) as {
346
+ approvalRequests: { id: string; approvalProgress: unknown; approvals?: unknown }[];
347
+ };
348
+ const row = approvalRequests.find((r) => r.id === request.id);
349
+ expect(row?.approvalProgress).toEqual({ approved: 1, required: 3 });
350
+ expect(row && "approvals" in row).toBe(false);
351
+
352
+ const detail = await fetch(`${baseUrl}/api/approval-requests/${request.id}`, {
353
+ headers: operator(),
354
+ });
355
+ const { approvalRequest } = (await detail.json()) as Record<string, any>;
356
+ expect(approvalRequest.approvalProgress).toEqual({ approved: 1, required: 3 });
357
+ expect(approvalRequest.approvals).toHaveLength(1);
358
+ });
359
+
360
+ test("one rejection from an eligible responder rejects a multi-approver request", async () => {
361
+ const request = await pendingRequest({ users: [alice.id, bob.id], policy: "all" });
362
+ expect((await respond(request.id, asUser(alice.token))).body.approvalRequest.status).toBe(
363
+ "pending",
364
+ );
365
+ const res = await respond(request.id, asUser(bob.token), {
366
+ responses: { q1: { approved: false } },
367
+ });
368
+ expect(res.body.approvalRequest.status).toBe("rejected");
369
+ expect(res.body.approvalRequest.resolvedBy).toBe(bob.id);
370
+ });
371
+ });
@@ -1135,6 +1135,19 @@ describe("getBasePrompt: harness tool discovery", () => {
1135
1135
  expect(result).toContain("Swarm tools are already in your tool list.");
1136
1136
  });
1137
1137
 
1138
+ test("amp gets its own guidance: tool_search plus code_exec, underscored names", async () => {
1139
+ const result = await getBasePrompt({
1140
+ ...minimalArgs,
1141
+ provider: "amp",
1142
+ traits: { ...localTraits, hasToolSearch: true },
1143
+ });
1144
+ expect(result).not.toContain(deferred);
1145
+ expect(result).not.toContain("Swarm tools are already in your tool list.");
1146
+ expect(result).toContain("`tool_search` and `code_exec`");
1147
+ expect(result).toContain("`store-progress` is `store_progress`");
1148
+ expect(result).toContain('import { store_progress } from "agent-swarm"');
1149
+ });
1150
+
1138
1151
  test("omitted tool-search capability defaults to direct tools", async () => {
1139
1152
  const result = await getBasePrompt({ ...minimalArgs, traits: localTraits });
1140
1153
  expect(result).not.toContain(deferred);
@@ -367,6 +367,7 @@ describe("preseedClaudeTrustDialog", () => {
367
367
 
368
368
  describe("CLAUDE_BINARY env override", () => {
369
369
  // Cache the originals and restore after each test so the suite stays clean.
370
+ let originalClaudeTransport: string | undefined;
370
371
  let originalClaudeBinary: string | undefined;
371
372
  let originalUseClaudeBridge: string | undefined;
372
373
  let originalOauthToken: string | undefined;
@@ -378,6 +379,8 @@ describe("CLAUDE_BINARY env override", () => {
378
379
  let spawnedEnvs: Array<Record<string, string> | undefined>;
379
380
 
380
381
  beforeEach(async () => {
382
+ originalClaudeTransport = process.env.CLAUDE_TRANSPORT;
383
+ delete process.env.CLAUDE_TRANSPORT;
381
384
  originalClaudeBinary = process.env.CLAUDE_BINARY;
382
385
  originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
383
386
  originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
@@ -416,6 +419,11 @@ describe("CLAUDE_BINARY env override", () => {
416
419
  } else {
417
420
  process.env.HOME = originalHome;
418
421
  }
422
+ if (originalClaudeTransport === undefined) {
423
+ delete process.env.CLAUDE_TRANSPORT;
424
+ } else {
425
+ process.env.CLAUDE_TRANSPORT = originalClaudeTransport;
426
+ }
419
427
  if (originalClaudeBinary === undefined) {
420
428
  delete process.env.CLAUDE_BINARY;
421
429
  } else {
@@ -677,6 +685,7 @@ describe("CLAUDE_BINARY env override", () => {
677
685
  });
678
686
 
679
687
  describe("Claude Bridge tmux fail-fast gate", () => {
688
+ let originalClaudeTransport: string | undefined;
680
689
  let originalClaudeBinary: string | undefined;
681
690
  let originalUseClaudeBridge: string | undefined;
682
691
  let originalOauthToken: string | undefined;
@@ -686,6 +695,8 @@ describe("Claude Bridge tmux fail-fast gate", () => {
686
695
  let whichSpy: ReturnType<typeof spyOn>;
687
696
 
688
697
  beforeEach(async () => {
698
+ originalClaudeTransport = process.env.CLAUDE_TRANSPORT;
699
+ delete process.env.CLAUDE_TRANSPORT;
689
700
  originalClaudeBinary = process.env.CLAUDE_BINARY;
690
701
  originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
691
702
  originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
@@ -708,6 +719,11 @@ describe("Claude Bridge tmux fail-fast gate", () => {
708
719
  } else {
709
720
  process.env.HOME = originalHome;
710
721
  }
722
+ if (originalClaudeTransport === undefined) {
723
+ delete process.env.CLAUDE_TRANSPORT;
724
+ } else {
725
+ process.env.CLAUDE_TRANSPORT = originalClaudeTransport;
726
+ }
711
727
  if (originalClaudeBinary === undefined) {
712
728
  delete process.env.CLAUDE_BINARY;
713
729
  } else {
@@ -794,6 +810,7 @@ describe("Claude Bridge tmux fail-fast gate", () => {
794
810
  });
795
811
 
796
812
  describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
813
+ let originalClaudeTransport: string | undefined;
797
814
  let originalClaudeBinary: string | undefined;
798
815
  let originalUseClaudeBridge: string | undefined;
799
816
  let originalOauthToken: string | undefined;
@@ -803,6 +820,8 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
803
820
  let whichSpy: ReturnType<typeof spyOn>;
804
821
 
805
822
  beforeEach(async () => {
823
+ originalClaudeTransport = process.env.CLAUDE_TRANSPORT;
824
+ delete process.env.CLAUDE_TRANSPORT;
806
825
  originalClaudeBinary = process.env.CLAUDE_BINARY;
807
826
  originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
808
827
  originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
@@ -828,6 +847,11 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
828
847
  } else {
829
848
  process.env.HOME = originalHome;
830
849
  }
850
+ if (originalClaudeTransport === undefined) {
851
+ delete process.env.CLAUDE_TRANSPORT;
852
+ } else {
853
+ process.env.CLAUDE_TRANSPORT = originalClaudeTransport;
854
+ }
831
855
  if (originalClaudeBinary === undefined) {
832
856
  delete process.env.CLAUDE_BINARY;
833
857
  } else {
@@ -206,6 +206,7 @@ describe("Claude stream-json event parsing", () => {
206
206
  // stdout) rather than asserting on hand-built fixtures, so a message-shape
207
207
  // regression here is caught instead of silently shipping the wrong text.
208
208
  describe("ClaudeSession processStreams — ProviderResult.output capture", () => {
209
+ let originalClaudeTransport: string | undefined;
209
210
  let spawnSpy: ReturnType<typeof spyOn>;
210
211
  const CLEAN_ENV: Record<string, string> = { CLAUDE_CODE_OAUTH_TOKEN: "example-test-oauth-token" };
211
212
 
@@ -244,11 +245,18 @@ describe("ClaudeSession processStreams — ProviderResult.output capture", () =>
244
245
  }
245
246
 
246
247
  beforeEach(() => {
248
+ originalClaudeTransport = process.env.CLAUDE_TRANSPORT;
249
+ delete process.env.CLAUDE_TRANSPORT;
247
250
  spawnSpy = spyOn(Bun, "spawn");
248
251
  });
249
252
 
250
253
  afterEach(() => {
251
254
  spawnSpy.mockRestore();
255
+ if (originalClaudeTransport === undefined) {
256
+ delete process.env.CLAUDE_TRANSPORT;
257
+ } else {
258
+ process.env.CLAUDE_TRANSPORT = originalClaudeTransport;
259
+ }
252
260
  });
253
261
 
254
262
  test("multiple assistant turns: the last non-empty text wins", async () => {