@desplega.ai/agent-swarm 1.156.0 → 1.158.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 (263) hide show
  1. package/dist/{acp-adapter-c1msptrw.js → acp-adapter-xaq4n6pc.js} +1 -1
  2. package/dist/{actions-wd41qtdc.js → actions-p9hz5yat.js} +15 -15
  3. package/dist/{app-tz2qrnah.js → app-g26wdcz6.js} +4 -4
  4. package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
  5. package/dist/{assistant-c3q1zj2t.js → assistant-wydfqkb3.js} +16 -16
  6. package/dist/{boot-reembed-0be1krkm.js → boot-reembed-g5ckg9yb.js} +6 -6
  7. package/dist/{boot-reembed-r00j03f3.js → boot-reembed-se8zmdcp.js} +5 -5
  8. package/dist/{boot-scrub-logs-9j4hd0vw.js → boot-scrub-logs-wfxqan61.js} +3 -3
  9. package/dist/{seed-pricing-ncgxz1r2.js → boot-seeds-s3q01xj3.js} +10 -10
  10. package/dist/{claude-adapter-112w28qc.js → claude-adapter-tv2x4f36.js} +7 -7
  11. package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
  12. package/dist/{claude-sdk-session-k7pb5kfz.js → claude-sdk-session-t442qz6f.js} +8 -8
  13. package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
  14. package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
  15. package/dist/{cli-mx3bzbv8.js → cli-20cj37vt.js} +1968 -44
  16. package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
  17. package/dist/{cli-24w39x6n.js → cli-47m6dkd3.js} +362 -126
  18. package/dist/{cli-90x9ewbz.js → cli-53sz8gkz.js} +1 -1
  19. package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
  20. package/dist/{cli-08n8jxcw.js → cli-65hjmev5.js} +57 -17
  21. package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
  22. package/dist/{cli-7j2098n5.js → cli-7gtvvpbh.js} +4 -4
  23. package/dist/{cli-b05d7szr.js → cli-7s3syk22.js} +18 -11
  24. package/dist/{cli-9w4ex8vh.js → cli-8qmme6vd.js} +2 -2
  25. package/dist/{cli-28wkjpdp.js → cli-987hg0aa.js} +2 -2
  26. package/dist/{cli-7pgcpcrg.js → cli-9httdpyq.js} +1 -1
  27. package/dist/{cli-jk26c1sz.js → cli-aky8tctm.js} +26 -93
  28. package/dist/{cli-865swy5p.js → cli-c07r9a2m.js} +3 -2
  29. package/dist/{cli-nyqza443.js → cli-fgqxwrpv.js} +291 -12
  30. package/dist/{cli-9pyah9c4.js → cli-ftfvggzx.js} +633 -166
  31. package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
  32. package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
  33. package/dist/{cli-y8bfxjtw.js → cli-j55n64b6.js} +23 -22
  34. package/dist/{cli-7vj52450.js → cli-j57qkrpq.js} +86 -28
  35. package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
  36. package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
  37. package/dist/{cli-tp0d7vzb.js → cli-rjehvrts.js} +4 -4
  38. package/dist/{cli-s8hpxeg4.js → cli-rqz8qbw7.js} +245 -75
  39. package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
  40. package/dist/cli-t1ze64h3.js +811 -0
  41. package/dist/{cli-hc2fkhdv.js → cli-ws6rqycm.js} +3 -2
  42. package/dist/{cli-t1cd3mhp.js → cli-wscdvcqz.js} +13 -5
  43. package/dist/{cli-xa4j64zh.js → cli-x24pshtq.js} +5 -5
  44. package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
  45. package/dist/{cli-szcyv1mm.js → cli-xhaeg2nc.js} +1 -1
  46. package/dist/cli.js +30 -16
  47. package/dist/{codex-adapter-g4tj1f2g.js → codex-adapter-cnm8wk11.js} +5 -5
  48. package/dist/{codex-session-runner-ad8ha7zt.js → codex-session-runner-gfg547zj.js} +5 -5
  49. package/dist/{commands-z3gd8ppc.js → commands-e3bn0p75.js} +3 -3
  50. package/dist/{db-bezm97r9.js → db-03v9kzr2.js} +17 -3
  51. package/dist/{dispatcher-a97hc33y.js → dispatcher-nrmg8yht.js} +14 -14
  52. package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
  53. package/dist/{handlers-wtdy1x02.js → handlers-aqcedbyb.js} +17 -17
  54. package/dist/{hook-7edgmzng.js → hook-7vkskkre.js} +19 -11
  55. package/dist/{hook-fvs58a5b.js → hook-ybvmrmhq.js} +4 -4
  56. package/dist/{http-phnf93hp.js → http-2eqa8t24.js} +432 -195
  57. package/dist/{index-7x131mmf.js → index-1yks3qcr.js} +14 -14
  58. package/dist/{index-1fktvwxc.js → index-a3vn96n8.js} +31 -28
  59. package/dist/{index-yp8rasxk.js → index-c6zsv1bd.js} +5 -5
  60. package/dist/{index-50jxgw0d.js → index-chgdkq36.js} +14 -14
  61. package/dist/{index-7jwk7nsm.js → index-x71bvq0p.js} +15 -15
  62. package/dist/{keepalive-nnz07ebd.js → keepalive-vtkf5n99.js} +5 -5
  63. package/dist/{lead-mg7qcgww.js → lead-h3sp357x.js} +26 -26
  64. package/dist/{maintenance-ywmnqrpx.js → maintenance-jzeqtk3q.js} +6 -6
  65. package/dist/{oauth-refresh-sweep-c63sqfs1.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
  66. package/dist/{onboard-2zj8sqkq.js → onboard-tmsp0qbz.js} +278 -81
  67. package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
  68. package/dist/{otel-impl-jgdfm26f.js → otel-impl-9mz73q65.js} +416 -301
  69. package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
  70. package/dist/{pricing-refresh-3whntb69.js → pricing-refresh-v1cbr9te.js} +5 -5
  71. package/dist/{rbac-roles-3zptcc3p.js → rbac-roles-c9wqz83x.js} +4 -4
  72. package/dist/{render-v2-dbvpyysc.js → render-v2-gryrh9h8.js} +4 -4
  73. package/dist/{setup-qwx0j8z9.js → setup-q9720bt6.js} +2 -2
  74. package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
  75. package/dist/{work-objects-sjbcmvep.js → work-objects-b76x7deb.js} +17 -17
  76. package/dist/{worker-xezqdkb7.js → worker-6vrxbv41.js} +26 -26
  77. package/openapi.json +1626 -629
  78. package/package.json +3 -2
  79. package/src/apps/catalog.generated.json +229 -0
  80. package/src/apps/page-validator.ts +71 -16
  81. package/src/apps/row-store.ts +66 -11
  82. package/src/apps/sync.ts +59 -17
  83. package/src/artifact-sdk/browser-sdk.ts +436 -2
  84. package/src/be/approval-sweeps.ts +109 -0
  85. package/src/be/boot-seeds.ts +40 -0
  86. package/src/be/chunking.ts +1 -1
  87. package/src/be/db/agents.ts +20 -0
  88. package/src/be/db/approvals.ts +78 -0
  89. package/src/be/db/tasks/read.ts +197 -39
  90. package/src/be/db/tasks/write.ts +41 -9
  91. package/src/be/db-client.ts +107 -48
  92. package/src/be/db.ts +219 -76
  93. package/src/be/events.ts +33 -1
  94. package/src/be/memory/graph-expansion.ts +10 -3
  95. package/src/be/memory/providers/sqlite-store.ts +55 -13
  96. package/src/be/memory/raters/retrieval.ts +5 -2
  97. package/src/be/memory/reranker.ts +26 -2
  98. package/src/be/memory/types.ts +2 -0
  99. package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
  100. package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
  101. package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
  102. package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
  103. package/src/be/models-catalog.ts +2 -0
  104. package/src/be/modelsdev-cache.json +1857 -0
  105. package/src/be/modelsdev-cache.ts +14 -1
  106. package/src/be/pricing-normalize.ts +2 -0
  107. package/src/be/scripts/typecheck.ts +1 -0
  108. package/src/be/seed-pricing.ts +3 -1
  109. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  110. package/src/be/steering.ts +53 -3
  111. package/src/be/swarm-config-guard.ts +3 -0
  112. package/src/be/task-citations.ts +29 -0
  113. package/src/cli.tsx +14 -1
  114. package/src/commands/onboard/compose-generator.ts +8 -3
  115. package/src/commands/onboard/container-engine.ts +220 -0
  116. package/src/commands/onboard/manifest.ts +1 -0
  117. package/src/commands/onboard/steps/health-check.tsx +4 -1
  118. package/src/commands/onboard/steps/prereq-check.tsx +50 -54
  119. package/src/commands/onboard/steps/start.tsx +31 -16
  120. package/src/commands/onboard/types.ts +7 -2
  121. package/src/commands/onboard.tsx +39 -4
  122. package/src/commands/profile-sync.ts +79 -26
  123. package/src/commands/runner.ts +8 -1
  124. package/src/extensions/catalog.generated.json +17 -0
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/heartbeat/heartbeat.ts +263 -103
  127. package/src/hooks/hook.ts +21 -12
  128. package/src/http/agents.ts +35 -0
  129. package/src/http/approval-requests.ts +136 -64
  130. package/src/http/apps.ts +14 -3
  131. package/src/http/config.ts +15 -1
  132. package/src/http/events.ts +62 -2
  133. package/src/http/index.ts +6 -4
  134. package/src/http/memory.ts +21 -7
  135. package/src/http/schedules.ts +1 -1
  136. package/src/http/session-data.ts +9 -4
  137. package/src/http/sessions.ts +25 -18
  138. package/src/http/stats.ts +19 -7
  139. package/src/http/status.ts +11 -1
  140. package/src/http/tasks.ts +40 -8
  141. package/src/http/webhooks.ts +67 -46
  142. package/src/http/workflows.ts +15 -20
  143. package/src/linear/outbound.ts +37 -15
  144. package/src/otel-impl.ts +151 -1
  145. package/src/otel.ts +14 -3
  146. package/src/prompts/memories.ts +19 -2
  147. package/src/providers/claude-managed-models.ts +8 -0
  148. package/src/providers/codex-agents-md.ts +140 -17
  149. package/src/providers/modelsdev-reasoning.json +551 -0
  150. package/src/rbac/legacy-policy.ts +16 -0
  151. package/src/rbac/permissions.ts +4 -0
  152. package/src/scheduler/deferred-task-waits.ts +28 -0
  153. package/src/scheduler/schedule-task.ts +3 -0
  154. package/src/scheduler/scheduler.ts +14 -0
  155. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  156. package/src/scripts-runtime/swarm-sdk.ts +9 -0
  157. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  158. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  159. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  160. package/src/server.ts +12 -7
  161. package/src/slack/app.ts +35 -4
  162. package/src/slack/connection-state.ts +17 -0
  163. package/src/tasks/context-key.ts +1 -0
  164. package/src/tasks/worker-follow-up.ts +6 -2
  165. package/src/tests/agent-task-activity.test.ts +74 -0
  166. package/src/tests/agents-list-model-display.test.ts +6 -1
  167. package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
  168. package/src/tests/approval-requests.test.ts +395 -0
  169. package/src/tests/approval-sweeps-config.test.ts +47 -0
  170. package/src/tests/approval-sweeps.test.ts +508 -0
  171. package/src/tests/apps-kibo-blocks.test.ts +77 -0
  172. package/src/tests/apps-spike.test.ts +56 -1
  173. package/src/tests/apps-sync-engine.test.ts +20 -9
  174. package/src/tests/boot-seeds.test.ts +170 -0
  175. package/src/tests/cancel-approval-request-tool.test.ts +115 -0
  176. package/src/tests/claude-managed-adapter.test.ts +6 -0
  177. package/src/tests/codex-adapter.test.ts +126 -1
  178. package/src/tests/config-key-filter.test.ts +80 -0
  179. package/src/tests/context-window.test.ts +1 -0
  180. package/src/tests/db-client-busy-retry.test.ts +96 -0
  181. package/src/tests/db-client.test.ts +60 -1
  182. package/src/tests/db-task-read-contract.test.ts +10 -0
  183. package/src/tests/db-task-write-contract.test.ts +9 -0
  184. package/src/tests/deferred-schedule-reaper.test.ts +193 -0
  185. package/src/tests/docs-index-footprint.test.js +3 -3
  186. package/src/tests/events-http.test.ts +103 -0
  187. package/src/tests/extensions-catalog.test.ts +22 -17
  188. package/src/tests/extensions-pre-task.test.ts +107 -0
  189. package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
  190. package/src/tests/follow-up-config-inheritance.test.ts +260 -0
  191. package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
  192. package/src/tests/heartbeat-reboot-live.test.ts +95 -0
  193. package/src/tests/heartbeat-stall-cas.test.ts +80 -0
  194. package/src/tests/heartbeat.test.ts +99 -3
  195. package/src/tests/hook-mcp-config.test.ts +29 -0
  196. package/src/tests/http-api-integration.test.ts +16 -2
  197. package/src/tests/kv-storage.test.ts +62 -0
  198. package/src/tests/linear-outbound-sync.test.ts +120 -5
  199. package/src/tests/list-endpoint-slimming.test.ts +91 -0
  200. package/src/tests/memory-graph-organic-route.test.ts +155 -0
  201. package/src/tests/memory-http-recall-gating.test.ts +14 -4
  202. package/src/tests/memory-hybrid.test.ts +161 -1
  203. package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
  204. package/src/tests/memory.test.ts +22 -0
  205. package/src/tests/onboard-container-engine.test.ts +240 -0
  206. package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
  207. package/src/tests/pricing-routes.test.ts +14 -0
  208. package/src/tests/profile-sync-warning.test.ts +8 -4
  209. package/src/tests/promote-draft-task-route.test.ts +19 -0
  210. package/src/tests/provider-registration.test.ts +296 -42
  211. package/src/tests/rbac-engine.test.ts +30 -0
  212. package/src/tests/reasoning-effort.test.ts +8 -0
  213. package/src/tests/script-connections.test.ts +7 -0
  214. package/src/tests/sdk-allowlist.test.ts +39 -0
  215. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  216. package/src/tests/sessions-detail-batch.test.ts +250 -0
  217. package/src/tests/sessions.test.ts +118 -0
  218. package/src/tests/slack-config.test.ts +22 -0
  219. package/src/tests/sonnet-5-5-registry.test.ts +44 -0
  220. package/src/tests/status.test.ts +36 -1
  221. package/src/tests/swarm-diff.test.ts +316 -10
  222. package/src/tests/task-completion-idempotency.test.ts +2 -1
  223. package/src/tests/tasks-requested-by-filter.test.ts +3 -1
  224. package/src/tests/tool-annotations.test.ts +1 -1
  225. package/src/tests/ui-list-slim-projections.test.ts +158 -0
  226. package/src/tests/webhooks-task-blocked.test.ts +127 -0
  227. package/src/tests/workflow-approval-recovery.test.ts +203 -0
  228. package/src/tests/workflow-hitl-routing.test.ts +22 -1
  229. package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
  230. package/src/tests/workflow-http-v2.test.ts +99 -8
  231. package/src/tests/workflow-retry-validation.test.ts +56 -0
  232. package/src/tests/workflow-tla-races.test.ts +389 -0
  233. package/src/tests/workflow-validation-port-routing.test.ts +113 -1
  234. package/src/tools/cancel-approval-request.ts +57 -0
  235. package/src/tools/memory-search.ts +1 -0
  236. package/src/tools/request-human-input.ts +4 -2
  237. package/src/tools/schedules/list-schedules.ts +4 -2
  238. package/src/tools/send-task.ts +4 -0
  239. package/src/tools/templates.ts +19 -0
  240. package/src/tools/tool-config.ts +2 -1
  241. package/src/tools/workflows/list-workflow-runs.ts +1 -0
  242. package/src/types.ts +38 -0
  243. package/src/utils/context-window.ts +1 -0
  244. package/src/workflows/approval-cancel.ts +152 -0
  245. package/src/workflows/approval-notifications.ts +84 -0
  246. package/src/workflows/checkpoint.ts +10 -5
  247. package/src/workflows/definition.ts +21 -0
  248. package/src/workflows/engine.ts +152 -21
  249. package/src/workflows/executors/human-in-the-loop.ts +1 -1
  250. package/src/workflows/recovery.ts +100 -45
  251. package/src/workflows/resume.ts +49 -18
  252. package/src/workflows/retry-poller.ts +71 -21
  253. package/templates/extensions/README.md +1 -0
  254. package/templates/extensions/github-sender-allowlist/README.md +18 -0
  255. package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
  256. package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
  257. package/templates/skills/apps/content.md +20 -1
  258. package/templates/skills/code-quality/content.md +26 -1
  259. package/templates/skills/pages/content.md +16 -4
  260. package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
  261. package/templates/skills/scheduling/content.md +1 -1
  262. package/dist/cli-2daksqps.js +0 -378
  263. package/dist/rbac-roles-3zw68x72.js +0 -37
@@ -26,7 +26,12 @@ import {
26
26
  withMutationLock,
27
27
  } from "../apps/row-store";
28
28
  import { createApp, getApp, updateApp } from "../apps/store";
29
- import { getAppSyncStatus, runAppSync, type SyncPassResult } from "../apps/sync";
29
+ import {
30
+ getAppSyncStatus,
31
+ RECONCILE_CHUNK_SIZE,
32
+ runAppSync,
33
+ type SyncPassResult,
34
+ } from "../apps/sync";
30
35
  import {
31
36
  closeDb,
32
37
  createAgent,
@@ -1277,17 +1282,20 @@ spawnDescribe("concurrency", () => {
1277
1282
  });
1278
1283
 
1279
1284
  test("a mid-pass write failure reports the committed churn instead of zero counts", async () => {
1280
- // Row writes are independent KV upserts, not a transaction: fail the
1281
- // SECOND create after the first already committed and the pass counts
1282
- // must say so instead of reporting the zero-count base.
1283
- const script = await fixtureScript("partial", [ghRecord(1), ghRecord(2)]);
1285
+ // Rows are written in chunked transactions: fail the second create of the
1286
+ // SECOND chunk after the first chunk committed. The pass counts must say
1287
+ // so instead of reporting the zero-count base, and the failing chunk must
1288
+ // roll back without leaving partial rows.
1289
+ const total = RECONCILE_CHUNK_SIZE + 2;
1290
+ const records = Array.from({ length: total }, (_, index) => ghRecord(index + 1));
1291
+ const script = await fixtureScript("partial", records);
1284
1292
  const appId = await createSyncApp(issueDefinition(script.id));
1285
1293
 
1286
1294
  const realCreate = rowStore.createAppRowUnlocked;
1287
1295
  let creates = 0;
1288
1296
  const spy = spyOn(rowStore, "createAppRowUnlocked").mockImplementation((...callArgs) => {
1289
1297
  creates += 1;
1290
- if (creates === 2) throw new Error("kv write failed (injected)");
1298
+ if (creates === RECONCILE_CHUNK_SIZE + 2) throw new Error("kv write failed (injected)");
1291
1299
  return realCreate(...callArgs);
1292
1300
  });
1293
1301
  try {
@@ -1296,9 +1304,12 @@ spawnDescribe("concurrency", () => {
1296
1304
 
1297
1305
  expect(result.ok).toBe(false);
1298
1306
  expect(pass.error).toContain("kv write failed (injected)");
1299
- expect(pass).toMatchObject({ pulled: 2, created: 1 });
1300
- expect((await rowsOf(appId)).map((row) => row.issueKey)).toEqual(["1"]);
1301
- expect(await getAppSyncStatus(appId, "issue", "gh")).toMatchObject({ ok: false, created: 1 });
1307
+ expect(pass).toMatchObject({ pulled: total, created: RECONCILE_CHUNK_SIZE });
1308
+ expect(await rowsOf(appId)).toHaveLength(RECONCILE_CHUNK_SIZE);
1309
+ expect(await getAppSyncStatus(appId, "issue", "gh")).toMatchObject({
1310
+ ok: false,
1311
+ created: RECONCILE_CHUNK_SIZE,
1312
+ });
1302
1313
  } finally {
1303
1314
  spy.mockRestore();
1304
1315
  }
@@ -0,0 +1,170 @@
1
+ import { afterAll, afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm, unlink, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { ensurePricingSeeded, ensureRbacSeeded } from "../be/boot-seeds";
6
+ import { closeDb, getDbClient, initDb } from "../be/db";
7
+ import { loadModelsDevCache } from "../be/modelsdev-cache";
8
+ import { BUILTIN_ROLES } from "../be/rbac-roles";
9
+ import { createServer } from "../server";
10
+
11
+ const TEST_DB_PATH = "./test-boot-seeds.sqlite";
12
+ const BUILTIN_ROLE_ID = "rbac-role-requester";
13
+
14
+ async function removeDbFiles(): Promise<void> {
15
+ for (const suffix of ["", "-wal", "-shm"]) {
16
+ try {
17
+ await unlink(TEST_DB_PATH + suffix);
18
+ } catch {
19
+ // File does not exist.
20
+ }
21
+ }
22
+ }
23
+
24
+ async function countPricingSeedRows(): Promise<number> {
25
+ const row = await getDbClient().get<{ n: number }>(
26
+ "SELECT COUNT(*) AS n FROM pricing WHERE effective_from = 0",
27
+ );
28
+ return row?.n ?? 0;
29
+ }
30
+
31
+ async function countBuiltinPermissions(): Promise<number> {
32
+ const row = await getDbClient().get<{ n: number }>(
33
+ "SELECT COUNT(*) AS n FROM role_permissions WHERE roleId = ?",
34
+ [BUILTIN_ROLE_ID],
35
+ );
36
+ return row?.n ?? 0;
37
+ }
38
+
39
+ // Fresh DB handle per test: the guards are keyed on the live handle.
40
+ beforeEach(async () => {
41
+ closeDb();
42
+ await removeDbFiles();
43
+ initDb(TEST_DB_PATH);
44
+ });
45
+
46
+ afterEach(() => {
47
+ closeDb();
48
+ });
49
+
50
+ afterAll(async () => {
51
+ await removeDbFiles();
52
+ });
53
+
54
+ describe("ensurePricingSeeded", () => {
55
+ test("seeds once per database handle, and again for a new handle", async () => {
56
+ ensurePricingSeeded();
57
+ const seeded = await countPricingSeedRows();
58
+ expect(seeded).toBeGreaterThan(0);
59
+
60
+ // A second call must not touch the table: wipe it and confirm it stays empty.
61
+ await getDbClient().run("DELETE FROM pricing WHERE effective_from = 0");
62
+ ensurePricingSeeded();
63
+ expect(await countPricingSeedRows()).toBe(0);
64
+
65
+ // Reopening the same file gives a new handle, which is seeded again.
66
+ closeDb();
67
+ initDb(TEST_DB_PATH);
68
+ ensurePricingSeeded();
69
+ expect(await countPricingSeedRows()).toBe(seeded);
70
+ });
71
+ });
72
+
73
+ describe("ensureRbacSeeded", () => {
74
+ test("syncs once per database handle, and again for a new handle", async () => {
75
+ ensureRbacSeeded();
76
+ const seeded = await countBuiltinPermissions();
77
+ expect(seeded).toBeGreaterThan(0);
78
+
79
+ await getDbClient().run("DELETE FROM role_permissions WHERE roleId = ?", [BUILTIN_ROLE_ID]);
80
+ ensureRbacSeeded();
81
+ expect(await countBuiltinPermissions()).toBe(0);
82
+
83
+ closeDb();
84
+ initDb(TEST_DB_PATH);
85
+ ensureRbacSeeded();
86
+ expect(await countBuiltinPermissions()).toBe(seeded);
87
+ });
88
+
89
+ test("a failed sync does not mark the handle done", async () => {
90
+ // Make the built-in role unrecoverable: its name is taken by another role id.
91
+ const builtin = BUILTIN_ROLES.find((role) => role.id === BUILTIN_ROLE_ID);
92
+ if (!builtin) throw new Error("test fixture: built-in role missing");
93
+ const client = getDbClient();
94
+ await client.run("PRAGMA foreign_keys = OFF");
95
+ await client.run("DELETE FROM roles WHERE id = ?", [BUILTIN_ROLE_ID]);
96
+ await client.run("PRAGMA foreign_keys = ON");
97
+ await client.run(
98
+ "INSERT INTO roles (id, name, description, isBuiltin, grantsAll) VALUES ('rbac-role-collision', ?, 'x', 0, 0)",
99
+ [builtin.name],
100
+ );
101
+
102
+ expect(() => ensureRbacSeeded()).toThrow(/already uses that name/);
103
+ // Not cached as done: the next caller retries and sees the same error.
104
+ expect(() => ensureRbacSeeded()).toThrow(/already uses that name/);
105
+
106
+ await client.run("DELETE FROM roles WHERE id = 'rbac-role-collision'");
107
+ ensureRbacSeeded();
108
+ expect(await countBuiltinPermissions()).toBeGreaterThan(0);
109
+ });
110
+ });
111
+
112
+ describe("createServer (one call per MCP session)", () => {
113
+ let savedDatabasePath: string | undefined;
114
+
115
+ beforeEach(() => {
116
+ savedDatabasePath = process.env.DATABASE_PATH;
117
+ process.env.DATABASE_PATH = TEST_DB_PATH;
118
+ });
119
+
120
+ afterEach(() => {
121
+ if (savedDatabasePath === undefined) delete process.env.DATABASE_PATH;
122
+ else process.env.DATABASE_PATH = savedDatabasePath;
123
+ });
124
+
125
+ test("seeds pricing and RBAC on the first session only", async () => {
126
+ await createServer();
127
+ expect(await countPricingSeedRows()).toBeGreaterThan(0);
128
+ expect(await countBuiltinPermissions()).toBeGreaterThan(0);
129
+
130
+ await getDbClient().run("DELETE FROM pricing WHERE effective_from = 0");
131
+ await getDbClient().run("DELETE FROM role_permissions WHERE roleId = ?", [BUILTIN_ROLE_ID]);
132
+
133
+ // A later session re-registers tools but must not re-run the seeds.
134
+ const second = await createServer();
135
+ expect(second).toBeDefined();
136
+ expect(await countPricingSeedRows()).toBe(0);
137
+ expect(await countBuiltinPermissions()).toBe(0);
138
+ });
139
+ });
140
+
141
+ describe("loadModelsDevCache", () => {
142
+ const saved = process.env.MODELSDEV_CACHE_PATH;
143
+ let dir: string | undefined;
144
+
145
+ afterEach(async () => {
146
+ if (saved === undefined) delete process.env.MODELSDEV_CACHE_PATH;
147
+ else process.env.MODELSDEV_CACHE_PATH = saved;
148
+ if (dir) await rm(dir, { recursive: true, force: true });
149
+ dir = undefined;
150
+ });
151
+
152
+ test("returns the same parsed object while it is alive", () => {
153
+ const first = loadModelsDevCache();
154
+ expect(first).not.toBeNull();
155
+ expect(loadModelsDevCache()).toBe(first);
156
+ });
157
+
158
+ test("keys the memo on the resolved path", async () => {
159
+ dir = await mkdtemp(path.join(tmpdir(), "modelsdev-cache-"));
160
+ const alt = path.join(dir, "cache.json");
161
+ await writeFile(alt, JSON.stringify({ anthropic: { id: "anthropic-alt", models: {} } }));
162
+
163
+ const bundled = loadModelsDevCache();
164
+ process.env.MODELSDEV_CACHE_PATH = alt;
165
+ const overridden = loadModelsDevCache();
166
+
167
+ expect(overridden?.anthropic?.id).toBe("anthropic-alt");
168
+ expect(overridden).not.toBe(bundled);
169
+ });
170
+ });
@@ -0,0 +1,115 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import {
5
+ closeDb,
6
+ createAgent,
7
+ createApprovalRequest,
8
+ createTaskExtended,
9
+ getApprovalRequestById,
10
+ getDbClient,
11
+ initDb,
12
+ } from "../be/db";
13
+ import { registerCancelApprovalRequestTool } from "../tools/cancel-approval-request";
14
+
15
+ const TEST_DB_PATH = "./test-cancel-approval-request-tool.sqlite";
16
+
17
+ type ToolResult = {
18
+ isError?: boolean;
19
+ structuredContent: { success: boolean; alreadyCancelled?: boolean; runCancelled?: boolean };
20
+ };
21
+
22
+ let callTool: (args: unknown, agentId: string) => Promise<ToolResult>;
23
+ let leadId = "";
24
+ let ownerId = "";
25
+ let workerId = "";
26
+
27
+ async function makeRequest() {
28
+ const sourceTask = await createTaskExtended("source", { agentId: ownerId, source: "mcp" });
29
+ const id = crypto.randomUUID();
30
+ await createApprovalRequest({
31
+ id,
32
+ title: "Tool cancel",
33
+ questions: [{ id: "q1", type: "approval", label: "Approve?", required: true }],
34
+ approvers: { policy: "any" },
35
+ sourceTaskId: sourceTask.id,
36
+ });
37
+ return id;
38
+ }
39
+
40
+ describe("cancel-approval-request tool", () => {
41
+ beforeAll(async () => {
42
+ initDb(TEST_DB_PATH);
43
+ leadId = (await createAgent({ name: "tool-lead", isLead: true, status: "idle" })).id;
44
+ ownerId = (await createAgent({ name: "tool-owner", isLead: false, status: "idle" })).id;
45
+ workerId = (await createAgent({ name: "tool-worker", isLead: false, status: "idle" })).id;
46
+ const server = new McpServer({ name: "cancel-approval-request-test", version: "1.0.0" });
47
+ registerCancelApprovalRequestTool(server);
48
+ const tool = (
49
+ server as unknown as {
50
+ _registeredTools: Record<
51
+ string,
52
+ { handler: (args: unknown, extra: unknown) => Promise<unknown> }
53
+ >;
54
+ }
55
+ )._registeredTools["cancel-approval-request"];
56
+ if (!tool) throw new Error("cancel-approval-request tool was not registered");
57
+ callTool = async (args, agentId) =>
58
+ (await tool.handler(args, {
59
+ sessionId: "cancel-approval-request-test",
60
+ requestInfo: { headers: { "x-agent-id": agentId } },
61
+ })) as ToolResult;
62
+ });
63
+
64
+ afterAll(async () => {
65
+ closeDb();
66
+ for (const suffix of ["", "-wal", "-shm"]) {
67
+ await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
68
+ }
69
+ });
70
+
71
+ test("a lead cancels a pending request, then a second call is a no-op", async () => {
72
+ const requestId = await makeRequest();
73
+
74
+ const first = await callTool({ requestId, reason: "not needed" }, leadId);
75
+ expect(first.isError).toBeFalsy();
76
+ expect(first.structuredContent.alreadyCancelled).toBe(false);
77
+ const row = await getApprovalRequestById(requestId);
78
+ expect(row!.status).toBe("cancelled");
79
+ expect(row!.resolvedBy).toBe(leadId);
80
+ expect(row!.resolutionReason).toBe("not needed");
81
+
82
+ const second = await callTool({ requestId }, leadId);
83
+ expect(second.isError).toBeFalsy();
84
+ expect(second.structuredContent.alreadyCancelled).toBe(true);
85
+ });
86
+
87
+ test("a worker that does not own the source task gets a tool error", async () => {
88
+ const requestId = await makeRequest();
89
+
90
+ const result = await callTool({ requestId }, workerId);
91
+
92
+ expect(result.isError).toBe(true);
93
+ expect((await getApprovalRequestById(requestId))!.status).toBe("pending");
94
+ });
95
+
96
+ test("the owner agent may cancel", async () => {
97
+ const requestId = await makeRequest();
98
+ const result = await callTool({ requestId }, ownerId);
99
+ expect(result.isError).toBeFalsy();
100
+ expect((await getApprovalRequestById(requestId))!.status).toBe("cancelled");
101
+ });
102
+
103
+ test("an expired pending request gets a tool error and becomes timeout", async () => {
104
+ const requestId = await makeRequest();
105
+ await getDbClient().run("UPDATE approval_requests SET expiresAt = ? WHERE id = ?", [
106
+ new Date(Date.now() - 60_000).toISOString(),
107
+ requestId,
108
+ ]);
109
+
110
+ const result = await callTool({ requestId }, leadId);
111
+
112
+ expect(result.isError).toBe(true);
113
+ expect((await getApprovalRequestById(requestId))!.status).toBe("timeout");
114
+ });
115
+ });
@@ -922,6 +922,12 @@ describe("ClaudeManagedAdapter (Phase 4) — repo provisioning + cost data", ()
922
922
  cacheReadPerMillion: 1.0,
923
923
  cacheWritePerMillion: 12.5,
924
924
  });
925
+ expect(CLAUDE_MANAGED_MODEL_PRICING["claude-sonnet-5-5"]).toEqual({
926
+ inputPerMillion: 2.0,
927
+ outputPerMillion: 10.0,
928
+ cacheReadPerMillion: 0.2,
929
+ cacheWritePerMillion: 2.5,
930
+ });
925
931
  expect(CLAUDE_MANAGED_MODEL_PRICING["claude-sonnet-5"]).toEqual({
926
932
  inputPerMillion: 2.0,
927
933
  outputPerMillion: 10.0,
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
10
- import { mkdirSync, rmSync } from "node:fs";
10
+ import { lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import type {
13
13
  AgentMessageItem,
@@ -1130,6 +1130,8 @@ describe("CodexAdapter.canResume", () => {
1130
1130
 
1131
1131
  describe("writeCodexAgentsMd round-trip", () => {
1132
1132
  const tmpDir = `/tmp/codex-agents-md-test-${Date.now()}`;
1133
+ // Crash-recovery records live outside every per-test repo dir.
1134
+ const stateDir = join(tmpDir, ".state");
1133
1135
 
1134
1136
  beforeAll(() => {
1135
1137
  mkdirSync(tmpDir, { recursive: true });
@@ -1239,6 +1241,129 @@ stale
1239
1241
  expect(after).not.toContain("<swarm_system_prompt>");
1240
1242
  expect(after).toContain("# Project instructions");
1241
1243
  });
1244
+
1245
+ test("cleanup restores a plain AGENTS.md byte-identically", async () => {
1246
+ const dir = join(tmpDir, "byte-identical");
1247
+ mkdirSync(dir, { recursive: true });
1248
+ const original = "# Project instructions\n\nDo things.\n";
1249
+ await Bun.write(join(dir, "AGENTS.md"), original);
1250
+
1251
+ const handle = await writeCodexAgentsMd(dir, "swarm prompt");
1252
+ await handle.cleanup();
1253
+
1254
+ expect(readFileSync(join(dir, "AGENTS.md"), "utf8")).toBe(original);
1255
+ });
1256
+
1257
+ test("never writes through an AGENTS.md -> CLAUDE.md symlink", async () => {
1258
+ const dir = join(tmpDir, "symlink");
1259
+ mkdirSync(dir, { recursive: true });
1260
+ const original = "# Repo rules\n\nDo things.\n";
1261
+ const claudeMd = join(dir, "CLAUDE.md");
1262
+ const agentsMd = join(dir, "AGENTS.md");
1263
+ await Bun.write(claudeMd, original);
1264
+ symlinkSync("CLAUDE.md", agentsMd);
1265
+
1266
+ const handle = await writeCodexAgentsMd(dir, "swarm prompt", { stateDir });
1267
+
1268
+ // During the session: AGENTS.md is a real file, CLAUDE.md is untouched.
1269
+ expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
1270
+ const during = readFileSync(agentsMd, "utf8");
1271
+ expect(during).toContain("swarm prompt");
1272
+ expect(during).toContain("# Repo rules");
1273
+ expect(readFileSync(claudeMd, "utf8")).toBe(original);
1274
+
1275
+ await handle.cleanup();
1276
+
1277
+ expect(lstatSync(agentsMd).isSymbolicLink()).toBe(true);
1278
+ expect(readlinkSync(agentsMd)).toBe("CLAUDE.md");
1279
+ expect(readFileSync(claudeMd, "utf8")).toBe(original);
1280
+ });
1281
+
1282
+ test("carries agent edits to a symlinked AGENTS.md over to the link target", async () => {
1283
+ const dir = join(tmpDir, "symlink-edit");
1284
+ mkdirSync(dir, { recursive: true });
1285
+ const claudeMd = join(dir, "CLAUDE.md");
1286
+ const agentsMd = join(dir, "AGENTS.md");
1287
+ await Bun.write(claudeMd, "# Repo rules\n");
1288
+ symlinkSync("CLAUDE.md", agentsMd);
1289
+
1290
+ const handle = await writeCodexAgentsMd(dir, "swarm prompt", { stateDir });
1291
+ await Bun.write(agentsMd, `${readFileSync(agentsMd, "utf8")}- new rule\n`);
1292
+ await handle.cleanup();
1293
+
1294
+ expect(lstatSync(agentsMd).isSymbolicLink()).toBe(true);
1295
+ expect(readFileSync(claudeMd, "utf8")).toBe("# Repo rules\n- new rule\n");
1296
+ });
1297
+
1298
+ test("restores a symlink left swapped out by a crashed session", async () => {
1299
+ const dir = join(tmpDir, "symlink-crash");
1300
+ mkdirSync(dir, { recursive: true });
1301
+ const original = "# Repo rules\n";
1302
+ const claudeMd = join(dir, "CLAUDE.md");
1303
+ const agentsMd = join(dir, "AGENTS.md");
1304
+ await Bun.write(claudeMd, original);
1305
+ symlinkSync("CLAUDE.md", agentsMd);
1306
+
1307
+ // First session never runs cleanup (crash).
1308
+ await writeCodexAgentsMd(dir, "first prompt", { stateDir });
1309
+ expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
1310
+
1311
+ const handle = await writeCodexAgentsMd(dir, "second prompt", { stateDir });
1312
+ const during = readFileSync(agentsMd, "utf8");
1313
+ expect(during).toContain("second prompt");
1314
+ expect(during).not.toContain("first prompt");
1315
+ expect(readFileSync(claudeMd, "utf8")).toBe(original);
1316
+
1317
+ await handle.cleanup();
1318
+ expect(lstatSync(agentsMd).isSymbolicLink()).toBe(true);
1319
+ expect(readFileSync(claudeMd, "utf8")).toBe(original);
1320
+ });
1321
+
1322
+ test("ignores a forged symlink marker pointing out of the repo", async () => {
1323
+ const dir = join(tmpDir, "forged-marker");
1324
+ mkdirSync(dir, { recursive: true });
1325
+ const secretPath = join(tmpDir, "outside-secret.txt");
1326
+ const secret = "TOP-SECRET-CONTENTS\n";
1327
+ await Bun.write(secretPath, secret);
1328
+ const agentsMd = join(dir, "AGENTS.md");
1329
+
1330
+ for (const forged of ["../outside-secret.txt", secretPath]) {
1331
+ const original = `<swarm_system_prompt symlink="${encodeURIComponent(forged)}">\nold\n</swarm_system_prompt>\n# Rules\n`;
1332
+ await Bun.write(agentsMd, original);
1333
+
1334
+ const handle = await writeCodexAgentsMd(dir, "swarm prompt", { stateDir });
1335
+ expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
1336
+ expect(readFileSync(agentsMd, "utf8")).not.toContain("TOP-SECRET");
1337
+ expect(readFileSync(secretPath, "utf8")).toBe(secret);
1338
+
1339
+ await handle.cleanup();
1340
+ expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
1341
+ expect(readFileSync(agentsMd, "utf8")).toBe(original);
1342
+ expect(readFileSync(secretPath, "utf8")).toBe(secret);
1343
+ }
1344
+ });
1345
+
1346
+ test("never reads or writes an out-of-tree symlink target", async () => {
1347
+ const dir = join(tmpDir, "symlink-out-of-tree");
1348
+ mkdirSync(dir, { recursive: true });
1349
+ const secretPath = join(tmpDir, "outside-target.txt");
1350
+ const secret = "OUT-OF-TREE\n";
1351
+ await Bun.write(secretPath, secret);
1352
+ const agentsMd = join(dir, "AGENTS.md");
1353
+ symlinkSync("../outside-target.txt", agentsMd);
1354
+
1355
+ const handle = await writeCodexAgentsMd(dir, "swarm prompt", { stateDir });
1356
+ expect(lstatSync(agentsMd).isSymbolicLink()).toBe(false);
1357
+ const during = readFileSync(agentsMd, "utf8");
1358
+ expect(during).toContain("swarm prompt");
1359
+ expect(during).not.toContain("OUT-OF-TREE");
1360
+
1361
+ await Bun.write(agentsMd, `${during}- agent edit\n`);
1362
+ await handle.cleanup();
1363
+
1364
+ expect(readlinkSync(agentsMd)).toBe("../outside-target.txt");
1365
+ expect(readFileSync(secretPath, "utf8")).toBe(secret);
1366
+ });
1242
1367
  });
1243
1368
 
1244
1369
  // ─── Phase 3: model catalogue ────────────────────────────────────────────────
@@ -0,0 +1,80 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { createServer, type Server } from "node:http";
3
+ import { closeDb, initDb, upsertSwarmConfig } from "../be/db";
4
+ import { handleConfig } from "../http/config";
5
+ import { handleCore } from "../http/core";
6
+ import { getPathSegments, parseQueryParams } from "../http/utils";
7
+ import { listenOnFreePort } from "./test-net";
8
+
9
+ // ctx.swarm.config_get / config_list send `key` to these routes. Both used to
10
+ // ignore it and return every row, so a script asking for one secret got all.
11
+
12
+ const API_KEY = "example-config-key-filter-test-key";
13
+
14
+ let server: Server;
15
+ let baseUrl: string;
16
+
17
+ beforeAll(async () => {
18
+ initDb(":memory:");
19
+ await upsertSwarmConfig({ scope: "global", key: "WANTED_KEY", value: "wanted" });
20
+ await upsertSwarmConfig({
21
+ scope: "global",
22
+ key: "OTHER_SECRET",
23
+ value: "other-secret-value",
24
+ isSecret: true,
25
+ });
26
+ await upsertSwarmConfig({ scope: "global", key: "PLAIN_KEY", value: "plain" });
27
+
28
+ server = createServer(async (req, res) => {
29
+ if (await handleCore(req, res, req.headers["x-agent-id"] as string | undefined, API_KEY)) {
30
+ return;
31
+ }
32
+ const segments = getPathSegments(req.url || "");
33
+ const query = parseQueryParams(req.url || "");
34
+ if (await handleConfig(req, res, segments, query)) return;
35
+ res.writeHead(404).end();
36
+ });
37
+ baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
38
+ });
39
+
40
+ afterAll(async () => {
41
+ await new Promise<void>((resolve) => server.close(() => resolve()));
42
+ closeDb();
43
+ });
44
+
45
+ async function getKeys(path: string): Promise<string[]> {
46
+ const response = await fetch(`${baseUrl}${path}`, {
47
+ headers: { Authorization: `Bearer ${API_KEY}` },
48
+ });
49
+ expect(response.status).toBe(200);
50
+ const body = (await response.json()) as { configs: Array<{ key: string }> };
51
+ return body.configs.map((config) => config.key);
52
+ }
53
+
54
+ const ALL_KEYS = ["OTHER_SECRET", "PLAIN_KEY", "WANTED_KEY"];
55
+
56
+ describe.each([
57
+ ["/api/config/resolved", "?"],
58
+ ["/api/config", "?scope=global&"],
59
+ ])("GET %s key filter", (path, prefix) => {
60
+ test("?key=<existing> returns exactly that row", async () => {
61
+ expect(await getKeys(`${path}${prefix}key=WANTED_KEY&includeSecrets=true`)).toEqual([
62
+ "WANTED_KEY",
63
+ ]);
64
+ });
65
+
66
+ test("?key=<unknown> returns an empty list", async () => {
67
+ expect(await getKeys(`${path}${prefix}key=NO_SUCH_KEY&includeSecrets=true`)).toEqual([]);
68
+ });
69
+
70
+ test("no key returns every row", async () => {
71
+ expect((await getKeys(`${path}${prefix}includeSecrets=true`)).sort()).toEqual(ALL_KEYS);
72
+ });
73
+
74
+ test("an empty key is rejected instead of returning every row", async () => {
75
+ const response = await fetch(`${baseUrl}${path}${prefix}key=`, {
76
+ headers: { Authorization: `Bearer ${API_KEY}` },
77
+ });
78
+ expect(response.status).toBe(400);
79
+ });
80
+ });
@@ -27,6 +27,7 @@ describe("getContextWindowSize", () => {
27
27
  });
28
28
 
29
29
  test("returns 1M for sonnet models", () => {
30
+ expect(getContextWindowSize("claude-sonnet-5-5")).toBe(1_000_000);
30
31
  expect(getContextWindowSize("claude-sonnet-5")).toBe(1_000_000);
31
32
  expect(getContextWindowSize("claude-sonnet-4-6")).toBe(1_000_000);
32
33
  expect(getContextWindowSize("sonnet")).toBe(1_000_000);