@desplega.ai/agent-swarm 1.152.2 → 1.154.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 (284) hide show
  1. package/README.md +2 -2
  2. package/dist/{acp-adapter-pfeje2v5.js → acp-adapter-eb2aj8xy.js} +4 -4
  3. package/dist/{actions-0sttc7f5.js → actions-kpnwp1kz.js} +31 -27
  4. package/dist/{anthropic-messages-2y8nktar.js → anthropic-messages-9vqsbznt.js} +112 -84
  5. package/dist/{app-p8teva95.js → app-zk1apkf6.js} +10 -7
  6. package/dist/{assistant-r45jnsrt.js → assistant-kwq4y50q.js} +32 -28
  7. package/dist/{azure-openai-responses-f38x2avq.js → azure-openai-responses-04qwdwfq.js} +27 -13
  8. package/dist/{boot-reembed-je26h5y7.js → boot-reembed-22hd3jjn.js} +9 -7
  9. package/dist/{boot-reembed-1qmybsd4.js → boot-reembed-qe168424.js} +10 -8
  10. package/dist/{boot-scrub-logs-gte3h1mm.js → boot-scrub-logs-pkdpsrjd.js} +7 -5
  11. package/dist/{claude-adapter-csc1mzka.js → claude-adapter-3fx90nww.js} +18 -16
  12. package/dist/{claude-managed-adapter-dsmkcrjy.js → claude-managed-adapter-ea2aga6w.js} +3 -3
  13. package/dist/{claude-sdk-session-fcd6f078.js → claude-sdk-session-z7fahf1k.js} +18 -16
  14. package/dist/{cli-r2fpe7cx.js → cli-00w2c66p.js} +61 -55
  15. package/dist/{cli-wbyk465z.js → cli-09y3tjxz.js} +1 -1
  16. package/dist/{cli-ht5rs63n.js → cli-0e31nm4n.js} +16 -7
  17. package/dist/cli-0x5qzvkg.js +4279 -0
  18. package/dist/{cli-d3p82erb.js → cli-18amys3y.js} +9 -9
  19. package/dist/{cli-4ahpr4sw.js → cli-1jpjafw4.js} +5 -5
  20. package/dist/{cli-jbmc0r7r.js → cli-243tzws8.js} +1 -1
  21. package/dist/{cli-ch38q5z4.js → cli-24k806sc.js} +6789 -2542
  22. package/dist/cli-3bkz2p7j.js +2828 -0
  23. package/dist/cli-3er6gcsn.js +1849 -0
  24. package/dist/cli-4k7jazkq.js +3339 -0
  25. package/dist/{cli-c4eeb5q2.js → cli-5b6b62tz.js} +1 -1
  26. package/dist/{cli-qd65dvr6.js → cli-5vs0nh43.js} +65 -44
  27. package/dist/{cli-z3wkng35.js → cli-6r3cgpk2.js} +224 -156
  28. package/dist/{cli-pgj4a5fb.js → cli-71cj99hx.js} +4 -3
  29. package/dist/cli-8sea27q4.js +14075 -0
  30. package/dist/{cli-jvy9qgjs.js → cli-913yakhv.js} +22 -7
  31. package/dist/{cli-pf9q3696.js → cli-927erqqt.js} +5 -2
  32. package/dist/cli-95jnpnqg.js +230 -0
  33. package/dist/{cli-wpv5rgg2.js → cli-abp3g4ax.js} +15 -2
  34. package/dist/{cli-ckz5t2hb.js → cli-acy6bfhr.js} +10 -7
  35. package/dist/{cli-4yc5d8fk.js → cli-avkd6mqv.js} +50 -0
  36. package/dist/{cli-8wrympd7.js → cli-bzr0ftht.js} +26 -7
  37. package/dist/{cli-qn042eh7.js → cli-c1v10xb2.js} +194 -1913
  38. package/dist/cli-cgj4hc12.js +18902 -0
  39. package/dist/{cli-2nnd56zt.js → cli-e0pg4zeh.js} +3 -2
  40. package/dist/{cli-r84ctdxn.js → cli-e31ehqwy.js} +2 -2
  41. package/dist/{cli-gphgnvpf.js → cli-e3229na9.js} +3 -3
  42. package/dist/{cli-gpqcs935.js → cli-e3cyf59d.js} +2 -2
  43. package/dist/{cli-s25avqg9.js → cli-e781c6f9.js} +1 -0
  44. package/dist/{cli-s2qjdd6z.js → cli-eetrvv91.js} +1 -1
  45. package/dist/{cli-j4nv2wqy.js → cli-fdzg6jhf.js} +10 -4
  46. package/dist/{cli-fm4ytsdk.js → cli-g2h29qar.js} +2 -2
  47. package/dist/{cli-zvgj5zg8.js → cli-ge67p080.js} +302 -3497
  48. package/dist/cli-h38x0qhg.js +6912 -0
  49. package/dist/{cli-9xvdm3bx.js → cli-h60qfasa.js} +742 -278
  50. package/dist/{cli-jew3jda5.js → cli-hws0vn86.js} +2 -2
  51. package/dist/{cli-j8dggbcr.js → cli-k0g1sjzm.js} +23 -31
  52. package/dist/{cli-xw7vak96.js → cli-kp3qs53m.js} +5 -5
  53. package/dist/{cli-rgvp6r2x.js → cli-mcnhgnn1.js} +193 -4
  54. package/dist/{cli-zvkq5csn.js → cli-myaxtgh8.js} +7 -6
  55. package/dist/{cli-56h18ng2.js → cli-n62atwh9.js} +1 -1
  56. package/dist/cli-n7h1kqpf.js +29 -0
  57. package/dist/{cli-fmy65jdn.js → cli-pf1v8xhb.js} +7 -5
  58. package/dist/{cli-xy1cchaf.js → cli-q85qhckh.js} +238 -11
  59. package/dist/{cli-3bpzj8me.js → cli-r1d3dzt6.js} +2 -2
  60. package/dist/{pi-mono-adapter-jcqt71cg.js → cli-rxkqvngf.js} +67343 -101083
  61. package/dist/{cli-kj76p1ez.js → cli-scwq51gn.js} +13 -7
  62. package/dist/{cli-b9dd0h3k.js → cli-stwsmd6k.js} +3 -2
  63. package/dist/{cli-c2mk1kgf.js → cli-v4en75c5.js} +8 -1
  64. package/dist/{cli-z61ykvam.js → cli-wg04s9kr.js} +4 -3
  65. package/dist/cli-yn0a417a.js +4069 -0
  66. package/dist/{cli-zfzznn7c.js → cli-ytxs7mn1.js} +26 -2
  67. package/dist/{cli-4j8k1m4w.js → cli-zt1227k1.js} +1162 -192
  68. package/dist/cli.js +28 -35
  69. package/dist/{codex-adapter-fxmpsabw.js → codex-adapter-ttdm995v.js} +12 -11
  70. package/dist/{codex-hook-8jexc34x.js → codex-hook-8gx7bcz0.js} +3 -3
  71. package/dist/{codex-session-runner-rr1e47ea.js → codex-session-runner-3v8nh4c8.js} +12 -11
  72. package/dist/{commands-0f5rxz51.js → commands-1cxwbhhq.js} +7 -5
  73. package/dist/{db-r3jxf3wy.js → db-y9zwsr79.js} +25 -5
  74. package/dist/{dispatcher-06vph6q4.js → dispatcher-9m9zss36.js} +30 -26
  75. package/dist/dsh-adapter-gsb2qqf9.js +19 -0
  76. package/dist/{e2b-413b8t4v.js → e2b-93x1k0j5.js} +6 -7
  77. package/dist/{google-generative-ai-ndjmj5fa.js → google-generative-ai-gwwrnzqn.js} +33 -73
  78. package/dist/{google-vertex-fr665tqs.js → google-vertex-5wfawpwz.js} +38 -68
  79. package/dist/{handlers-0hgtc910.js → handlers-36fm1bwn.js} +33 -29
  80. package/dist/{hook-9kvrv1er.js → hook-pntw4r8h.js} +12 -10
  81. package/dist/{hook-fbfhkat3.js → hook-vwq8y26p.js} +13 -11
  82. package/dist/{http-xbyx8k5j.js → http-20jesakk.js} +357 -261
  83. package/dist/{index-jsw3aynb.js → index-6artrehg.js} +30 -26
  84. package/dist/{cli-pdz9qde4.js → index-9ch1n761.js} +10 -11
  85. package/dist/{index-rnyahfn5.js → index-9n5z2492.js} +30 -26
  86. package/dist/{index-66xz5e9j.js → index-h5gs261z.js} +521 -9
  87. package/dist/{index-tfsdxyv6.js → index-j5wrf1yw.js} +40 -25
  88. package/dist/{index-2sp0384z.js → index-kkeftv5h.js} +738 -21
  89. package/dist/{index-sgk50mbg.js → index-m2xqpz5r.js} +31 -27
  90. package/dist/{index-gpfn1741.js → index-m9ks8a7t.js} +9 -7
  91. package/dist/index-w097eg0x.js +5 -0
  92. package/dist/jiti-loader-b4837mtm.js +34 -0
  93. package/dist/jiti-static-loader-q2g5ds0y.js +31267 -0
  94. package/dist/{keepalive-s3xpr7sw.js → keepalive-pwxvxd2c.js} +9 -7
  95. package/dist/{lead-rq4n5r16.js → lead-m789wqa3.js} +46 -41
  96. package/dist/{maintenance-f0atj1ab.js → maintenance-bvg6w7e2.js} +10 -8
  97. package/dist/{mistral-conversations-qhr84ph3.js → mistral-conversations-zx0kn554.js} +24 -18
  98. package/dist/{node-x1g4v0pq.js → node-5qtycf6a.js} +16 -11
  99. package/dist/{oauth-refresh-sweep-q0t3bxre.js → oauth-refresh-sweep-6v4bd737.js} +8 -6
  100. package/dist/{onboard-br5j1kch.js → onboard-snckpcj1.js} +115 -35
  101. package/dist/{openai-codex-responses-wy6a43wd.js → openai-codex-responses-4vjqzvjp.js} +33 -27
  102. package/dist/{openai-completions-32rdwmze.js → openai-completions-pxsmym7g.js} +52 -64
  103. package/dist/{openai-responses-6xwf7bzg.js → openai-responses-tmn0rqp0.js} +23 -24
  104. package/dist/{opencode-adapter-28axqa09.js → opencode-adapter-qbmv4tpy.js} +6 -6
  105. package/dist/{otel-impl-vdqge2nt.js → otel-impl-e0ytens5.js} +4 -4
  106. package/dist/{pi-messages-b1mbr6gp.js → pi-messages-txpjj39q.js} +3 -3
  107. package/dist/pi-mono-adapter-c4ggemhz.js +1316 -0
  108. package/dist/{pricing-refresh-hb3691fd.js → pricing-refresh-52g2ywrw.js} +9 -7
  109. package/dist/{rbac-roles-t78xt39b.js → rbac-roles-5455kfdn.js} +8 -6
  110. package/dist/{rbac-roles-jh280k9g.js → rbac-roles-hbf0xa52.js} +9 -7
  111. package/dist/{render-v2-7p8r2ymg.js → render-v2-npm8xdzh.js} +16 -9
  112. package/dist/{seed-pricing-hf889enh.js → seed-pricing-650wbr1n.js} +8 -6
  113. package/dist/{setup-w6mb03jv.js → setup-k1mew5rf.js} +4 -5
  114. package/dist/virtual-modules-2q5b2pk4.js +54 -0
  115. package/dist/{work-objects-nt8gy1kj.js → work-objects-2ea3kvwg.js} +33 -29
  116. package/dist/{worker-dwy617t0.js → worker-ec6hvc4m.js} +46 -41
  117. package/openapi.json +109 -16
  118. package/package.json +7 -6
  119. package/plugin/opencode-plugins/lib/memory-raters.ts +7 -0
  120. package/src/be/automation-preflight.ts +6 -4
  121. package/src/be/db/agents.ts +75 -2
  122. package/src/be/db/tasks/read.ts +2 -0
  123. package/src/be/db/tasks/write.ts +7 -1
  124. package/src/be/db.ts +140 -18
  125. package/src/be/extensions/db.ts +35 -4
  126. package/src/be/memory/raters/llm.ts +2 -6
  127. package/src/be/memory/raters/noop.ts +2 -3
  128. package/src/be/memory/raters/registry.ts +4 -10
  129. package/src/be/migrations/157_defer_provenance_and_marker.sql +27 -0
  130. package/src/be/models-catalog.ts +2 -0
  131. package/src/be/modelsdev-cache.json +213 -0
  132. package/src/be/rbac-audit.ts +2 -0
  133. package/src/be/seed/agent-fs-provision.ts +23 -0
  134. package/src/be/seed-pricing.ts +3 -1
  135. package/src/be/swarm-config-guard.ts +1 -0
  136. package/src/commands/onboard/compose-generator.ts +16 -1
  137. package/src/commands/onboard/env-generator.ts +14 -1
  138. package/src/commands/onboard/manifest.ts +1 -0
  139. package/src/commands/onboard/steps/integration-slack.tsx +56 -16
  140. package/src/commands/onboard/steps/review.tsx +5 -0
  141. package/src/commands/onboard/types.ts +6 -0
  142. package/src/commands/onboard.tsx +5 -1
  143. package/src/commands/provider-credentials.ts +14 -2
  144. package/src/commands/runner.ts +18 -2
  145. package/src/extensions/identity.ts +15 -7
  146. package/src/extensions/lifecycle.ts +15 -21
  147. package/src/hooks/codex-hook.ts +7 -1
  148. package/src/http/agents.ts +12 -4
  149. package/src/http/config.ts +1 -1
  150. package/src/http/core.ts +5 -3
  151. package/src/http/extensions.ts +108 -34
  152. package/src/http/poll.ts +7 -0
  153. package/src/http/schedules.ts +10 -0
  154. package/src/http/script-runs.ts +35 -26
  155. package/src/http/status.ts +24 -11
  156. package/src/http/tasks.ts +6 -1
  157. package/src/prompts/base-prompt.ts +15 -7
  158. package/src/prompts/memories.ts +5 -7
  159. package/src/prompts/session-templates.ts +3 -2
  160. package/src/prompts/steering-delivery.ts +2 -0
  161. package/src/providers/claude-managed-models.ts +8 -0
  162. package/src/providers/codex-adapter.ts +3 -6
  163. package/src/providers/codex-models.ts +15 -1
  164. package/src/providers/dsh-adapter.ts +248 -0
  165. package/src/providers/index.ts +5 -1
  166. package/src/providers/modelsdev-reasoning.json +50 -0
  167. package/src/providers/pi-mono-extension.ts +2 -5
  168. package/src/rbac/legacy-policy.ts +15 -1
  169. package/src/rbac/types.ts +2 -0
  170. package/src/server.ts +7 -1
  171. package/src/slack/ack.ts +43 -8
  172. package/src/slack/app.ts +32 -10
  173. package/src/slack/config.ts +29 -0
  174. package/src/slack/handlers.ts +7 -1
  175. package/src/slack/render-v2.ts +186 -17
  176. package/src/slack/responses.ts +1 -3
  177. package/src/slack/task-output.ts +66 -8
  178. package/src/slack/thread-buffer.ts +27 -3
  179. package/src/telemetry.ts +2 -3
  180. package/src/tests/agent-fs-provision-seeder.test.ts +56 -2
  181. package/src/tests/agents-harness-provider.test.ts +19 -0
  182. package/src/tests/agents-list-model-display.test.ts +60 -1
  183. package/src/tests/base-prompt.test.ts +25 -0
  184. package/src/tests/claude-managed-adapter.test.ts +6 -0
  185. package/src/tests/codex-gpt6-release.test.ts +97 -0
  186. package/src/tests/codex-steering-hook.test.ts +2 -2
  187. package/src/tests/config-api-only-keys.test.ts +9 -0
  188. package/src/tests/context-window.test.ts +1 -0
  189. package/src/tests/db-task-read-contract.test.ts +9 -0
  190. package/src/tests/db-task-write-contract.test.ts +9 -0
  191. package/src/tests/defer-task.test.ts +115 -16
  192. package/src/tests/dsh-adapter.test.ts +276 -0
  193. package/src/tests/e2e-nightly-report.test.ts +3 -3
  194. package/src/tests/e2e-redact.test.ts +2 -0
  195. package/src/tests/env-flag.test.ts +6 -0
  196. package/src/tests/extension-agent-visibility.test.ts +474 -0
  197. package/src/tests/extensions-db.test.ts +41 -0
  198. package/src/tests/extensions-http.test.ts +148 -4
  199. package/src/tests/extensions-lifecycle.test.ts +21 -1
  200. package/src/tests/extensions-mcp-tools.test.ts +9 -5
  201. package/src/tests/fetch-resolved-env.test.ts +43 -0
  202. package/src/tests/internal-ai/complete-structured.test.ts +32 -9
  203. package/src/tests/internal-ai/credentials.test.ts +4 -4
  204. package/src/tests/internal-ai/models.test.ts +20 -0
  205. package/src/tests/memory-rate-tool.test.ts +7 -10
  206. package/src/tests/memory-rater-defaults.test.ts +23 -0
  207. package/src/tests/memory-rater-e2e.test.ts +2 -3
  208. package/src/tests/migration-046-budgets.test.ts +2 -0
  209. package/src/tests/model-control.test.ts +2 -2
  210. package/src/tests/onboard-compose.test.ts +14 -0
  211. package/src/tests/onboard-env.test.ts +17 -0
  212. package/src/tests/onboard-manifest.test.ts +12 -0
  213. package/src/tests/opus-5-5-registry.test.ts +48 -0
  214. package/src/tests/pricing-refresh.test.ts +1 -1
  215. package/src/tests/pricing-routes.test.ts +15 -3
  216. package/src/tests/provider-registration.test.ts +5 -2
  217. package/src/tests/rbac-admission-e2e.test.ts +54 -0
  218. package/src/tests/rbac-e2e-helpers.ts +2 -0
  219. package/src/tests/rbac-engine.test.ts +32 -0
  220. package/src/tests/rbac-wire-e2e.test.ts +33 -0
  221. package/src/tests/reasoning-effort.test.ts +8 -0
  222. package/src/tests/reload-config.test.ts +33 -0
  223. package/src/tests/script-connections.test.ts +3 -0
  224. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  225. package/src/tests/slack-config.test.ts +136 -0
  226. package/src/tests/slack-mode-lifecycle.test.ts +69 -0
  227. package/src/tests/slack-render-v2.test.ts +446 -16
  228. package/src/tests/slack-steering.test.ts +69 -1
  229. package/src/tests/slack-task-output.test.ts +33 -12
  230. package/src/tests/status.test.ts +22 -2
  231. package/src/tests/steering-core.test.ts +35 -0
  232. package/src/tests/steering-delivery.test.ts +39 -0
  233. package/src/tests/steering-transport.test.ts +2 -2
  234. package/src/tests/task-tool-preload-mcp.test.ts +3 -1
  235. package/src/tests/telemetry-init.test.ts +18 -0
  236. package/src/tests/workflow-live-recovery.test.ts +126 -0
  237. package/src/tests/workflow-llm.test.ts +3 -3
  238. package/src/tools/defer-task.ts +64 -26
  239. package/src/tools/extension-common.ts +4 -0
  240. package/src/tools/extension-install.ts +1 -1
  241. package/src/tools/extension-list.ts +4 -3
  242. package/src/tools/join-swarm.ts +1 -1
  243. package/src/tools/schedules/create-schedule.ts +11 -1
  244. package/src/tools/schedules/patch-schedule.ts +5 -0
  245. package/src/tools/schedules/update-schedule.ts +5 -0
  246. package/src/tools/send-task.ts +6 -0
  247. package/src/tools/utils.ts +1 -1
  248. package/src/types.ts +30 -8
  249. package/src/utils/context-window.ts +1 -0
  250. package/src/utils/credentials.ts +8 -0
  251. package/src/utils/internal-ai/complete-structured.ts +8 -0
  252. package/src/utils/internal-ai/models.ts +5 -5
  253. package/src/utils/memory-raters.ts +3 -0
  254. package/src/workflows/engine.ts +28 -0
  255. package/src/workflows/recovery.ts +7 -4
  256. package/templates/schedules/daily-status-report/content.md +25 -12
  257. package/templates/schedules/daily-workflow-health-audit/content.md +1 -1
  258. package/templates/skills/swarm-extensions/content.md +9 -5
  259. package/dist/cli-4wppfkcm.js +0 -16384
  260. package/dist/cli-55qrx3q8.js +0 -6041
  261. package/dist/cli-cg4vnyh4.js +0 -305
  262. package/dist/cli-jyetr64j.js +0 -52
  263. package/dist/cli-pp9ax9pa.js +0 -520
  264. package/dist/cli-qa66qaf0.js +0 -6076
  265. package/dist/cli-qghpvca2.js +0 -590
  266. package/dist/cli-rembwgt8.js +0 -861
  267. package/dist/cli-tcvj7j2r.js +0 -1971
  268. package/dist/cli-vfcth11k.js +0 -36
  269. package/dist/cli-w1d1bgkk.js +0 -1422
  270. package/dist/cli-y6x0gqtg.js +0 -5977
  271. package/dist/es5-tj3tn4jj.js +0 -844
  272. package/dist/index-1kq5hw2p.js +0 -467
  273. package/dist/index-1p8wdstt.js +0 -7
  274. package/dist/index-8fdjzb2f.js +0 -100
  275. package/dist/index-bxmwzbgw.js +0 -365
  276. package/dist/index-nzerxd5z.js +0 -7
  277. package/dist/index-pyymnzat.js +0 -1168
  278. package/dist/index-qv6atbab.js +0 -619
  279. package/dist/index-rja32qa7.js +0 -801
  280. package/dist/index-tzg5srfp.js +0 -211
  281. package/dist/index-vhncw983.js +0 -90
  282. package/dist/index-xk036zsk.js +0 -859
  283. package/dist/index-zyazbzcb.js +0 -6
  284. package/dist/{claude-managed-setup-48dwg9bv.js → claude-managed-setup-f5pmsyrp.js} +3 -3
