@desplega.ai/agent-swarm 1.152.2 → 1.153.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 (224) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-pfeje2v5.js → acp-adapter-ahmjxhek.js} +4 -4
  3. package/dist/{actions-0sttc7f5.js → actions-jzrfpkmn.js} +30 -26
  4. package/dist/{anthropic-messages-2y8nktar.js → anthropic-messages-9vqsbznt.js} +112 -84
  5. package/dist/{app-p8teva95.js → app-03na2vwb.js} +9 -6
  6. package/dist/{assistant-r45jnsrt.js → assistant-hfkxvhgt.js} +31 -27
  7. package/dist/{azure-openai-responses-f38x2avq.js → azure-openai-responses-04qwdwfq.js} +27 -13
  8. package/dist/{boot-reembed-je26h5y7.js → boot-reembed-c98xrad3.js} +8 -6
  9. package/dist/{boot-reembed-1qmybsd4.js → boot-reembed-zt3qm9xn.js} +9 -7
  10. package/dist/{boot-scrub-logs-gte3h1mm.js → boot-scrub-logs-jcqd8ksj.js} +6 -4
  11. package/dist/{claude-adapter-csc1mzka.js → claude-adapter-mgryg8g3.js} +17 -15
  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-jjf4zss1.js} +17 -15
  14. package/dist/{cli-r2fpe7cx.js → cli-00w2c66p.js} +61 -55
  15. package/dist/{cli-fm4ytsdk.js → cli-0d9nbqxf.js} +1 -1
  16. package/dist/cli-0x5qzvkg.js +4279 -0
  17. package/dist/{cli-d3p82erb.js → cli-18amys3y.js} +9 -9
  18. package/dist/{cli-wbyk465z.js → cli-1hwnb4hc.js} +1 -1
  19. package/dist/{cli-xw7vak96.js → cli-3bcmp015.js} +5 -5
  20. package/dist/cli-3er6gcsn.js +1849 -0
  21. package/dist/{cli-c4eeb5q2.js → cli-5b6b62tz.js} +1 -1
  22. package/dist/{cli-4ahpr4sw.js → cli-5mbmvxnn.js} +5 -5
  23. package/dist/{cli-xy1cchaf.js → cli-5taqtpze.js} +235 -8
  24. package/dist/{cli-fmy65jdn.js → cli-6ks9xf1n.js} +7 -5
  25. package/dist/cli-7g9s78jf.js +2827 -0
  26. package/dist/{cli-jew3jda5.js → cli-7pnz2v6s.js} +2 -2
  27. package/dist/{cli-r84ctdxn.js → cli-83wkg402.js} +2 -2
  28. package/dist/{cli-jvy9qgjs.js → cli-a9scg8sv.js} +4 -4
  29. package/dist/{cli-4yc5d8fk.js → cli-avkd6mqv.js} +50 -0
  30. package/dist/{cli-ht5rs63n.js → cli-b2g6mzjc.js} +15 -6
  31. package/dist/{cli-jbmc0r7r.js → cli-bw0dfwkr.js} +1 -1
  32. package/dist/{cli-8wrympd7.js → cli-bzr0ftht.js} +26 -7
  33. package/dist/{cli-wpv5rgg2.js → cli-cdq08v3r.js} +2 -2
  34. package/dist/cli-cgj4hc12.js +18902 -0
  35. package/dist/cli-d6hk10xx.js +14075 -0
  36. package/dist/{cli-zvgj5zg8.js → cli-djvxhnc4.js} +31 -3324
  37. package/dist/{cli-s25avqg9.js → cli-e781c6f9.js} +1 -0
  38. package/dist/{cli-3bpzj8me.js → cli-eazstkad.js} +2 -2
  39. package/dist/{cli-s2qjdd6z.js → cli-eetrvv91.js} +1 -1
  40. package/dist/cli-h38x0qhg.js +6912 -0
  41. package/dist/{cli-9xvdm3bx.js → cli-h60qfasa.js} +742 -278
  42. package/dist/{pi-mono-adapter-jcqt71cg.js → cli-ja7mrygn.js} +67343 -101083
  43. package/dist/{cli-j8dggbcr.js → cli-k0g1sjzm.js} +23 -31
  44. package/dist/{cli-gphgnvpf.js → cli-m04pt18h.js} +2 -2
  45. package/dist/{cli-rgvp6r2x.js → cli-mcnhgnn1.js} +193 -4
  46. package/dist/{cli-j4nv2wqy.js → cli-mzgyhxbe.js} +10 -4
  47. package/dist/{cli-qn042eh7.js → cli-n0q9b7j4.js} +46 -1870
  48. package/dist/cli-n7h1kqpf.js +29 -0
  49. package/dist/{cli-ckz5t2hb.js → cli-nq0kgyxe.js} +5 -5
  50. package/dist/{cli-z3wkng35.js → cli-qrqhc31j.js} +151 -125
  51. package/dist/{cli-56h18ng2.js → cli-r8tdds6s.js} +1 -1
  52. package/dist/{cli-ch38q5z4.js → cli-rj7de4b2.js} +6788 -2541
  53. package/dist/{cli-qd65dvr6.js → cli-s5s1f2yn.js} +56 -40
  54. package/dist/{cli-kj76p1ez.js → cli-scwq51gn.js} +13 -7
  55. package/dist/{cli-2nnd56zt.js → cli-tdh4hjr2.js} +3 -2
  56. package/dist/{cli-c2mk1kgf.js → cli-v4en75c5.js} +8 -1
  57. package/dist/{cli-b9dd0h3k.js → cli-vw03hg5t.js} +3 -2
  58. package/dist/{cli-zvkq5csn.js → cli-ybpp1srm.js} +6 -6
  59. package/dist/cli-yn0a417a.js +4069 -0
  60. package/dist/{cli-gpqcs935.js → cli-ysc1kbet.js} +2 -2
  61. package/dist/{cli-zfzznn7c.js → cli-ytxs7mn1.js} +26 -2
  62. package/dist/cli-zcsyfw2z.js +3335 -0
  63. package/dist/{cli-4j8k1m4w.js → cli-zt1227k1.js} +1162 -192
  64. package/dist/cli-zy5w9gpt.js +230 -0
  65. package/dist/cli.js +26 -34
  66. package/dist/{codex-adapter-fxmpsabw.js → codex-adapter-8548daap.js} +12 -11
  67. package/dist/{codex-session-runner-rr1e47ea.js → codex-session-runner-2dad8p4x.js} +12 -11
  68. package/dist/{commands-0f5rxz51.js → commands-355dbpa8.js} +6 -4
  69. package/dist/{db-r3jxf3wy.js → db-4jjvwz1s.js} +6 -4
  70. package/dist/{dispatcher-06vph6q4.js → dispatcher-ca590nkz.js} +29 -25
  71. package/dist/dsh-adapter-rgvj6ykp.js +19 -0
  72. package/dist/{e2b-413b8t4v.js → e2b-93x1k0j5.js} +6 -7
  73. package/dist/{google-generative-ai-ndjmj5fa.js → google-generative-ai-xtw7tggr.js} +33 -73
  74. package/dist/{google-vertex-fr665tqs.js → google-vertex-zdqpms6n.js} +38 -68
  75. package/dist/{handlers-0hgtc910.js → handlers-2y6dqpvf.js} +32 -28
  76. package/dist/{hook-9kvrv1er.js → hook-0s8knepr.js} +11 -9
  77. package/dist/{hook-fbfhkat3.js → hook-k0gec254.js} +12 -10
  78. package/dist/{http-xbyx8k5j.js → http-jp2nb4zw.js} +300 -237
  79. package/dist/{index-tfsdxyv6.js → index-0wzg3sp3.js} +39 -24
  80. package/dist/{index-rnyahfn5.js → index-3mq6ce93.js} +29 -25
  81. package/dist/{index-jsw3aynb.js → index-42s7eezq.js} +29 -25
  82. package/dist/{cli-pdz9qde4.js → index-9ch1n761.js} +10 -11
  83. package/dist/{index-gpfn1741.js → index-b56xkbc7.js} +8 -6
  84. package/dist/{index-66xz5e9j.js → index-h5gs261z.js} +521 -9
  85. package/dist/{index-2sp0384z.js → index-kkeftv5h.js} +738 -21
  86. package/dist/{index-sgk50mbg.js → index-s37dbevy.js} +30 -26
  87. package/dist/index-w097eg0x.js +5 -0
  88. package/dist/jiti-loader-b4837mtm.js +34 -0
  89. package/dist/jiti-static-loader-q2g5ds0y.js +31267 -0
  90. package/dist/{keepalive-s3xpr7sw.js → keepalive-9pfw7t02.js} +8 -6
  91. package/dist/{lead-rq4n5r16.js → lead-7vcmcdq4.js} +44 -39
  92. package/dist/{maintenance-f0atj1ab.js → maintenance-x277jbpd.js} +9 -7
  93. package/dist/{mistral-conversations-qhr84ph3.js → mistral-conversations-zx0kn554.js} +24 -18
  94. package/dist/{node-x1g4v0pq.js → node-5qtycf6a.js} +16 -11
  95. package/dist/{oauth-refresh-sweep-q0t3bxre.js → oauth-refresh-sweep-5fwa7th2.js} +7 -5
  96. package/dist/{onboard-br5j1kch.js → onboard-17sv0f51.js} +103 -35
  97. package/dist/{openai-codex-responses-wy6a43wd.js → openai-codex-responses-4vjqzvjp.js} +33 -27
  98. package/dist/{openai-completions-32rdwmze.js → openai-completions-pxsmym7g.js} +52 -64
  99. package/dist/{openai-responses-6xwf7bzg.js → openai-responses-tmn0rqp0.js} +23 -24
  100. package/dist/{opencode-adapter-28axqa09.js → opencode-adapter-qbmv4tpy.js} +6 -6
  101. package/dist/{otel-impl-vdqge2nt.js → otel-impl-srjy4be4.js} +4 -4
  102. package/dist/{pi-messages-b1mbr6gp.js → pi-messages-txpjj39q.js} +3 -3
  103. package/dist/pi-mono-adapter-946a0sdd.js +1316 -0
  104. package/dist/{pricing-refresh-hb3691fd.js → pricing-refresh-vetcm563.js} +8 -6
  105. package/dist/{rbac-roles-jh280k9g.js → rbac-roles-cfzgr6r1.js} +8 -6
  106. package/dist/{rbac-roles-t78xt39b.js → rbac-roles-cmxaddmy.js} +7 -5
  107. package/dist/{render-v2-7p8r2ymg.js → render-v2-f9hzff72.js} +9 -6
  108. package/dist/{seed-pricing-hf889enh.js → seed-pricing-jggk6rsy.js} +7 -5
  109. package/dist/{setup-w6mb03jv.js → setup-8mfceb9t.js} +4 -5
  110. package/dist/virtual-modules-ax661xp0.js +54 -0
  111. package/dist/{work-objects-nt8gy1kj.js → work-objects-vdsdcczj.js} +32 -28
  112. package/dist/{worker-dwy617t0.js → worker-cd0nhyv1.js} +44 -39
  113. package/openapi.json +36 -14
  114. package/package.json +6 -6
  115. package/src/be/automation-preflight.ts +6 -4
  116. package/src/be/extensions/db.ts +35 -4
  117. package/src/be/models-catalog.ts +2 -0
  118. package/src/be/modelsdev-cache.json +213 -0
  119. package/src/be/rbac-audit.ts +2 -0
  120. package/src/be/seed-pricing.ts +3 -1
  121. package/src/be/swarm-config-guard.ts +1 -0
  122. package/src/commands/onboard/compose-generator.ts +6 -1
  123. package/src/commands/onboard/env-generator.ts +6 -1
  124. package/src/commands/onboard/manifest.ts +1 -0
  125. package/src/commands/onboard/steps/integration-slack.tsx +56 -16
  126. package/src/commands/onboard/steps/review.tsx +5 -0
  127. package/src/commands/onboard/types.ts +6 -0
  128. package/src/commands/onboard.tsx +5 -1
  129. package/src/commands/provider-credentials.ts +14 -2
  130. package/src/extensions/lifecycle.ts +15 -21
  131. package/src/http/agents.ts +2 -2
  132. package/src/http/config.ts +1 -1
  133. package/src/http/core.ts +5 -3
  134. package/src/http/extensions.ts +108 -34
  135. package/src/http/status.ts +22 -10
  136. package/src/prompts/base-prompt.ts +15 -7
  137. package/src/providers/claude-managed-models.ts +8 -0
  138. package/src/providers/codex-models.ts +15 -1
  139. package/src/providers/dsh-adapter.ts +248 -0
  140. package/src/providers/index.ts +5 -1
  141. package/src/providers/modelsdev-reasoning.json +50 -0
  142. package/src/rbac/legacy-policy.ts +15 -1
  143. package/src/rbac/types.ts +2 -0
  144. package/src/server.ts +7 -1
  145. package/src/slack/app.ts +32 -10
  146. package/src/slack/config.ts +29 -0
  147. package/src/telemetry.ts +2 -3
  148. package/src/tests/agents-harness-provider.test.ts +19 -0
  149. package/src/tests/agents-list-model-display.test.ts +60 -1
  150. package/src/tests/base-prompt.test.ts +25 -0
  151. package/src/tests/claude-managed-adapter.test.ts +6 -0
  152. package/src/tests/codex-gpt6-release.test.ts +97 -0
  153. package/src/tests/config-api-only-keys.test.ts +9 -0
  154. package/src/tests/context-window.test.ts +1 -0
  155. package/src/tests/dsh-adapter.test.ts +276 -0
  156. package/src/tests/e2e-nightly-report.test.ts +3 -3
  157. package/src/tests/e2e-redact.test.ts +2 -0
  158. package/src/tests/env-flag.test.ts +6 -0
  159. package/src/tests/extensions-db.test.ts +41 -0
  160. package/src/tests/extensions-http.test.ts +148 -4
  161. package/src/tests/extensions-lifecycle.test.ts +18 -0
  162. package/src/tests/extensions-mcp-tools.test.ts +9 -5
  163. package/src/tests/internal-ai/complete-structured.test.ts +32 -9
  164. package/src/tests/internal-ai/credentials.test.ts +4 -4
  165. package/src/tests/internal-ai/models.test.ts +20 -0
  166. package/src/tests/migration-046-budgets.test.ts +2 -0
  167. package/src/tests/model-control.test.ts +2 -2
  168. package/src/tests/onboard-compose.test.ts +14 -0
  169. package/src/tests/onboard-env.test.ts +17 -0
  170. package/src/tests/onboard-manifest.test.ts +12 -0
  171. package/src/tests/opus-5-5-registry.test.ts +48 -0
  172. package/src/tests/pricing-refresh.test.ts +1 -1
  173. package/src/tests/pricing-routes.test.ts +15 -3
  174. package/src/tests/provider-registration.test.ts +5 -2
  175. package/src/tests/rbac-admission-e2e.test.ts +54 -0
  176. package/src/tests/rbac-e2e-helpers.ts +2 -0
  177. package/src/tests/rbac-engine.test.ts +32 -0
  178. package/src/tests/rbac-wire-e2e.test.ts +33 -0
  179. package/src/tests/reasoning-effort.test.ts +8 -0
  180. package/src/tests/reload-config.test.ts +33 -0
  181. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  182. package/src/tests/slack-config.test.ts +136 -0
  183. package/src/tests/slack-mode-lifecycle.test.ts +69 -0
  184. package/src/tests/status.test.ts +22 -2
  185. package/src/tests/task-tool-preload-mcp.test.ts +3 -1
  186. package/src/tests/telemetry-init.test.ts +18 -0
  187. package/src/tests/workflow-llm.test.ts +3 -3
  188. package/src/tools/extension-common.ts +4 -0
  189. package/src/tools/extension-install.ts +1 -1
  190. package/src/tools/extension-list.ts +4 -3
  191. package/src/types.ts +17 -8
  192. package/src/utils/context-window.ts +1 -0
  193. package/src/utils/credentials.ts +8 -0
  194. package/src/utils/internal-ai/complete-structured.ts +8 -0
  195. package/src/utils/internal-ai/models.ts +5 -5
  196. package/templates/schedules/daily-status-report/content.md +25 -12
  197. package/templates/schedules/daily-workflow-health-audit/content.md +1 -1
  198. package/templates/skills/swarm-extensions/content.md +9 -5
  199. package/dist/cli-4wppfkcm.js +0 -16384
  200. package/dist/cli-55qrx3q8.js +0 -6041
  201. package/dist/cli-cg4vnyh4.js +0 -305
  202. package/dist/cli-jyetr64j.js +0 -52
  203. package/dist/cli-pp9ax9pa.js +0 -520
  204. package/dist/cli-qa66qaf0.js +0 -6076
  205. package/dist/cli-qghpvca2.js +0 -590
  206. package/dist/cli-rembwgt8.js +0 -861
  207. package/dist/cli-tcvj7j2r.js +0 -1971
  208. package/dist/cli-vfcth11k.js +0 -36
  209. package/dist/cli-w1d1bgkk.js +0 -1422
  210. package/dist/cli-y6x0gqtg.js +0 -5977
  211. package/dist/es5-tj3tn4jj.js +0 -844
  212. package/dist/index-1kq5hw2p.js +0 -467
  213. package/dist/index-1p8wdstt.js +0 -7
  214. package/dist/index-8fdjzb2f.js +0 -100
  215. package/dist/index-bxmwzbgw.js +0 -365
  216. package/dist/index-nzerxd5z.js +0 -7
  217. package/dist/index-pyymnzat.js +0 -1168
  218. package/dist/index-qv6atbab.js +0 -619
  219. package/dist/index-rja32qa7.js +0 -801
  220. package/dist/index-tzg5srfp.js +0 -211
  221. package/dist/index-vhncw983.js +0 -90
  222. package/dist/index-xk036zsk.js +0 -859
  223. package/dist/index-zyazbzcb.js +0 -6
  224. package/dist/{claude-managed-setup-48dwg9bv.js → claude-managed-setup-f5pmsyrp.js} +3 -3