@@ -6,6 +6,7 @@ import {
6
6
  getExtensionByName,
7
7
  insertExtensionRun,
8
8
  listExtensionRuns,
9
+ listExtensionVersions,
9
10
  setExtensionState,
10
11
  } from "../be/extensions/db";
11
12
  import { enqueueAuditRow, flushAuditBuffer } from "../be/rbac-audit";
@@ -259,16 +260,159 @@ describe("/api/extensions HTTP", () => {
259
260
  expect((await response.json()).runs).toEqual([second, first]);
260
261
  });
261
262
 
262
- test("lead agents may install, workers may not, and agent installs stay disabled", async () => {
263
- expect((await install("minimal", workerId)).status).toBe(403);
264
- const response = await install("minimal", leadId);
263
+ test("worker installs are owned, disabled, and cannot activate their own extension", async () => {
264
+ const response = await install("minimal", workerId);
265
265
  expect(response.status).toBe(200);
266
266
  const extension = (await response.json()).extension as {
267
267
  enabled: boolean;
268
268
  createdByAgentId: string | null;
269
269
  };
270
270
  expect(extension.enabled).toBe(false);
271
- expect(extension.createdByAgentId).toBe(leadId);
271
+ expect(extension.createdByAgentId).toBe(workerId);
272
+ const stored = (await getExtensionByName("minimal"))!;
273
+ expect(stored.status).toBe("disabled");
274
+ expect(stored.agentId).toBeNull();
275
+ for (const operation of ["enable", "disable", "activate-version"]) {
276
+ expect(
277
+ (
278
+ await dispatch(`/api/extensions/${stored.id}/${operation}`, {
279
+ method: "POST",
280
+ agentId: workerId,
281
+ body: JSON.stringify({ version: 1 }),
282
+ })
283
+ ).status,
284
+ ).toBe(403);
285
+ }
286
+ expect(await listExtensionRuns(stored.id)).toHaveLength(0);
287
+ expect((await dispatch(`/api/extensions/${stored.id}`)).text).toContain(workerId);
288
+ expect((await dispatch("/api/extensions")).text).toContain(workerId);
289
+ const changed = await loadBundleFixture("minimal");
290
+ changed.files["hooks.ts"] += "\n// worker draft v2\n";
291
+ expect(
292
+ (
293
+ await dispatch("/api/extensions/install", {
294
+ method: "POST",
295
+ agentId: workerId,
296
+ body: JSON.stringify(changed),
297
+ })
298
+ ).status,
299
+ ).toBe(200);
300
+ expect(await getExtensionByName("minimal")).toMatchObject({
301
+ version: 2,
302
+ activeVersion: 1,
303
+ enabled: false,
304
+ status: "disabled",
305
+ createdByAgentId: workerId,
306
+ });
307
+ const versions = await listExtensionVersions(stored.id);
308
+ expect(versions).toHaveLength(2);
309
+ expect(versions.every((version) => version.changedByAgentId === workerId)).toBe(true);
310
+ });
311
+
312
+ test("install never evaluates worker bundle code", async () => {
313
+ const bundle = await loadBundleFixture("minimal");
314
+ bundle.files["hooks.ts"] += '\nthrow new Error("must only run on activation");\n';
315
+ const response = await dispatch("/api/extensions/install", {
316
+ method: "POST",
317
+ agentId: workerId,
318
+ body: JSON.stringify(bundle),
319
+ });
320
+ expect(response.status).toBe(200);
321
+ const stored = (await getExtensionByName("minimal"))!;
322
+ expect(stored).toMatchObject({ enabled: false, status: "disabled", agentId: null });
323
+ expect(await listExtensionRuns(stored.id)).toHaveLength(0);
324
+ const activation = await dispatch(`/api/extensions/${stored.id}/enable`, { method: "POST" });
325
+ expect(activation.status).toBe(500);
326
+ expect(activation.text).toContain("must only run on activation");
327
+ });
328
+
329
+ test("workers cannot overwrite, patch, or delete another owner's extension", async () => {
330
+ await install("minimal", leadId);
331
+ const stored = (await getExtensionByName("minimal"))!;
332
+ expect((await install("minimal", workerId)).status).toBe(403);
333
+ for (const method of ["PATCH", "DELETE"]) {
334
+ expect(
335
+ (
336
+ await dispatch(`/api/extensions/${stored.id}`, {
337
+ method,
338
+ agentId: workerId,
339
+ ...(method === "PATCH" ? { body: JSON.stringify({ priority: 7 }) } : {}),
340
+ })
341
+ ).status,
342
+ ).toBe(403);
343
+ }
344
+ expect(await getExtensionByName("minimal")).toMatchObject({
345
+ createdByAgentId: leadId,
346
+ priority: 100,
347
+ version: 1,
348
+ });
349
+ });
350
+
351
+ test("worker owners edit and delete disabled drafts, but cannot reload live code", async () => {
352
+ await install("minimal", workerId);
353
+ const stored = (await getExtensionByName("minimal"))!;
354
+ const path = `/api/extensions/${stored.id}`;
355
+ expect(
356
+ (
357
+ await dispatch(path, {
358
+ method: "PATCH",
359
+ agentId: workerId,
360
+ body: JSON.stringify({ priority: 7 }),
361
+ })
362
+ ).status,
363
+ ).toBe(200);
364
+ expect((await dispatch(`${path}/enable`, { method: "POST", agentId: leadId })).status).toBe(
365
+ 200,
366
+ );
367
+ expect(
368
+ (
369
+ await dispatch(path, {
370
+ method: "PATCH",
371
+ agentId: workerId,
372
+ body: JSON.stringify({ config: { live: true } }),
373
+ })
374
+ ).status,
375
+ ).toBe(403);
376
+ const changed = await loadBundleFixture("minimal");
377
+ changed.files["hooks.ts"] += '\nthrow new Error("inactive worker version executed");\n';
378
+ expect(
379
+ (
380
+ await dispatch("/api/extensions/install", {
381
+ method: "POST",
382
+ agentId: workerId,
383
+ body: JSON.stringify({ ...changed, config: { live: true } }),
384
+ })
385
+ ).status,
386
+ ).toBe(403);
387
+ expect(
388
+ (
389
+ await dispatch("/api/extensions/install", {
390
+ method: "POST",
391
+ agentId: workerId,
392
+ body: JSON.stringify(changed),
393
+ })
394
+ ).status,
395
+ ).toBe(200);
396
+ // A permitted reload must keep using version 1, never evaluate the worker draft.
397
+ expect(
398
+ (
399
+ await dispatch(path, {
400
+ method: "PATCH",
401
+ agentId: leadId,
402
+ body: JSON.stringify({ priority: 7 }),
403
+ })
404
+ ).status,
405
+ ).toBe(200);
406
+ expect(await getExtensionByName("minimal")).toMatchObject({
407
+ version: 2,
408
+ activeVersion: 1,
409
+ enabled: true,
410
+ configJson: "{}",
411
+ });
412
+ expect((await dispatch(path, { method: "DELETE", agentId: workerId })).status).toBe(409);
413
+ expect((await dispatch(`${path}/disable`, { method: "POST" })).status).toBe(200);
414
+ expect((await dispatch(path, { method: "DELETE", agentId: workerId })).status).toBe(200);
415
+ expect(await getExtensionByName("minimal")).toBeNull();
272
416
  });
273
417
 
274
418
  test("operator reinstall activates a new version when the extension is enabled", async () => {
@@ -65,7 +65,9 @@ describe("extension lifecycle", () => {
65
65
  expect(enabled).toMatchObject({ enabled: true, status: "enabled" });
66
66
  expect(listRegistered().map((loaded) => loaded.record.id)).toEqual([extension.id]);
67
67
 
68
- const agent = (await getAllAgents()).find((candidate) => candidate.name === "ext:minimal");
68
+ const agent = (await getAllAgents({ includeExtensions: true })).find(
69
+ (candidate) => candidate.name === "ext:minimal",
70
+ );
69
71
  expect(agent).toMatchObject({
70
72
  id: enabled.agentId,
71
73
  status: "offline",
@@ -81,6 +83,24 @@ describe("extension lifecycle", () => {
81
83
  expect(listRegistered()).toHaveLength(0);
82
84
  });
83
85
 
86
+ test("activation loads its immutable snapshot, not a concurrently advanced file projection", async () => {
87
+ const extension = await install(await loadBundleFixture("minimal"));
88
+ // Simulate a worker draft replacing the mutable files after activation read its row.
89
+ await getDbClient().run("UPDATE extension_files SET content = ? WHERE extensionId = ?", [
90
+ 'throw new Error("unapproved draft executed");',
91
+ extension.id,
92
+ ]);
93
+ const enabled = await enableExtension(extension.id);
94
+ expect(enabled).toMatchObject({ enabled: true, activeVersion: 1 });
95
+ expect(
96
+ await dispatchPre("pre.task.create", {
97
+ options: {},
98
+ description: "snapshot check",
99
+ origin: "rest",
100
+ }),
101
+ ).toEqual({ action: "continue" });
102
+ });
103
+
84
104
  test("activate-version swaps source and reloads an enabled extension", async () => {
85
105
  const bundle = await loadBundleFixture("priority-a");
86
106
  const extension = await install(bundle);
@@ -200,16 +200,20 @@ describe("extension MCP HTTP proxy tools", () => {
200
200
  expect(result.structuredContent.message).toBeTruthy();
201
201
  });
202
202
 
203
- test("worker installation returns the REST permission error in the shared tool envelope", async () => {
203
+ test("worker installation returns ownership and a disabled draft in the shared tool envelope", async () => {
204
204
  const tools = buildToolServer();
205
205
  const result = (await tools.install.handler(
206
206
  await loadBundleFixture("minimal"),
207
207
  meta(workerId),
208
208
  )) as ToolResult;
209
209
 
210
- expect(result.isError).toBe(true);
211
- expect(result.structuredContent).toMatchObject({ success: false });
212
- expect(String(result.structuredContent.message)).toContain("Forbidden");
210
+ expect(result.isError).toBe(false);
211
+ expect(result.structuredContent).toMatchObject({
212
+ success: true,
213
+ createdByAgentId: workerId,
214
+ enabled: false,
215
+ status: "disabled",
216
+ });
213
217
  });
214
218
 
215
219
  test("lead reinstall stores an inactive version of an enabled extension", async () => {
@@ -254,7 +258,7 @@ describe("extension MCP HTTP proxy tools", () => {
254
258
  const all = (await tools.list.handler({}, meta(leadId))) as ToolResult;
255
259
  expect(all.isError).toBe(false);
256
260
  expect(all.content[0]?.text).toContain(
257
- "| Name | Version | Active | Enabled | Status | Priority | Failures |",
261
+ "| Name | Owner agent | Version | Active | Enabled | Status | Priority | Failures |",
258
262
  );
259
263
  expect(all.structuredContent).toMatchObject({ success: true });
260
264
  expect((all.structuredContent.extensions as unknown[]).length).toBe(1);
@@ -414,3 +414,46 @@ describe("applyResolvedEnvToProcessEnv", () => {
414
414
  expect(process.env.MODEL_OVERRIDE).toBe("new-value");
415
415
  });
416
416
  });
417
+
418
+ describe("memory rater config reload", () => {
419
+ const deploymentValue = process.env.MEMORY_RATERS;
420
+ afterEach(() => {
421
+ if (deploymentValue === undefined) delete process.env.MEMORY_RATERS;
422
+ else process.env.MEMORY_RATERS = deploymentValue;
423
+ });
424
+
425
+ test("empty override survives reload; deleting it restores the deployment value", async () => {
426
+ const agentId = "memory-rater-reload";
427
+ mockResponsesByAgentId.set(agentId, {
428
+ status: 200,
429
+ body: { configs: [{ key: "MEMORY_RATERS", value: "" }] },
430
+ });
431
+ const disabled = await fetchResolvedEnv(testUrl, "key", agentId);
432
+ applyResolvedEnvToProcessEnv(disabled.env);
433
+ expect(process.env.MEMORY_RATERS).toBe("");
434
+ const repeated = await fetchResolvedEnv(testUrl, "key", agentId);
435
+ expect(repeated.env.MEMORY_RATERS).toBe("");
436
+
437
+ mockResponsesByAgentId.set(agentId, { status: 200, body: { configs: [] } });
438
+ const removed = await fetchResolvedEnv(testUrl, "key", agentId);
439
+ applyResolvedEnvToProcessEnv(removed.env);
440
+ expect(process.env.MEMORY_RATERS).toBe(deploymentValue);
441
+ });
442
+
443
+ test.each([undefined, "", "llm"])("missing row restores base value %s", async (value) => {
444
+ const result = await fetchResolvedEnv(testUrl, "key", "memory-rater-base", {
445
+ MEMORY_RATERS: value,
446
+ });
447
+ process.env.MEMORY_RATERS = "explicit-self,llm";
448
+ applyResolvedEnvToProcessEnv(result.env);
449
+ expect(process.env.MEMORY_RATERS).toBe(value);
450
+ });
451
+
452
+ test("failed reload retains the last applied empty override", async () => {
453
+ process.env.MEMORY_RATERS = "";
454
+ mockResponsesByAgentId.set("memory-rater-failure", { status: 500, body: {} });
455
+ const result = await fetchResolvedEnv(testUrl, "key", "memory-rater-failure");
456
+ applyResolvedEnvToProcessEnv(result.env);
457
+ expect(process.env.MEMORY_RATERS).toBe("");
458
+ });
459
+ });
@@ -51,7 +51,7 @@ describe("completeStructured", () => {
51
51
  _credentialOverride: {
52
52
  kind: "openrouter",
53
53
  apiKey: "test",
54
- modelDefault: "openrouter/google/gemini-3-flash-preview",
54
+ modelDefault: "openrouter/deepseek/deepseek-v4.1-flash",
55
55
  },
56
56
  _complete: async () => {
57
57
  invocations++;
@@ -98,7 +98,7 @@ describe("completeStructured", () => {
98
98
  _credentialOverride: {
99
99
  kind: "openrouter",
100
100
  apiKey: "test",
101
- modelDefault: "openrouter/google/gemini-3-flash-preview",
101
+ modelDefault: "openrouter/deepseek/deepseek-v4.1-flash",
102
102
  },
103
103
  _complete: async () => {
104
104
  invocations++;
@@ -111,18 +111,41 @@ describe("completeStructured", () => {
111
111
  });
112
112
  }
113
113
 
114
+ test("unknown catalog model returns null without calling complete", async () => {
115
+ let invocations = 0;
116
+ const result = await completeStructured({
117
+ zodSchema: ResultZodSchema,
118
+ toolSchema: ResultToolSchema,
119
+ toolName: "record_result",
120
+ toolDescription: "Record the result.",
121
+ systemPrompt: "sys",
122
+ userPrompt: "user",
123
+ _credentialOverride: {
124
+ kind: "openai-codex",
125
+ apiKey: "test",
126
+ modelDefault: "openai-codex/nonexistent-model",
127
+ },
128
+ _complete: async () => {
129
+ invocations++;
130
+ return makeMsg([]);
131
+ },
132
+ });
133
+ expect(result).toBeNull();
134
+ expect(invocations).toBe(0);
135
+ });
136
+
114
137
  test("passes a provider-compatible forced tool choice", async () => {
115
138
  const credentials: ResolvedCredential[] = [
116
139
  {
117
140
  kind: "openrouter",
118
141
  apiKey: "test",
119
- modelDefault: "openrouter/google/gemini-3-flash-preview",
142
+ modelDefault: "openrouter/deepseek/deepseek-v4.1-flash",
120
143
  },
121
- { kind: "openai", apiKey: "test", modelDefault: "openai/gpt-5.4-mini" },
144
+ { kind: "openai", apiKey: "test", modelDefault: "openai/gpt-6-luna" },
122
145
  {
123
146
  kind: "openai-codex",
124
147
  apiKey: "test",
125
- modelDefault: "openai-codex/gpt-5.4-mini",
148
+ modelDefault: "openai-codex/gpt-6-luna",
126
149
  },
127
150
  {
128
151
  kind: "anthropic",
@@ -172,7 +195,7 @@ describe("completeStructured", () => {
172
195
  _credentialOverride: {
173
196
  kind: "openrouter",
174
197
  apiKey: "test",
175
- modelDefault: "openrouter/google/gemini-3-flash-preview",
198
+ modelDefault: "openrouter/deepseek/deepseek-v4.1-flash",
176
199
  },
177
200
  _complete: async () => {
178
201
  invocations++;
@@ -210,7 +233,7 @@ describe("completeStructured", () => {
210
233
  _credentialOverride: {
211
234
  kind: "openrouter",
212
235
  apiKey: "test",
213
- modelDefault: "openrouter/google/gemini-3-flash-preview",
236
+ modelDefault: "openrouter/deepseek/deepseek-v4.1-flash",
214
237
  },
215
238
  _complete: async () => {
216
239
  invocations++;
@@ -241,7 +264,7 @@ describe("completeStructured", () => {
241
264
  _credentialOverride: {
242
265
  kind: "openrouter",
243
266
  apiKey: "test",
244
- modelDefault: "openrouter/google/gemini-3-flash-preview",
267
+ modelDefault: "openrouter/deepseek/deepseek-v4.1-flash",
245
268
  },
246
269
  _complete: async () => {
247
270
  invocations++;
@@ -418,7 +441,7 @@ describe("completeStructured", () => {
418
441
  _credentialOverride: {
419
442
  kind: "openrouter",
420
443
  apiKey: "test",
421
- modelDefault: "openrouter/google/gemini-3-flash-preview",
444
+ modelDefault: "openrouter/deepseek/deepseek-v4.1-flash",
422
445
  },
423
446
  _complete: async () =>
424
447
  makeMsg([
@@ -30,7 +30,7 @@ describe("resolveCredential", () => {
30
30
  expect(cred?.kind).toBe("openrouter");
31
31
  if (cred?.kind === "openrouter") {
32
32
  expect(cred.apiKey).toBe("or-1");
33
- expect(cred.modelDefault).toBe("openrouter/google/gemini-3-flash-preview");
33
+ expect(cred.modelDefault).toBe("openrouter/deepseek/deepseek-v4.1-flash");
34
34
  }
35
35
  });
36
36
 
@@ -50,7 +50,7 @@ describe("resolveCredential", () => {
50
50
  expect(cred?.kind).toBe("openai");
51
51
  if (cred?.kind === "openai") {
52
52
  expect(cred.apiKey).toBe("sk-o-1");
53
- expect(cred.modelDefault).toBe("openai/gpt-5.4-mini");
53
+ expect(cred.modelDefault).toBe("openai/gpt-6-luna");
54
54
  }
55
55
  });
56
56
 
@@ -79,7 +79,7 @@ describe("resolveCredential", () => {
79
79
  expect(cred?.kind).toBe("openai-codex");
80
80
  if (cred?.kind === "openai-codex") {
81
81
  expect(cred.apiKey).toBe("example-codex-api-key-derived");
82
- expect(cred.modelDefault).toBe("openai-codex/gpt-5.4-mini");
82
+ expect(cred.modelDefault).toBe("openai-codex/gpt-6-luna");
83
83
  }
84
84
  });
85
85
 
@@ -101,7 +101,7 @@ describe("resolveCredential", () => {
101
101
  expect(cred).toEqual({
102
102
  kind: "openai-codex",
103
103
  apiKey: "example-at_provider_runtime",
104
- modelDefault: "openai-codex/gpt-5.4-mini",
104
+ modelDefault: "openai-codex/gpt-6-luna",
105
105
  });
106
106
  });
107
107
 
@@ -0,0 +1,20 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { getBuiltinModel } from "@earendil-works/pi-ai/providers/all";
3
+ import { DEFAULT_MODEL, parseModelStr } from "../../utils/internal-ai/models.js";
4
+
5
+ describe("internal-ai default models", () => {
6
+ for (const [kind, modelString] of Object.entries(DEFAULT_MODEL)) {
7
+ if (kind === "claude-cli") continue;
8
+
9
+ test(`${kind} resolves in the pinned pi-ai catalog`, () => {
10
+ const [provider, modelId] = parseModelStr(modelString);
11
+ const model = getBuiltinModel(
12
+ provider as Parameters<typeof getBuiltinModel>[0],
13
+ modelId as never,
14
+ );
15
+ expect(model).toBeDefined();
16
+ expect(model.id).toBe(modelId);
17
+ expect(model.provider).toBe(provider);
18
+ });
19
+ }
20
+ });
@@ -212,16 +212,13 @@ describe("renderMemoriesPrompt — conditional rate-tool hint", () => {
212
212
  expect(result).toBeNull();
213
213
  });
214
214
 
215
- test("MEMORY_RATERS unset → no rate-tool hint (byte-identical to pre-step-5)", () => {
216
- const result = renderMemoriesPrompt(sampleMemories);
217
- expect(result).not.toBeNull();
218
- expect(result).not.toContain("memory_rate");
219
- expect(result).toContain("### Relevant Past Knowledge");
220
- expect(result).toContain("- **Foo bug fix** (id: m-1):");
221
- // Snapshot — exact byte parity with main's runner.ts:1579 block.
222
- expect(result).toBe(
223
- `\n\n### Relevant Past Knowledge\n\nThese memories from your previous sessions may be useful. Use \`memory-get\` with the memory ID to retrieve full details.\n\n- **Foo bug fix** (id: m-1): use Bun.serve not express\n`,
224
- );
215
+ test("MEMORY_RATERS unset → self-rating hint", () => {
216
+ expect(renderMemoriesPrompt(sampleMemories)).toContain("memory_rate");
217
+ });
218
+
219
+ test("MEMORY_RATERS=llm → no self-rating hint", () => {
220
+ process.env.MEMORY_RATERS = "llm";
221
+ expect(renderMemoriesPrompt(sampleMemories)).not.toContain("memory_rate");
225
222
  });
226
223
 
227
224
  test("MEMORY_RATERS empty string → no hint", () => {
@@ -0,0 +1,23 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { getRegisteredRaters } from "../be/memory/raters/registry";
3
+
4
+ const original = process.env.MEMORY_RATERS;
5
+ afterEach(() => {
6
+ if (original === undefined) delete process.env.MEMORY_RATERS;
7
+ else process.env.MEMORY_RATERS = original;
8
+ });
9
+
10
+ describe("memory rater defaults", () => {
11
+ test.each([
12
+ [undefined, ["implicit-citation", "explicit-self"]],
13
+ ["", ["noop"]],
14
+ [" ", ["noop"]],
15
+ ["llm", ["llm"]],
16
+ ["explicit-self,llm", ["explicit-self", "llm"]],
17
+ [" implicit-citation , llm ", ["implicit-citation", "llm"]],
18
+ ] as const)("registry resolves %s", (value, expected) => {
19
+ if (value === undefined) delete process.env.MEMORY_RATERS;
20
+ else process.env.MEMORY_RATERS = value;
21
+ expect(getRegisteredRaters().map((rater) => rater.name)).toEqual([...expected]);
22
+ });
23
+ });
@@ -292,9 +292,8 @@ describe("memory-rater v1.5 — cross-cutting e2e", () => {
292
292
 
293
293
  // Fire the server-rater orchestration the way `store-progress` does.
294
294
  // Inject the rater explicitly — the test process inherits its own
295
- // MEMORY_RATERS env (typically unset to avoid disturbing other suites),
296
- // and we want to exercise this rater regardless. Step G covers the
297
- // unset-env "byte-identical" backward-compat case separately.
295
+ // MEMORY_RATERS env (unset defaults to implicit-citation,explicit-self),
296
+ // and we want to exercise implicit-citation regardless.
298
297
  const evidenceRows = await getDbClient().query<{ content: string }>(
299
298
  "SELECT content FROM session_logs WHERE taskId = ? ORDER BY iteration, lineNumber",
300
299
  [taskId],
@@ -21,6 +21,8 @@ async function removeDbFiles(path: string): Promise<void> {
21
21
 
22
22
  beforeAll(() => {
23
23
  initDb(TEST_DB_PATH);
24
+ // Mirror API boot: new releases are snapshot-seeded, not new migrations.
25
+ seedPricingFromModelsDev({ quiet: true });
24
26
  });
25
27
 
26
28
  afterAll(async () => {
@@ -505,10 +505,10 @@ describe("Model Control - Priority Resolution Logic", () => {
505
505
  expect(resolveModelTier({ tier: "smart", harnessProvider: "codex" })).toBe("gpt-5.6-sol");
506
506
  expect(resolveModelTier({ tier: "ultra", harnessProvider: "codex" })).toBe("gpt-5.6-sol");
507
507
  expect(resolveModelTier({ tier: "smart", harnessProvider: "opencode" })).toBe(
508
- "openrouter/deepseek/deepseek-v4-pro",
508
+ "openrouter/deepseek/deepseek-v4-pro-0813",
509
509
  );
510
510
  expect(resolveModelTier({ tier: "ultra", harnessProvider: "pi" })).toBe(
511
- "openrouter/anthropic/claude-opus-4.8",
511
+ "openrouter/anthropic/claude-opus-5.5",
512
512
  );
513
513
  });
514
514
 
@@ -8,6 +8,11 @@ function makeState(overrides: Partial<OnboardState>): OnboardState {
8
8
  return { ...INITIAL_STATE, ...overrides };
9
9
  }
10
10
 
11
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Docker Compose env var syntax
12
+ const COMPOSE_SLACK_MODE = "SLACK_MODE=${SLACK_MODE:-socket}";
13
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Docker Compose env var syntax
14
+ const COMPOSE_SLACK_SIGNING_SECRET = "SLACK_SIGNING_SECRET=${SLACK_SIGNING_SECRET}";
15
+
11
16
  describe("generateCompose", () => {
12
17
  test("uses the full preset by default and includes every official template once", () => {
13
18
  const full = getPresetById("full");
@@ -286,6 +291,8 @@ describe("generateCompose", () => {
286
291
  // Slack vars on API service
287
292
  expect(yaml).toContain("SLACK_BOT_TOKEN");
288
293
  expect(yaml).toContain("SLACK_APP_TOKEN");
294
+ expect(yaml).toContain(COMPOSE_SLACK_MODE);
295
+ expect(yaml).not.toContain("SLACK_SIGNING_SECRET");
289
296
  // GitLab vars on agent services
290
297
  expect(yaml).toContain("GITLAB_TOKEN");
291
298
  expect(yaml).toContain("GITLAB_EMAIL");
@@ -297,6 +304,13 @@ describe("generateCompose", () => {
297
304
  expect(yaml).toContain("SLACK_DISABLE=false");
298
305
  });
299
306
 
307
+ test("HTTP Slack compose configuration uses the signing secret and no app token", () => {
308
+ const yaml = generateCompose({ ...allIntegrationsState, slackMode: "http" });
309
+ expect(yaml).toContain(COMPOSE_SLACK_MODE);
310
+ expect(yaml).toContain(COMPOSE_SLACK_SIGNING_SECRET);
311
+ expect(yaml).not.toContain("SLACK_APP_TOKEN");
312
+ });
313
+
300
314
  // ── No integrations ──
301
315
 
302
316
  test("no integrations omits integration env vars from agent services", () => {
@@ -99,9 +99,26 @@ describe("generateEnv", () => {
99
99
  const env = generateEnv(state);
100
100
  expect(env).toContain("SLACK_BOT_TOKEN=example-xoxb-slack-bot");
101
101
  expect(env).toContain("SLACK_APP_TOKEN=example-xapp-slack-app");
102
+ expect(env).toContain("SLACK_MODE=socket");
103
+ expect(env).not.toContain("SLACK_SIGNING_SECRET=");
102
104
  expect(env).toContain("SLACK_DISABLE=false");
103
105
  });
104
106
 
107
+ test("Slack HTTP mode emits only its mode-specific credential", () => {
108
+ const env = generateEnv(
109
+ makeState({
110
+ integrations: { github: false, slack: true, gitlab: false, sentry: false },
111
+ slackMode: "http",
112
+ slackBotToken: "example-xoxb-slack-bot",
113
+ slackSigningSecret: "synthetic-signing-secret",
114
+ }),
115
+ );
116
+ expect(env).toContain("SLACK_MODE=http");
117
+ expect(env).toContain("SLACK_BOT_TOKEN=example-xoxb-slack-bot");
118
+ expect(env).toContain("SLACK_SIGNING_SECRET=synthetic-signing-secret");
119
+ expect(env).not.toContain("SLACK_APP_TOKEN=");
120
+ });
121
+
105
122
  // ── No integrations: only core section ──
106
123
 
107
124
  test("no integrations outputs disable flags and no integration credentials", () => {
@@ -36,6 +36,7 @@ describe("generateManifest", () => {
36
36
  expect(manifest).toHaveProperty("harness");
37
37
  expect(manifest).toHaveProperty("services");
38
38
  expect(manifest).toHaveProperty("integrations");
39
+ expect(manifest).toHaveProperty("slackMode");
39
40
  expect(manifest).toHaveProperty("composePath");
40
41
  expect(manifest).toHaveProperty("envPath");
41
42
  expect(manifest).toHaveProperty("apiUrl");
@@ -112,6 +113,17 @@ describe("generateManifest", () => {
112
113
  expect(integrations.slack).toBe(false);
113
114
  expect(integrations.gitlab).toBe(false);
114
115
  expect(integrations.sentry).toBe(true);
116
+ expect(manifest.slackMode).toBe("socket");
117
+ });
118
+
119
+ test("records HTTP transport without persisting its signing secret", () => {
120
+ const manifest = generateManifest({
121
+ ...devState,
122
+ slackMode: "http",
123
+ slackSigningSecret: "synthetic-signing-secret",
124
+ }) as Record<string, unknown>;
125
+ expect(manifest.slackMode).toBe("http");
126
+ expect(JSON.stringify(manifest)).not.toContain("synthetic-signing-secret");
115
127
  });
116
128
 
117
129
  test("all integrations false when none enabled", () => {