@@ -10,6 +10,7 @@
10
10
  import { Database } from "bun:sqlite";
11
11
  import { afterAll, describe, expect, setDefaultTimeout, test } from "bun:test";
12
12
  import { join } from "node:path";
13
+ import { loadBundleFixture } from "./fixtures/extensions/load";
13
14
  import {
14
15
  api,
15
16
  makeScratchDir,
@@ -483,3 +484,56 @@ describe("RBAC admission over real HTTP", () => {
483
484
  );
484
485
  });
485
486
  });
487
+
488
+ test.each(["true", "false"])("worker extension ownership with RBAC_ENABLED=%s", async (enabled) => {
489
+ const scratch = await makeScratchDir();
490
+ const instance = await spawnSwarmServer({
491
+ dbPath: join(scratch, "extension-admission.sqlite"),
492
+ logPath: join(scratch, "extension-admission.log"),
493
+ env: { RBAC_ENABLED: enabled },
494
+ });
495
+ try {
496
+ await registerAgent(instance.base, WORKER_A, "extension-owner", false);
497
+ const bundle = await loadBundleFixture("minimal");
498
+ const installed = await api(instance.base, "POST", "/api/extensions/install", {
499
+ agentId: WORKER_A,
500
+ body: bundle,
501
+ });
502
+ expect(installed.status).toBe(200);
503
+ expect(installed.body.extension).toMatchObject({
504
+ createdByAgentId: WORKER_A,
505
+ enabled: false,
506
+ status: "disabled",
507
+ });
508
+ const path = `/api/extensions/${installed.body.extension.id}`;
509
+ for (const operation of ["enable", "disable", "activate-version"]) {
510
+ expect(
511
+ (
512
+ await api(instance.base, "POST", `${path}/${operation}`, {
513
+ agentId: WORKER_A,
514
+ body: { version: 1 },
515
+ })
516
+ ).status,
517
+ ).toBe(403);
518
+ }
519
+ const operatorBundle = { ...bundle, manifest: { ...bundle.manifest, name: "operator-owned" } };
520
+ const operator = await api(instance.base, "POST", "/api/extensions/install", {
521
+ body: operatorBundle,
522
+ });
523
+ expect(operator.status).toBe(200);
524
+ expect(operator.body.extension.createdByAgentId).toBeNull();
525
+ for (const method of ["PATCH", "DELETE"]) {
526
+ expect(
527
+ (
528
+ await api(instance.base, method, `/api/extensions/${operator.body.extension.id}`, {
529
+ agentId: WORKER_A,
530
+ ...(method === "PATCH" ? { body: { priority: 1 } } : {}),
531
+ })
532
+ ).status,
533
+ ).toBe(403);
534
+ }
535
+ } finally {
536
+ await instance.stop();
537
+ await removeScratchDir(scratch);
538
+ }
539
+ });
@@ -78,6 +78,8 @@ export async function spawnSwarmServer(opts: {
78
78
  API_KEY: E2E_API_KEY,
79
79
  AGENT_SWARM_API_KEY: E2E_API_KEY,
80
80
  PORT: String(port),
81
+ // MCP proxy tools must call this scratch server, never an inherited live URL.
82
+ MCP_BASE_URL: `http://localhost:${port}`,
81
83
  SLACK_DISABLE: "true",
82
84
  GITHUB_DISABLE: "true",
83
85
  JIRA_DISABLE: "true",
@@ -255,6 +255,38 @@ describe("lead-or-operator-or-user verbs", () => {
255
255
  }
256
256
  });
257
257
 
258
+ describe("extension ownership", () => {
259
+ test("workers create extensions and write only their own existing resource", () => {
260
+ const worker = { kind: "agent", agentId: "extension-owner", isLead: false } as const;
261
+ for (const resource of [
262
+ { kind: "extension" },
263
+ { kind: "extension", extensionId: "ext-1", createdByAgentId: worker.agentId },
264
+ ] as const) {
265
+ expect(
266
+ can({ principal: worker, verb: "extension.write", resource, source: "http" }).allow,
267
+ ).toBe(true);
268
+ }
269
+ for (const createdByAgentId of [null, "other-agent"]) {
270
+ expect(
271
+ can({
272
+ principal: worker,
273
+ verb: "extension.write",
274
+ resource: { kind: "extension", extensionId: "ext-1", createdByAgentId },
275
+ source: "http",
276
+ }).allow,
277
+ ).toBe(false);
278
+ }
279
+ expect(
280
+ can({
281
+ principal: { ...worker, agentId: "" },
282
+ verb: "extension.write",
283
+ resource: { kind: "extension" },
284
+ source: "http",
285
+ }).allow,
286
+ ).toBe(false);
287
+ });
288
+ });
289
+
258
290
  describe("lead-or-resource-owner verbs", () => {
259
291
  const expected: Expected = {
260
292
  lead: true,
@@ -18,6 +18,7 @@
18
18
  import { Database } from "bun:sqlite";
19
19
  import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
20
20
  import { join } from "node:path";
21
+ import { loadBundleFixture } from "./fixtures/extensions/load";
21
22
  import {
22
23
  api,
23
24
  LEAD,
@@ -119,6 +120,38 @@ afterAll(async () => {
119
120
  });
120
121
 
121
122
  describe("MCP gate matrix", () => {
123
+ test("worker MCP installs retain ownership and lifecycle calls remain lead-only", async () => {
124
+ const bundle = await loadBundleFixture("minimal");
125
+ bundle.manifest.name = "worker-owned-wire";
126
+ const installed = await mcpCall(base, WORKER_A, sidA, "extension-install", bundle);
127
+ expect(installed.structuredContent).toMatchObject({ success: true });
128
+ expect(installed.structuredContent).toMatchObject({
129
+ createdByAgentId: WORKER_A,
130
+ enabled: false,
131
+ status: "disabled",
132
+ });
133
+ expectRow({ type: "agent", id: WORKER_A }, "extension.write", "allow", null, "http");
134
+ const id = installed.structuredContent.id;
135
+ const listed = await mcpCall(base, WORKER_A, sidA, "extension-list", {});
136
+ expect(listed.structuredContent.extensions).toContainEqual(
137
+ expect.objectContaining({ id, createdByAgentId: WORKER_A }),
138
+ );
139
+ for (const tool of ["extension-enable", "extension-disable", "extension-activate-version"]) {
140
+ const denied = await mcpCall(base, WORKER_A, sidA, tool, {
141
+ id,
142
+ ...(tool === "extension-activate-version" ? { version: 1 } : {}),
143
+ });
144
+ expect(denied.structuredContent.success).toBe(false);
145
+ expectRow(
146
+ { type: "agent", id: WORKER_A },
147
+ "extension.activate",
148
+ "deny",
149
+ "requires operator/user authentication, or a lead with EXTENSION_ALLOW_LEAD_ACTIVATION enabled",
150
+ "http",
151
+ );
152
+ }
153
+ });
154
+
122
155
  test("config: set/delete lead-gated; includeSecrets masks for non-lead (DES-445 follow-up)", async () => {
123
156
  // set-config (non-credential key): worker denied, lead allowed.
124
157
  const setWorker = await mcpCall(base, WORKER_A, sidA, "set-config", {
@@ -22,6 +22,14 @@ describe("REASONING_EFFORT_LEVELS", () => {
22
22
  });
23
23
 
24
24
  describe("reasoningCapability — cache-sourced levels", () => {
25
+ test("Claude Opus 5.5 exposes supported CLI effort levels", () => {
26
+ expect(reasoningCapability("claude", "claude-opus-5-5").levels).toEqual([
27
+ "low",
28
+ "medium",
29
+ "high",
30
+ "xhigh",
31
+ ]);
32
+ });
25
33
  test("claude claude-opus-4-8: levels come from reasoning_options.effort, not the fallback", () => {
26
34
  const cap = reasoningCapability("claude", "claude-opus-4-8");
27
35
  expect(cap.supported).toBe(true);
@@ -323,6 +323,39 @@ describe("reload-config", () => {
323
323
  }
324
324
  });
325
325
 
326
+ test("reload applies HTTP mode without reporting a Slack socket as reinitialized", async () => {
327
+ const keys = [
328
+ "SLACK_MODE",
329
+ "SLACK_DISABLE",
330
+ "SLACK_BOT_TOKEN",
331
+ "SLACK_SIGNING_SECRET",
332
+ ] as const;
333
+ const originals = new Map(keys.map((key) => [key, process.env[key]]));
334
+ process.env.SLACK_DISABLE = "false";
335
+ process.env.SLACK_BOT_TOKEN = "xoxb-test";
336
+ process.env.SLACK_SIGNING_SECRET = "synthetic-signing-secret";
337
+ const config = await upsertSwarmConfig({
338
+ scope: "global",
339
+ key: "SLACK_MODE",
340
+ value: "http",
341
+ });
342
+
343
+ try {
344
+ const result = await reloadGlobalConfigsAndIntegrations();
345
+ expect(result.keysUpdated).toContain("SLACK_MODE");
346
+ expect(result.integrationsReinitialized).not.toContain("slack");
347
+ expect(process.env.SLACK_MODE).toBe("http");
348
+ } finally {
349
+ await deleteSwarmConfig(config.id);
350
+ await loadGlobalConfigsIntoEnv(true);
351
+ for (const key of keys) {
352
+ const value = originals.get(key);
353
+ if (value === undefined) delete process.env[key];
354
+ else process.env[key] = value;
355
+ }
356
+ }
357
+ });
358
+
326
359
  // Regression: injection used to be one-way. Deleting a global row left the
327
360
  // previously-injected value live in process.env until the process restarted,
328
361
  // so "reset to default" in the dashboard silently did nothing.
@@ -133,6 +133,7 @@ describe("normalizeModelKey()", () => {
133
133
  expect(normalizeModelKey("claude", "claude-opus-4-7")).toBe("claude-opus-4-7");
134
134
  expect(normalizeModelKey("claude", "claude-fable-5-1")).toBe("claude-fable-5-1");
135
135
  expect(normalizeModelKey("claude", "claude-mythos-5-1")).toBe("claude-mythos-5-1");
136
+ expect(normalizeModelKey("claude", "claude-opus-5-5")).toBe("claude-opus-5-5");
136
137
  expect(normalizeModelKey("claude", "claude-opus-5")).toBe("claude-opus-5");
137
138
  expect(normalizeModelKey("claude", "claude-fable-5")).toBe("claude-fable-5");
138
139
  expect(normalizeModelKey("claude", "claude-mythos-5")).toBe("claude-mythos-5");
@@ -0,0 +1,136 @@
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
+ import { startSlackMock, stopSlackMock } from "../../scripts/e2e/slack";
3
+ import { closeDb, initDb } from "../be/db";
4
+ import { getEnabledCapabilities } from "../server";
5
+ import { getSlackApp, startSlackApp, stopSlackApp } from "../slack/app";
6
+ import { getSlackConfiguration, isSlackConfigured } from "../slack/config";
7
+
8
+ const SLACK_ENV_KEYS = [
9
+ "NODE_ENV",
10
+ "SLACK_ALLOW_DEV_SOCKET_MODE",
11
+ "CAPABILITIES",
12
+ "SLACK_MODE",
13
+ "SLACK_DISABLE",
14
+ "SLACK_BOT_TOKEN",
15
+ "SLACK_APP_TOKEN",
16
+ "SLACK_SIGNING_SECRET",
17
+ "SLACK_API_URL",
18
+ ] as const;
19
+ const originalEnv = new Map(SLACK_ENV_KEYS.map((key) => [key, process.env[key]]));
20
+
21
+ beforeEach(() => {
22
+ for (const key of SLACK_ENV_KEYS) delete process.env[key];
23
+ process.env.NODE_ENV = "test";
24
+ process.env.SLACK_DISABLE = "false";
25
+ });
26
+
27
+ afterEach(async () => {
28
+ await stopSlackApp();
29
+ for (const key of SLACK_ENV_KEYS) {
30
+ const value = originalEnv.get(key);
31
+ if (value === undefined) delete process.env[key];
32
+ else process.env[key] = value;
33
+ }
34
+ });
35
+
36
+ describe("Slack transport configuration", () => {
37
+ test("defaults to Socket Mode and requires the existing token pair", () => {
38
+ expect(getSlackConfiguration({})).toEqual({
39
+ disabled: false,
40
+ mode: "socket",
41
+ missingCredentials: ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"],
42
+ });
43
+ expect(getEnabledCapabilities()).toContain("slack");
44
+ expect(isSlackConfigured({ SLACK_BOT_TOKEN: "xoxb-test", SLACK_APP_TOKEN: "xapp-test" })).toBe(
45
+ true,
46
+ );
47
+ });
48
+
49
+ test("HTTP mode requires bot and signing credentials without an app token", () => {
50
+ const env = {
51
+ SLACK_MODE: "http",
52
+ SLACK_BOT_TOKEN: "xoxb-test",
53
+ SLACK_SIGNING_SECRET: "synthetic-signing-secret",
54
+ };
55
+ expect(getSlackConfiguration(env)).toEqual({
56
+ disabled: false,
57
+ mode: "http",
58
+ missingCredentials: [],
59
+ });
60
+ expect(isSlackConfigured(env)).toBe(true);
61
+ });
62
+
63
+ test("disabled and invalid configurations fail closed", () => {
64
+ expect(
65
+ isSlackConfigured({
66
+ SLACK_DISABLE: "true",
67
+ SLACK_BOT_TOKEN: "xoxb-test",
68
+ SLACK_APP_TOKEN: "xapp-test",
69
+ }),
70
+ ).toBe(false);
71
+ expect(
72
+ getSlackConfiguration({
73
+ SLACK_MODE: "webhook",
74
+ SLACK_BOT_TOKEN: "xoxb-test",
75
+ SLACK_APP_TOKEN: "xapp-test",
76
+ }),
77
+ ).toMatchObject({ mode: null });
78
+ });
79
+
80
+ test("HTTP selection remains unavailable and never creates a socket app", async () => {
81
+ process.env.SLACK_MODE = "http";
82
+ expect(getEnabledCapabilities()).not.toContain("slack");
83
+ process.env.SLACK_BOT_TOKEN = "xoxb-test";
84
+ process.env.SLACK_SIGNING_SECRET = "synthetic-signing-secret";
85
+ delete process.env.SLACK_APP_TOKEN;
86
+
87
+ const error = spyOn(console, "error").mockImplementation(() => {});
88
+ try {
89
+ expect(await startSlackApp()).toBe(false);
90
+ expect(getSlackApp()).toBeNull();
91
+ expect(error).toHaveBeenCalledWith(
92
+ "[Slack] HTTP mode is configured but unavailable until the HTTP receiver is installed",
93
+ );
94
+ } finally {
95
+ error.mockRestore();
96
+ }
97
+ });
98
+ });
99
+
100
+ test.each([
101
+ "disabled",
102
+ "invalid mode",
103
+ "missing credentials",
104
+ "HTTP unavailable",
105
+ "socket blocked",
106
+ ])("failed init is retryable without stop: %s", async (reason) => {
107
+ const slack = await startSlackMock(false);
108
+ initDb(":memory:");
109
+ try {
110
+ Object.assign(process.env, slack.mock.env);
111
+ process.env.SLACK_SIGNING_SECRET = "synthetic-signing-secret";
112
+ if (reason === "disabled") process.env.SLACK_DISABLE = "true";
113
+ if (reason === "invalid mode") process.env.SLACK_MODE = "invalid";
114
+ if (reason === "missing credentials") delete process.env.SLACK_APP_TOKEN;
115
+ if (reason === "HTTP unavailable") process.env.SLACK_MODE = "http";
116
+ if (reason === "socket blocked") process.env.NODE_ENV = "development";
117
+
118
+ expect(await startSlackApp()).toBe(false);
119
+ expect(getSlackApp()).toBeNull();
120
+ expect(slack.mock.apiCalls("apps.connections.open")).toHaveLength(0);
121
+
122
+ Object.assign(process.env, slack.mock.env);
123
+ process.env.SLACK_DISABLE = "false";
124
+ process.env.SLACK_MODE = "socket";
125
+ process.env.NODE_ENV = "test";
126
+ // Deliberately retry without stopSlackApp: failed initialization must not latch.
127
+ expect(await startSlackApp()).toBe(true);
128
+ await slack.mock.waitForConnection(10_000);
129
+ expect(slack.mock.connectionCount).toBe(1);
130
+ expect(slack.mock.apiCalls("apps.connections.open")).toHaveLength(1);
131
+ } finally {
132
+ await stopSlackApp();
133
+ await stopSlackMock(slack, false);
134
+ closeDb();
135
+ }
136
+ }, 20_000);
@@ -0,0 +1,69 @@
1
+ import { expect, test } from "bun:test";
2
+ import { createApiClient, pollUntil } from "../../scripts/e2e/http";
3
+ import { startSlackMock, stopSlackMock } from "../../scripts/e2e/slack";
4
+ import { type Sut, startSut, stopSut } from "../../scripts/e2e/sut";
5
+
6
+ test("config reload disconnects a live socket for HTTP and can return to Socket Mode", async () => {
7
+ const slack = await startSlackMock(false);
8
+ let sut: Sut | undefined;
9
+ try {
10
+ sut = await startSut(false, slack.mock.env, {
11
+ SLACK_SIGNING_SECRET: "synthetic-signing-secret",
12
+ HEARTBEAT_DISABLE: "true",
13
+ });
14
+ await slack.mock.waitForConnection(10_000);
15
+ expect(slack.mock.connectionCount).toBe(1);
16
+ const opened = slack.mock.apiCalls("apps.connections.open").length;
17
+ const api = createApiClient(sut.baseUrl, sut.apiKey);
18
+ const http = await api("PUT", "/api/config", {
19
+ body: { scope: "global", key: "SLACK_MODE", value: "http" },
20
+ });
21
+ expect(http.status).toBe(200);
22
+ expect(await pollUntil(() => slack.mock.connectionCount === 0, 10_000, 50)).toBe(true);
23
+ const reload = await api("POST", "/api/config/reload");
24
+ expect(reload.status).toBe(200);
25
+ expect(reload.json).toMatchObject({ success: true });
26
+ expect(
27
+ (reload.json as { integrationsReinitialized: string[] }).integrationsReinitialized,
28
+ ).not.toContain("slack");
29
+ expect(slack.mock.apiCalls("apps.connections.open").length).toBe(opened);
30
+ const socket = await api("PUT", "/api/config", {
31
+ body: { scope: "global", key: "SLACK_MODE", value: "socket" },
32
+ });
33
+ expect(socket.status).toBe(200);
34
+ expect(await pollUntil(() => slack.mock.connectionCount === 1, 10_000, 50)).toBe(true);
35
+ expect(slack.mock.apiCalls("apps.connections.open").length).toBe(opened + 1);
36
+ } finally {
37
+ if (sut) await stopSut(sut, false);
38
+ await stopSlackMock(slack, false);
39
+ }
40
+ }, 30_000);
41
+
42
+ test("config reload recovers from an initial unavailable HTTP transport", async () => {
43
+ const slack = await startSlackMock(false);
44
+ let sut: Sut | undefined;
45
+ try {
46
+ sut = await startSut(false, slack.mock.env, {
47
+ SLACK_MODE: "http",
48
+ SLACK_SIGNING_SECRET: "synthetic-signing-secret",
49
+ HEARTBEAT_DISABLE: "true",
50
+ });
51
+ const api = createApiClient(sut.baseUrl, sut.apiKey);
52
+ const initialReload = await api("POST", "/api/config/reload");
53
+ expect(initialReload.status).toBe(200);
54
+ expect(
55
+ (initialReload.json as { integrationsReinitialized: string[] }).integrationsReinitialized,
56
+ ).not.toContain("slack");
57
+ expect(slack.mock.apiCalls("apps.connections.open")).toHaveLength(0);
58
+
59
+ const socket = await api("PUT", "/api/config", {
60
+ body: { scope: "global", key: "SLACK_MODE", value: "socket" },
61
+ });
62
+ expect(socket.status).toBe(200);
63
+ expect(await pollUntil(() => slack.mock.connectionCount === 1, 10_000, 50)).toBe(true);
64
+ expect(slack.mock.apiCalls("apps.connections.open")).toHaveLength(1);
65
+ } finally {
66
+ if (sut) await stopSut(sut, false);
67
+ await stopSlackMock(slack, false);
68
+ }
69
+ }, 30_000);
@@ -96,6 +96,8 @@ const ENV_KEYS_TO_RESET = [
96
96
  "DEVIN_ORG_ID",
97
97
  "SLACK_BOT_TOKEN",
98
98
  "SLACK_APP_TOKEN",
99
+ "SLACK_SIGNING_SECRET",
100
+ "SLACK_MODE",
99
101
  "SLACK_DISABLE",
100
102
  "GITHUB_WEBHOOK_SECRET",
101
103
  "GITHUB_APP_ID",
@@ -409,20 +411,38 @@ describe("setup milestones", () => {
409
411
  });
410
412
  });
411
413
 
412
- test("slack: needs both bot+app tokens AND not disabled", async () => {
414
+ test("slack: reports mode-aware credentials as configured, not live-verified", async () => {
413
415
  process.env.SLACK_BOT_TOKEN = "xoxb-test";
414
416
  const a = await buildStatusPayload();
415
417
  expect(getMilestone(a, "slack").state).toBe("unverified");
416
418
 
417
419
  process.env.SLACK_APP_TOKEN = "xapp-test";
418
420
  const b = await buildStatusPayload();
419
- expect(getMilestone(b, "slack").state).toBe("verified");
421
+ expect(getMilestone(b, "slack").state).toBe("configured");
422
+ expect(getMilestone(b, "slack").hint).toContain("not been verified");
423
+
424
+ process.env.SLACK_MODE = "http";
425
+ delete process.env.SLACK_APP_TOKEN;
426
+ process.env.SLACK_SIGNING_SECRET = "synthetic-signing-secret";
427
+ const http = await buildStatusPayload();
428
+ expect(getMilestone(http, "slack").state).toBe("unverified");
429
+ expect(getMilestone(http, "slack").hint).toContain("unavailable");
420
430
 
421
431
  process.env.SLACK_DISABLE = "true";
422
432
  const c = await buildStatusPayload();
423
433
  expect(getMilestone(c, "slack").state).toBe("unverified");
424
434
  });
425
435
 
436
+ test("slack: invalid mode fails closed with a specific hint", async () => {
437
+ process.env.SLACK_MODE = "webhook";
438
+ process.env.SLACK_BOT_TOKEN = "xoxb-test";
439
+ process.env.SLACK_APP_TOKEN = "xapp-test";
440
+
441
+ const milestone = getMilestone(await buildStatusPayload(), "slack");
442
+ expect(milestone.state).toBe("unverified");
443
+ expect(milestone.hint).toContain("Invalid SLACK_MODE");
444
+ });
445
+
426
446
  test("github: needs webhook secret + app id + private key", async () => {
427
447
  process.env.GITHUB_WEBHOOK_SECRET = "secret";
428
448
  process.env.GITHUB_APP_ID = "12345";
@@ -73,6 +73,8 @@ async function pins(client: Client): Promise<string[]> {
73
73
  .sort();
74
74
  }
75
75
 
76
+ // Eight real MCP sessions approach the 10s CI default under parallel runner load.
77
+ // Keep the session-lifetime assertions together with room for connection setup.
76
78
  test("wire response is default-on, per-task, additive, and fixed for one MCP session", async () => {
77
79
  const task = await createTaskExtended("review fixture", { agentId, taskType: "review" });
78
80
  const other = await createTaskExtended("other fixture", { agentId, taskType: "other" });
@@ -115,7 +117,7 @@ test("wire response is default-on, per-task, additive, and fixed for one MCP ses
115
117
  await configure("TASK_TOOL_PRELOAD_ENABLED", "true");
116
118
  await configure("TASK_TOOL_MANIFESTS", "invalid deployment JSON");
117
119
  expect(await pins(await connect(task.id))).toEqual([]);
118
- });
120
+ }, 30_000);
119
121
 
120
122
  test("a manifest cannot expand the scripts-only capability surface", async () => {
121
123
  const task = await createTaskExtended("script fixture", { agentId, taskType: "script" });
@@ -794,6 +794,24 @@ describe("initTelemetry", () => {
794
794
  ).toBe(false);
795
795
  });
796
796
 
797
+ test("Slack HTTP mode uses the signing secret instead of an app token", () => {
798
+ expect(
799
+ _hasSlackChannel({
800
+ SLACK_MODE: "http",
801
+ SLACK_BOT_TOKEN: "xoxb-1",
802
+ SLACK_SIGNING_SECRET: "synthetic-signing-secret",
803
+ }),
804
+ ).toBe(true);
805
+ expect(_hasSlackChannel({ SLACK_MODE: "http", SLACK_BOT_TOKEN: "xoxb-1" })).toBe(false);
806
+ expect(
807
+ _hasSlackChannel({
808
+ SLACK_MODE: "invalid",
809
+ SLACK_BOT_TOKEN: "xoxb-1",
810
+ SLACK_APP_TOKEN: "xapp-1",
811
+ }),
812
+ ).toBe(false);
813
+ });
814
+
797
815
  test("Email requires AGENTMAIL_WEBHOOK_SECRET", () => {
798
816
  expect(_hasEmailChannel({})).toBe(false);
799
817
  expect(_hasEmailChannel({ AGENTMAIL_WEBHOOK_SECRET: "whsec_1" })).toBe(true);
@@ -11,7 +11,7 @@ describe("resolveWorkflowLlmConfig", () => {
11
11
  expect(config).toEqual({
12
12
  apiKey: "sk-or-test",
13
13
  baseURL: "https://gateway.example.test/v1",
14
- model: "google/gemini-3-flash-preview",
14
+ model: "deepseek/deepseek-v4.1-flash",
15
15
  });
16
16
  });
17
17
 
@@ -23,7 +23,7 @@ describe("resolveWorkflowLlmConfig", () => {
23
23
  expect(config).toEqual({
24
24
  apiKey: "example-sk-openai-test",
25
25
  baseURL: undefined,
26
- model: "gpt-5.4-mini",
26
+ model: "gpt-6-luna",
27
27
  });
28
28
  });
29
29
 
@@ -34,7 +34,7 @@ describe("resolveWorkflowLlmConfig", () => {
34
34
  const config = await resolveWorkflowLlmConfig(undefined, {
35
35
  OPENAI_API_KEY: "example-sk-openai-test",
36
36
  });
37
- expect(config.model).toBe("gpt-5.4-mini");
37
+ expect(config.model).toBe("gpt-6-luna");
38
38
  } finally {
39
39
  if (previous === undefined) delete process.env.MEMORY_RATER_MODEL;
40
40
  else process.env.MEMORY_RATER_MODEL = previous;
@@ -10,6 +10,7 @@ const extensionListItemSchema = z.looseObject({
10
10
  name: z.string().optional(),
11
11
  version: z.number().optional(),
12
12
  activeVersion: z.number().optional(),
13
+ createdByAgentId: z.string().nullable().optional(),
13
14
  enabled: z.boolean().optional(),
14
15
  status: z.string().optional(),
15
16
  priority: z.number().optional(),
@@ -21,6 +22,7 @@ export const extensionToolOutputSchema = swarmToolOutputSchema({
21
22
  name: z.string().optional(),
22
23
  version: z.number().optional(),
23
24
  activeVersion: z.number().optional(),
25
+ createdByAgentId: z.string().nullable().optional(),
24
26
  enabled: z.boolean().optional(),
25
27
  status: z.string().optional(),
26
28
  priority: z.number().optional(),
@@ -38,6 +40,8 @@ export function coerceExtensionSummary(entry: unknown) {
38
40
  const extension = isRecord(entry) ? entry : {};
39
41
  return {
40
42
  id: typeof extension.id === "string" ? extension.id : undefined,
43
+ createdByAgentId:
44
+ typeof extension.createdByAgentId === "string" ? extension.createdByAgentId : null,
41
45
  name: typeof extension.name === "string" ? extension.name : undefined,
42
46
  version: typeof extension.version === "number" ? extension.version : undefined,
43
47
  activeVersion:
@@ -9,7 +9,7 @@ import {
9
9
  } from "./extension-common";
10
10
 
11
11
  export const EXTENSION_INSTALL_DESCRIPTION =
12
- "Validate and store a TypeScript extension bundle (manifest plus a files map with one hooks.ts). Fetch the hook contract first: GET /api/extensions/type-defs returns swarm-extension.d.ts with every event and modify shape. The extension remains disabled until a lead, operator, or dashboard user enables it with extension-enable.";
12
+ "Any authenticated agent may create and install its own TypeScript extension bundle (manifest plus a files map with one hooks.ts). Fetch the hook contract first: GET /api/extensions/type-defs returns swarm-extension.d.ts with every event and modify shape. The installing agent is recorded as createdByAgentId. Workers may install subsequent versions only for their own extensions; those versions remain inactive. A new extension remains disabled until a lead, operator, or dashboard user enables it with extension-enable.";
13
13
 
14
14
  function extensionInstallResult(data: unknown, fallbackName: string) {
15
15
  const body = data as {
@@ -24,11 +24,12 @@ function extensionListItems(data: unknown): ExtensionListItem[] {
24
24
  }
25
25
 
26
26
  function renderExtensionsTable(extensions: ExtensionListItem[]): string {
27
- const header = "| Name | Version | Active | Enabled | Status | Priority | Failures |";
28
- const divider = "| --- | ---: | ---: | --- | --- | ---: | ---: |";
27
+ const header =
28
+ "| Name | Owner agent | Version | Active | Enabled | Status | Priority | Failures |";
29
+ const divider = "| --- | --- | ---: | ---: | --- | --- | ---: | ---: |";
29
30
  const rows = extensions.map(
30
31
  (extension) =>
31
- `| ${extension.name ?? "?"} | ${extension.version ?? "?"} | ${extension.activeVersion ?? "?"} | ${extension.enabled ?? false} | ${extension.status ?? "unknown"} | ${extension.priority ?? "?"} | ${extension.consecutiveFailures ?? 0} |`,
32
+ `| ${extension.name ?? "?"} | ${extension.createdByAgentId ?? "operator/user"} | ${extension.version ?? "?"} | ${extension.activeVersion ?? "?"} | ${extension.enabled ?? false} | ${extension.status ?? "unknown"} | ${extension.priority ?? "?"} | ${extension.consecutiveFailures ?? 0} |`,
32
33
  );
33
34
  return [header, divider, ...rows].join("\n");
34
35
  }
package/src/types.ts CHANGED
@@ -136,16 +136,22 @@ export const DEFAULT_MODEL_TIER_MAP: Record<ProviderName, Record<ModelTier, stri
136
136
  ultra: "gpt-5.6-sol",
137
137
  },
138
138
  pi: {
139
- smol: "openrouter/deepseek/deepseek-v4-flash",
140
- regular: "openrouter/deepseek/deepseek-v4-flash",
141
- smart: "openrouter/deepseek/deepseek-v4-pro",
142
- ultra: "openrouter/anthropic/claude-opus-4.8",
139
+ smol: "openrouter/deepseek/deepseek-v4.1-flash",
140
+ regular: "openrouter/deepseek/deepseek-v4.1-flash",
141
+ smart: "openrouter/deepseek/deepseek-v4-pro-0813",
142
+ ultra: "openrouter/anthropic/claude-opus-5.5",
143
143
  },
144
144
  opencode: {
145
- smol: "openrouter/deepseek/deepseek-v4-flash",
146
- regular: "openrouter/deepseek/deepseek-v4-flash",
147
- smart: "openrouter/deepseek/deepseek-v4-pro",
148
- ultra: "openrouter/anthropic/claude-opus-4.8",
145
+ smol: "openrouter/deepseek/deepseek-v4.1-flash",
146
+ regular: "openrouter/deepseek/deepseek-v4.1-flash",
147
+ smart: "openrouter/deepseek/deepseek-v4-pro-0813",
148
+ ultra: "openrouter/anthropic/claude-opus-5.5",
149
+ },
150
+ dsh: {
151
+ smol: "openrouter/deepseek/deepseek-v4.1-flash",
152
+ regular: "openrouter/deepseek/deepseek-v4.1-flash",
153
+ smart: "openrouter/deepseek/deepseek-v4-pro-0813",
154
+ ultra: "openrouter/anthropic/claude-opus-5.5",
149
155
  },
150
156
  devin: {
151
157
  smol: "devin",
@@ -367,6 +373,7 @@ export const ProviderNameSchema = z.enum([
367
373
  "claude-managed",
368
374
  "opencode",
369
375
  "acp",
376
+ "dsh",
370
377
  ]);
371
378
  export type ProviderName = z.infer<typeof ProviderNameSchema>;
372
379
 
@@ -455,6 +462,7 @@ export const PROVIDER_STEER_CAPABILITIES: Record<ProviderName, SteerMode[]> = {
455
462
  // single in-flight turn and the only interrupt is `session/cancel` (abort).
456
463
  // Advertise nothing rather than promise semantics we can't honor.
457
464
  acp: [],
465
+ dsh: [],
458
466
  };
459
467
 
460
468
  export type DevinProviderMeta = {
@@ -474,6 +482,7 @@ export type ProviderMetaMap = {
474
482
  "claude-managed": NoProviderMeta;
475
483
  opencode: NoProviderMeta;
476
484
  acp: NoProviderMeta;
485
+ dsh: NoProviderMeta;
477
486
  };
478
487
 
479
488
  export const FollowUpConfigSchema = z