@desplega.ai/agent-swarm 1.113.0 → 1.115.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 (237) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
  3. package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
  4. package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
  5. package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
  6. package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
  7. package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
  8. package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
  9. package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
  10. package/dist/cli-0pygq9r1.js +429 -0
  11. package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
  12. package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
  13. package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
  14. package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
  15. package/dist/cli-52fg7s7x.js +269 -0
  16. package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
  17. package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
  18. package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
  19. package/dist/cli-86fdnerz.js +405 -0
  20. package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
  21. package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
  22. package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
  23. package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
  24. package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
  25. package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
  26. package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
  27. package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
  28. package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
  29. package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
  30. package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
  31. package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
  32. package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
  33. package/dist/cli-jkhwmmgg.js +136 -0
  34. package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
  35. package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
  36. package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
  37. package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
  38. package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
  39. package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
  40. package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
  41. package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
  42. package/dist/cli-t0vqevnm.js +393 -0
  43. package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
  44. package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
  45. package/dist/cli-wh91zby6.js +187222 -0
  46. package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
  47. package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
  48. package/dist/cli.js +32 -15
  49. package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
  50. package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
  51. package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
  52. package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
  53. package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
  54. package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
  55. package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
  56. package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
  57. package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
  58. package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
  59. package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
  60. package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
  61. package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
  62. package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
  63. package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
  64. package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
  65. package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
  66. package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
  67. package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
  68. package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
  69. package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
  70. package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
  71. package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
  72. package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
  73. package/dist/rbac-roles-kfesjy9y.js +28 -0
  74. package/dist/rbac-roles-mwcwhbxc.js +283 -0
  75. package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
  76. package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
  77. package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
  78. package/openapi.json +1327 -20
  79. package/package.json +3 -3
  80. package/src/agentmail/handlers.ts +16 -6
  81. package/src/agentmail/templates.ts +25 -5
  82. package/src/be/audit-user.ts +28 -4
  83. package/src/be/db-queries/oauth.ts +42 -0
  84. package/src/be/db.ts +314 -28
  85. package/src/be/identity.ts +79 -0
  86. package/src/be/mcp-proxy.ts +132 -0
  87. package/src/be/migrations/109_rbac_roles.sql +79 -0
  88. package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
  89. package/src/be/migrations/112_script_connections_graphql.sql +96 -0
  90. package/src/be/migrations/113_task_routing_affinity.sql +9 -0
  91. package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
  92. package/src/be/modelsdev-cache.json +135 -0
  93. package/src/be/oauth-credential-bindings.ts +35 -0
  94. package/src/be/oauth-refresh-sweep.ts +159 -0
  95. package/src/be/rbac-audit.ts +52 -13
  96. package/src/be/rbac-roles.ts +424 -0
  97. package/src/be/script-connections.ts +459 -40
  98. package/src/be/script-credential-broker.ts +29 -3
  99. package/src/be/scripts/typecheck.ts +51 -7
  100. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
  101. package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
  102. package/src/be/users.ts +24 -0
  103. package/src/cli.tsx +17 -0
  104. package/src/github/handlers.ts +10 -9
  105. package/src/github/templates.ts +45 -9
  106. package/src/gitlab/handlers.ts +20 -8
  107. package/src/gitlab/templates.ts +18 -6
  108. package/src/heartbeat/heartbeat.ts +143 -21
  109. package/src/http/all-routes.ts +3 -0
  110. package/src/http/approval-requests.ts +12 -0
  111. package/src/http/config.ts +44 -10
  112. package/src/http/core.ts +30 -3
  113. package/src/http/favorites.ts +1 -5
  114. package/src/http/index.ts +25 -1
  115. package/src/http/mcp-oauth.ts +2 -0
  116. package/src/http/mcp-servers.ts +44 -3
  117. package/src/http/oauth-generic.ts +105 -0
  118. package/src/http/poll.ts +20 -3
  119. package/src/http/script-connection-proxy.ts +116 -0
  120. package/src/http/script-connections.ts +1657 -0
  121. package/src/http/scripts.ts +27 -7
  122. package/src/http/skills.ts +11 -0
  123. package/src/http/tasks.ts +27 -13
  124. package/src/http/x.ts +6 -2
  125. package/src/integrations/kapso/inbound.ts +23 -6
  126. package/src/jira/sync.ts +111 -17
  127. package/src/jira/templates.ts +10 -2
  128. package/src/mcp-client/http-client.ts +194 -0
  129. package/src/oauth/app-validation.ts +16 -0
  130. package/src/oauth/ensure-token.ts +69 -14
  131. package/src/oauth/mcp-wrapper.ts +15 -0
  132. package/src/oauth/wrapper.ts +48 -20
  133. package/src/prompts/session-templates.ts +3 -3
  134. package/src/providers/codex-models.ts +100 -30
  135. package/src/providers/modelsdev-reasoning.json +51 -0
  136. package/src/providers/pi-mono-mcp-client.ts +2 -133
  137. package/src/providers/reasoning-effort.ts +11 -5
  138. package/src/rbac/admission.ts +68 -0
  139. package/src/rbac/index.ts +2 -0
  140. package/src/rbac/legacy-policy.ts +12 -0
  141. package/src/rbac/permissions.ts +48 -0
  142. package/src/scheduler/scheduler.ts +9 -1
  143. package/src/scripts-runtime/api-client.ts +151 -19
  144. package/src/scripts-runtime/api-types.ts +38 -6
  145. package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
  146. package/src/scripts-runtime/credential-broker/types.ts +8 -0
  147. package/src/scripts-runtime/ctx.ts +11 -1
  148. package/src/scripts-runtime/eval-harness.ts +5 -1
  149. package/src/scripts-runtime/executors/types.ts +2 -1
  150. package/src/scripts-runtime/loader.ts +3 -1
  151. package/src/scripts-runtime/mcp-client.ts +87 -0
  152. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  153. package/src/scripts-runtime/types/stdlib.d.ts +6 -0
  154. package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
  155. package/src/server-user.ts +114 -59
  156. package/src/server.ts +12 -0
  157. package/src/slack/assistant.ts +14 -6
  158. package/src/slack/enrich.ts +17 -0
  159. package/src/slack/handlers.ts +8 -22
  160. package/src/slack/responses.ts +91 -1
  161. package/src/slack/thread-buffer.ts +6 -3
  162. package/src/slack/watcher.ts +9 -6
  163. package/src/tasks/worker-follow-up.ts +100 -3
  164. package/src/tests/agentmail-handlers.test.ts +32 -3
  165. package/src/tests/approval-requests.test.ts +61 -0
  166. package/src/tests/audit-user.test.ts +47 -1
  167. package/src/tests/codex-adapter.test.ts +46 -23
  168. package/src/tests/credential-broker.test.ts +5 -2
  169. package/src/tests/delete-page-tool.test.ts +197 -0
  170. package/src/tests/github-handlers.test.ts +28 -2
  171. package/src/tests/gitlab-handlers.test.ts +125 -0
  172. package/src/tests/heartbeat.test.ts +48 -3
  173. package/src/tests/identity.test.ts +117 -0
  174. package/src/tests/jira-sync.test.ts +99 -1
  175. package/src/tests/kapso-inbound.test.ts +7 -0
  176. package/src/tests/mcp-tools-user.test.ts +60 -6
  177. package/src/tests/model-control.test.ts +8 -2
  178. package/src/tests/oauth-credential-bindings.test.ts +490 -0
  179. package/src/tests/oauth-refresh-sweep.test.ts +160 -0
  180. package/src/tests/pool-affinity.test.ts +401 -0
  181. package/src/tests/prompt-template-remaining.test.ts +2 -2
  182. package/src/tests/rbac-admission-e2e.test.ts +386 -0
  183. package/src/tests/rbac-admission.test.ts +492 -0
  184. package/src/tests/rbac-e2e-helpers.ts +3 -0
  185. package/src/tests/rbac-engine.test.ts +50 -0
  186. package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
  187. package/src/tests/rbac-roles.test.ts +328 -0
  188. package/src/tests/reasoning-effort.test.ts +15 -2
  189. package/src/tests/schedule-target-type.test.ts +50 -0
  190. package/src/tests/scheduled-tasks.test.ts +40 -0
  191. package/src/tests/script-apis-mcp.test.ts +37 -13
  192. package/src/tests/script-connections-http.test.ts +1158 -0
  193. package/src/tests/script-connections-mcp.test.ts +894 -0
  194. package/src/tests/script-connections.test.ts +754 -6
  195. package/src/tests/scripts-external-api.test.ts +49 -1
  196. package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
  197. package/src/tests/scripts-runtime.test.ts +291 -0
  198. package/src/tests/seed-scripts.test.ts +36 -0
  199. package/src/tests/slack-identity-resolution.test.ts +37 -3
  200. package/src/tests/slack-inline-output.test.ts +65 -0
  201. package/src/tests/slack-thread-buffer.test.ts +28 -0
  202. package/src/tests/tool-annotations.test.ts +1 -0
  203. package/src/tests/user-token-rest-auth.test.ts +140 -1
  204. package/src/tests/workflow-swarm-script.test.ts +44 -0
  205. package/src/tests/workflow-triggers-v2.test.ts +300 -1
  206. package/src/tools/credential-bindings/tool.ts +277 -18
  207. package/src/tools/delete-page.ts +145 -0
  208. package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
  209. package/src/tools/request-human-input.ts +2 -0
  210. package/src/tools/resolve-user.ts +77 -27
  211. package/src/tools/script-apis.ts +62 -3
  212. package/src/tools/script-connections/tool.ts +275 -31
  213. package/src/tools/send-task.ts +15 -1
  214. package/src/tools/task-action.ts +34 -2
  215. package/src/tools/templates.ts +53 -1
  216. package/src/tools/tool-config.ts +2 -1
  217. package/src/tools/utils.ts +2 -0
  218. package/src/tools/workflows/create-workflow.ts +8 -1
  219. package/src/tools/workflows/update-workflow.ts +11 -2
  220. package/src/types.ts +90 -11
  221. package/src/utils/scoped-resource.ts +25 -0
  222. package/src/workflows/executors/human-in-the-loop.ts +1 -0
  223. package/src/workflows/executors/swarm-script.ts +7 -1
  224. package/src/workflows/triggers.ts +146 -18
  225. package/templates/skills/swarm-scripts/content.md +33 -8
  226. package/dist/cli-bgef578c.js +0 -771
  227. package/dist/cli-c9mtm09b.js +0 -43
  228. package/dist/cli-ceewqajw.js +0 -1698
  229. package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
  230. package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
  231. package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
  232. package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
  233. package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
  234. package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
  235. package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
  236. package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
  237. package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
@@ -10,12 +10,13 @@ import {
10
10
  maskSecrets,
11
11
  upsertSwarmConfig,
12
12
  } from "../be/db";
13
+ import { getUserGrant } from "../be/rbac-roles";
13
14
  import {
14
15
  isReservedConfigKey,
15
16
  reservedKeyError,
16
17
  validateConfigValue,
17
18
  } from "../be/swarm-config-guard";
18
- import { can, type PermissionVerb } from "../rbac";
19
+ import { can, isRbacEnabled, type PermissionVerb } from "../rbac";
19
20
  import { getRequestAuth } from "../utils/request-auth-context";
20
21
  import { registerVolatileSecret } from "../utils/secret-scrubber";
21
22
  import { reloadGlobalConfigsAndIntegrations, scheduleIntegrationsReload } from "./core";
@@ -23,6 +24,8 @@ import { route } from "./route-def";
23
24
  import { json, jsonError } from "./utils";
24
25
 
25
26
  const MAX_ENV_PRESENCE_KEYS = 200;
27
+ const SECRETS_FORCE_MASK_NOTE =
28
+ " (secret values masked: reading unmasked secrets requires the lead agent)";
26
29
 
27
30
  // Config keys the API server owns for its own use and must NEVER hand out over
28
31
  // HTTP — workers/entrypoints read config to build their env, and the agent-fs
@@ -40,6 +43,25 @@ function singleHeader(req: IncomingMessage, name: string): string | undefined {
40
43
  return Array.isArray(raw) ? raw[0] : raw;
41
44
  }
42
45
 
46
+ function userMayReadSecrets(req: IncomingMessage): boolean {
47
+ const auth = getRequestAuth(req);
48
+ if (auth?.kind === "operator") return true;
49
+ // Agent/no-user HTTP reads keep their pre-RBAC behavior; MCP get-config has
50
+ // the per-agent config.read.secrets gate.
51
+ if (auth?.kind !== "user") return true;
52
+ if (!isRbacEnabled()) return true;
53
+
54
+ const grant = getUserGrant(auth.userId);
55
+ return grant.grantsAll || grant.verbs.has("config.read.secrets");
56
+ }
57
+
58
+ function resolveSecretsRead(req: IncomingMessage, includeSecrets: boolean) {
59
+ if (!includeSecrets || userMayReadSecrets(req)) {
60
+ return { effectiveIncludeSecrets: includeSecrets, secretsNote: "" };
61
+ }
62
+ return { effectiveIncludeSecrets: false, secretsNote: SECRETS_FORCE_MASK_NOTE };
63
+ }
64
+
43
65
  /**
44
66
  * Gate a config write/delete over HTTP (DES-445 follow-up).
45
67
  *
@@ -213,18 +235,22 @@ export async function handleConfig(
213
235
  const parsed = await getResolvedConfigRoute.parse(req, res, pathSegments, queryParams);
214
236
  if (!parsed) return true;
215
237
  const includeSecrets = parsed.query.includeSecrets === "true";
238
+ const { effectiveIncludeSecrets, secretsNote } = resolveSecretsRead(req, includeSecrets);
216
239
  const configs = stripApiOnlyKeys(
217
240
  getResolvedConfig(parsed.query.agentId || undefined, parsed.query.repoId || undefined),
218
241
  );
219
- const result = includeSecrets ? configs : maskSecrets(configs);
220
- if (includeSecrets) {
242
+ const result = effectiveIncludeSecrets ? configs : maskSecrets(configs);
243
+ if (effectiveIncludeSecrets) {
221
244
  for (const c of result) {
222
245
  if (c.isSecret && c.value) {
223
246
  registerVolatileSecret(c.value, `config:${c.key}`);
224
247
  }
225
248
  }
226
249
  }
227
- json(res, { configs: result });
250
+ json(res, {
251
+ configs: result,
252
+ ...(secretsNote ? { message: `Found ${result.length} config(s).${secretsNote}` } : {}),
253
+ });
228
254
  return true;
229
255
  }
230
256
 
@@ -271,11 +297,15 @@ export async function handleConfig(
271
297
  jsonError(res, "Config not found", 404);
272
298
  return true;
273
299
  }
274
- const singleResult = includeSecrets ? config : maskSecrets([config])[0]!;
275
- if (includeSecrets && singleResult.isSecret && singleResult.value) {
300
+ const { effectiveIncludeSecrets, secretsNote } = resolveSecretsRead(req, includeSecrets);
301
+ const singleResult = effectiveIncludeSecrets ? config : maskSecrets([config])[0]!;
302
+ if (effectiveIncludeSecrets && singleResult.isSecret && singleResult.value) {
276
303
  registerVolatileSecret(singleResult.value, `config:${singleResult.key}`);
277
304
  }
278
- json(res, singleResult);
305
+ json(res, {
306
+ ...singleResult,
307
+ ...(secretsNote ? { message: `Found 1 config(s).${secretsNote}` } : {}),
308
+ });
279
309
  return true;
280
310
  }
281
311
 
@@ -283,21 +313,25 @@ export async function handleConfig(
283
313
  const parsed = await listConfig.parse(req, res, pathSegments, queryParams);
284
314
  if (!parsed) return true;
285
315
  const includeSecrets = parsed.query.includeSecrets === "true";
316
+ const { effectiveIncludeSecrets, secretsNote } = resolveSecretsRead(req, includeSecrets);
286
317
  const configs = stripApiOnlyKeys(
287
318
  getSwarmConfigs({
288
319
  scope: parsed.query.scope || undefined,
289
320
  scopeId: parsed.query.scopeId || undefined,
290
321
  }),
291
322
  );
292
- const listResult = includeSecrets ? configs : maskSecrets(configs);
293
- if (includeSecrets) {
323
+ const listResult = effectiveIncludeSecrets ? configs : maskSecrets(configs);
324
+ if (effectiveIncludeSecrets) {
294
325
  for (const c of listResult) {
295
326
  if (c.isSecret && c.value) {
296
327
  registerVolatileSecret(c.value, `config:${c.key}`);
297
328
  }
298
329
  }
299
330
  }
300
- json(res, { configs: listResult });
331
+ json(res, {
332
+ configs: listResult,
333
+ ...(secretsNote ? { message: `Found ${listResult.length} config(s).${secretsNote}` } : {}),
334
+ });
301
335
  return true;
302
336
  }
303
337
 
package/src/http/core.ts CHANGED
@@ -10,17 +10,20 @@ import {
10
10
  shouldBlockPolling,
11
11
  updateAgentStatus,
12
12
  } from "../be/db";
13
+ import { enqueueAdmissionRow } from "../be/rbac-audit";
14
+ import { getUserGrant } from "../be/rbac-roles";
13
15
  import { initGitHub, resetGitHub } from "../github";
14
16
  import { initJira, resetJira } from "../jira";
15
17
  import { initLinear, resetLinear } from "../linear";
18
+ import { decideAdmission, isRbacEnabled } from "../rbac";
16
19
  import { startSlackApp, stopSlackApp } from "../slack";
17
20
  import type { AgentStatus } from "../types";
18
21
  import { setRequestAuth } from "../utils/request-auth-context";
19
22
  import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
20
23
  import { resolveHttpRequestAuth } from "./auth";
21
24
  import { generateOpenApiSpec, SCALAR_HTML } from "./openapi";
22
- import { isPublicRoute } from "./route-def";
23
- import { agentWithCapacity, getPathSegments, parseQueryParams } from "./utils";
25
+ import { findRoute, isPublicRoute } from "./route-def";
26
+ import { agentWithCapacity, getPathSegments, jsonError, parseQueryParams } from "./utils";
24
27
 
25
28
  /**
26
29
  * Load global swarm_config entries into process.env.
@@ -243,12 +246,13 @@ export async function handleCore(
243
246
  // either the global swarm key or an active user-bound `aswt_` token.
244
247
  const pathSegments = getPathSegments(req.url || "");
245
248
  const isUserMcpRoute = req.url === "/mcp-user";
249
+ let auth = null as ReturnType<typeof resolveHttpRequestAuth>;
246
250
  // `/mcp-user` runs its own `aswt_`-token auth in `handleMcpUser`; the swarm
247
251
  // API key must not gate it.
248
252
  if (isUserMcpRoute || isPublicRoute(req.method, pathSegments)) {
249
253
  setRequestAuth(req, null);
250
254
  } else {
251
- const auth = resolveHttpRequestAuth(req, apiKey);
255
+ auth = resolveHttpRequestAuth(req, apiKey);
252
256
 
253
257
  if (!auth) {
254
258
  setRequestAuth(req, null);
@@ -259,6 +263,29 @@ export async function handleCore(
259
263
  setRequestAuth(req, auth);
260
264
  }
261
265
 
266
+ if (auth?.kind === "user" && isRbacEnabled()) {
267
+ const grant = getUserGrant(auth.userId);
268
+ if (!grant.grantsAll) {
269
+ const def = findRoute(req.method, pathSegments);
270
+ const decision = decideAdmission({
271
+ method: req.method ?? "",
272
+ rbac: def?.rbac,
273
+ routeKnown: def !== undefined,
274
+ grant,
275
+ });
276
+ enqueueAdmissionRow({
277
+ userId: auth.userId,
278
+ decision,
279
+ method: req.method,
280
+ route: def?.path ?? pathSegments.join("/"),
281
+ });
282
+ if (!decision.allow) {
283
+ jsonError(res, `Forbidden: ${decision.reason}`, 403);
284
+ return true;
285
+ }
286
+ }
287
+ }
288
+
262
289
  // POST /internal/reload-config — re-read swarm_config into process.env and re-init integrations
263
290
  if (req.method === "POST" && req.url === "/internal/reload-config") {
264
291
  try {
@@ -28,11 +28,7 @@ const putFavorite = route({
28
28
  pattern: ["api", "favorites"],
29
29
  summary: "Set favorite state for an item",
30
30
  tags: ["Favorites"],
31
- // Self-scoped write: the row is keyed to the authenticated user
32
- // (resolveHttpAuditUserId), never a body-supplied id, so there is no
33
- // cross-principal authorization to gate — a user can only favorite for
34
- // themselves.
35
- rbac: { ungated: "self-scoped to the authenticated user; no cross-principal access" },
31
+ rbac: { permission: "favorite.write.own" },
36
32
  body: z.object({
37
33
  itemType: FavoriteItemTypeSchema,
38
34
  itemId: z.string().min(1),
package/src/http/index.ts CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  withRemoteContext,
30
30
  withSpanContext,
31
31
  } from "../otel";
32
- import { clearAuditSink, setAuditSink } from "../rbac";
32
+ import { clearAuditSink, isRbacEnabled, setAuditSink } from "../rbac";
33
33
  import { startScriptRunSupervisor, stopScriptRunSupervisor } from "../script-workflows/supervisor";
34
34
  import { getServerSessionsProcessed } from "../server-runtime-counters";
35
35
  import { startSlackApp, stopSlackApp } from "../slack";
@@ -69,6 +69,7 @@ import { handleMcpServers } from "./mcp-servers";
69
69
  import { closeIdleMcpUserTransports, handleMcpUser } from "./mcp-user";
70
70
  import { handleMemory, startMemoryGc, stopMemoryGc } from "./memory";
71
71
  import { handleMetrics } from "./metrics";
72
+ import { handleGenericOAuth } from "./oauth-generic";
72
73
  import { handleOAuthLocks } from "./oauth-locks";
73
74
  import { handlePageProxy } from "./page-proxy";
74
75
  import { handlePages } from "./pages";
@@ -79,6 +80,8 @@ import { handlePromptTemplates } from "./prompt-templates";
79
80
  import { handleRepos } from "./repos";
80
81
  import { describeRequestRoute } from "./route-def";
81
82
  import { handleSchedules } from "./schedules";
83
+ import { handleScriptConnectionProxy } from "./script-connection-proxy";
84
+ import { handleScriptConnections } from "./script-connections";
82
85
  import { handleScriptRuns } from "./script-runs";
83
86
  import { handleScripts } from "./scripts";
84
87
  import { handleSessionData } from "./session-data";
@@ -323,6 +326,8 @@ const httpServer = createHttpServer(async (req, res) => {
323
326
  () => handleMetrics(req, res, pathSegments, queryParams, myAgentId),
324
327
  () => handleRepos(req, res, pathSegments, queryParams),
325
328
  () => handleSkills(req, res, pathSegments, queryParams, myAgentId),
329
+ () => handleScriptConnections(req, res, pathSegments, queryParams, myAgentId),
330
+ () => handleScriptConnectionProxy(req, res, pathSegments, queryParams, myAgentId),
326
331
  () => handleScriptRuns(req, res, pathSegments, queryParams, myAgentId),
327
332
  () => handleScripts(req, res, pathSegments, queryParams, myAgentId),
328
333
  () => handleX(req, res, pathSegments),
@@ -331,6 +336,7 @@ const httpServer = createHttpServer(async (req, res) => {
331
336
  () => handleMcpOAuth(req, res, pathSegments, queryParams),
332
337
  () => handleMemory(req, res, pathSegments, myAgentId),
333
338
  () => handleOAuthLocks(req, res, pathSegments, queryParams),
339
+ () => handleGenericOAuth(req, res, pathSegments, queryParams),
334
340
  () => handleCodexOAuthKeepWarm(req, res, pathSegments),
335
341
  () => handlePagesPublic(req, res, pathSegments, queryParams),
336
342
  () => handlePageProxy(req, res),
@@ -504,6 +510,16 @@ try {
504
510
  console.error("[startup] Failed to seed pricing rows:", err);
505
511
  }
506
512
 
513
+ try {
514
+ const { ensureRbacSeedsSynced } = await import("../be/rbac-roles");
515
+ ensureRbacSeedsSynced();
516
+ } catch (err) {
517
+ console.error("[startup] Failed to sync RBAC seed rows:", err);
518
+ // RBAC flag-on must fail closed; flag-off deployments should not be bricked
519
+ // by role-catalog drift for a disabled security feature.
520
+ if (isRbacEnabled()) throw err;
521
+ }
522
+
507
523
  // Seed the built-in entity catalog (scripts today; more kinds later) so
508
524
  // `script-search` & co. return useful hits from a fresh DB. Idempotent and
509
525
  // version-aware: a pristine entity updates when its source changes, a
@@ -611,6 +627,14 @@ httpServer
611
627
  startOAuthKeepalive();
612
628
  }
613
629
 
630
+ // Start generic OAuth token refresh sweep (all oauth_apps providers,
631
+ // 15-min tick, first run ~1 min after boot). Complements the tracker-only
632
+ // keepalive above — see src/be/oauth-refresh-sweep.ts.
633
+ if (process.env.OAUTH_REFRESH_SWEEP_DISABLE !== "true") {
634
+ const { startOAuthRefreshSweep } = await import("../be/oauth-refresh-sweep");
635
+ startOAuthRefreshSweep();
636
+ }
637
+
614
638
  // Start MCP OAuth pending-session garbage collector (5-min tick)
615
639
  startMcpOAuthPendingGc();
616
640
 
@@ -192,6 +192,7 @@ const authorizeRoute = route({
192
192
  userId: z.string().optional(),
193
193
  scopes: z.string().optional(),
194
194
  }),
195
+ rbac: { permission: "mcp-oauth.authorize.any" },
195
196
  responses: {
196
197
  302: { description: "Redirect to authorization server" },
197
198
  400: { description: "MCP has no URL / does not require OAuth" },
@@ -213,6 +214,7 @@ const authorizeUrlRoute = route({
213
214
  userId: z.string().optional(),
214
215
  scopes: z.string().optional(),
215
216
  }),
217
+ rbac: { permission: "mcp-oauth.authorize.any" },
216
218
  responses: {
217
219
  200: { description: "{ providerUrl: string }" },
218
220
  400: { description: "MCP has no URL / does not require OAuth" },
@@ -11,7 +11,11 @@ import {
11
11
  uninstallMcpServer,
12
12
  updateMcpServer,
13
13
  } from "../be/db";
14
+ import { enqueueAdmissionRow } from "../be/rbac-audit";
15
+ import { getUserGrant } from "../be/rbac-roles";
14
16
  import { ensureMcpToken } from "../oauth/ensure-mcp-token";
17
+ import { isRbacEnabled, type PermissionVerb } from "../rbac";
18
+ import { getRequestAuth } from "../utils/request-auth-context";
15
19
  import { route } from "./route-def";
16
20
  import { json, jsonError } from "./utils";
17
21
 
@@ -74,6 +78,7 @@ const createMcpServerRoute = route({
74
78
  201: { description: "MCP server created" },
75
79
  400: { description: "Validation error" },
76
80
  },
81
+ rbac: { permission: "mcp-server.create.swarm" },
77
82
  });
78
83
 
79
84
  const updateMcpServerRoute = route({
@@ -89,6 +94,7 @@ const updateMcpServerRoute = route({
89
94
  200: { description: "MCP server updated" },
90
95
  404: { description: "MCP server not found" },
91
96
  },
97
+ rbac: { permission: "mcp-server.update.any" },
92
98
  });
93
99
 
94
100
  const deleteMcpServerRoute = route({
@@ -103,6 +109,7 @@ const deleteMcpServerRoute = route({
103
109
  200: { description: "MCP server deleted" },
104
110
  404: { description: "MCP server not found" },
105
111
  },
112
+ rbac: { permission: "mcp-server.delete.any" },
106
113
  });
107
114
 
108
115
  const installMcpServerRoute = route({
@@ -120,6 +127,7 @@ const installMcpServerRoute = route({
120
127
  200: { description: "MCP server installed" },
121
128
  404: { description: "MCP server not found" },
122
129
  },
130
+ rbac: { permission: "mcp-server.install.any" },
123
131
  });
124
132
 
125
133
  const uninstallMcpServerRoute = route({
@@ -133,6 +141,7 @@ const uninstallMcpServerRoute = route({
133
141
  responses: {
134
142
  200: { description: "MCP server uninstalled" },
135
143
  },
144
+ rbac: { permission: "mcp-server.uninstall.any" },
136
145
  });
137
146
 
138
147
  const getAgentMcpServersRoute = route({
@@ -153,6 +162,30 @@ const getAgentMcpServersRoute = route({
153
162
 
154
163
  // ─── Handler ─────────────────────────────────────────────────────────────────
155
164
 
165
+ function canResolveMcpSecretsForHttpUser(req: IncomingMessage): boolean {
166
+ const auth = getRequestAuth(req);
167
+ if (auth?.kind !== "user") return true;
168
+ if (!isRbacEnabled()) return true;
169
+
170
+ const verb: PermissionVerb = "mcp-server.read.secrets";
171
+ const grant = getUserGrant(auth.userId);
172
+ const decision =
173
+ grant.grantsAll || grant.verbs.has(verb)
174
+ ? ({ allow: true, verb } as const)
175
+ : ({
176
+ allow: false,
177
+ reason: `admission: missing permission '${verb}'`,
178
+ verb,
179
+ } as const);
180
+ enqueueAdmissionRow({
181
+ userId: auth.userId,
182
+ decision,
183
+ method: req.method,
184
+ route: getAgentMcpServersRoute.def.path,
185
+ });
186
+ return decision.allow;
187
+ }
188
+
156
189
  export async function handleMcpServers(
157
190
  req: IncomingMessage,
158
191
  res: ServerResponse,
@@ -168,6 +201,11 @@ export async function handleMcpServers(
168
201
  const resolveSecrets = parsed.query.resolveSecrets === "true";
169
202
 
170
203
  if (resolveSecrets) {
204
+ if (!canResolveMcpSecretsForHttpUser(req)) {
205
+ jsonError(res, "Forbidden: admission: missing permission 'mcp-server.read.secrets'", 403);
206
+ return true;
207
+ }
208
+
171
209
  const configs = getResolvedConfig(parsed.params.id);
172
210
  const configMap = new Map(configs.map((c) => [c.key, c.value]));
173
211
 
@@ -408,12 +446,15 @@ export async function handleMcpServers(
408
446
  const parsed = await deleteMcpServerRoute.parse(req, res, pathSegments, queryParams);
409
447
  if (!parsed) return true;
410
448
 
411
- const deleted = deleteMcpServer(parsed.params.id);
412
- if (!deleted) {
449
+ const result = deleteMcpServer(parsed.params.id);
450
+ if (!result.deleted) {
413
451
  jsonError(res, "MCP server not found", 404);
414
452
  return true;
415
453
  }
416
- json(res, { success: true });
454
+ json(res, {
455
+ success: true,
456
+ deletedScriptConnectionCount: result.deletedScriptConnectionCount,
457
+ });
417
458
  return true;
418
459
  }
419
460
 
@@ -0,0 +1,105 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { z } from "zod";
3
+ import { getOAuthProviderConfig } from "@/be/oauth-credential-bindings";
4
+ import { exchangeCode } from "@/oauth/wrapper";
5
+ import { route } from "./route-def";
6
+ import { jsonError } from "./utils";
7
+
8
+ const DEDICATED_CALLBACK_PROVIDERS = new Set(["linear"]);
9
+
10
+ const genericOAuthCallbackRoute = route({
11
+ method: "get",
12
+ path: "/api/oauth/{provider}/callback",
13
+ pattern: ["api", "oauth", null, "callback"],
14
+ operationId: "oauth_generic_callback",
15
+ summary: "Generic OAuth redirect target for script credential bindings",
16
+ tags: ["OAuth"],
17
+ auth: { apiKey: false },
18
+ params: z.object({
19
+ provider: z
20
+ .string()
21
+ .min(1)
22
+ .max(255)
23
+ .regex(/^[A-Za-z0-9_-]+$/),
24
+ }),
25
+ query: z.object({
26
+ code: z.string().optional(),
27
+ state: z.string().optional(),
28
+ error: z.string().optional(),
29
+ error_description: z.string().optional(),
30
+ }),
31
+ responses: {
32
+ 200: { description: "OAuth authorization completed" },
33
+ 400: { description: "Missing or invalid OAuth callback parameters" },
34
+ 404: { description: "OAuth app not configured" },
35
+ 409: { description: "Provider has a dedicated OAuth callback" },
36
+ 502: { description: "Token exchange failed" },
37
+ },
38
+ });
39
+
40
+ function sendAuthorizedHtml(res: ServerResponse, provider: string): void {
41
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
42
+ res.end(`<!DOCTYPE html>
43
+ <html>
44
+ <head><title>OAuth Authorized</title></head>
45
+ <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
46
+ <main style="text-align: center;">
47
+ <h1>${provider} authorized</h1>
48
+ <p>You can close this tab.</p>
49
+ </main>
50
+ </body>
51
+ </html>`);
52
+ }
53
+
54
+ export async function handleGenericOAuth(
55
+ req: IncomingMessage,
56
+ res: ServerResponse,
57
+ pathSegments: string[],
58
+ queryParams: URLSearchParams,
59
+ ): Promise<boolean> {
60
+ if (!genericOAuthCallbackRoute.match(req.method, pathSegments)) return false;
61
+
62
+ const parsed = await genericOAuthCallbackRoute.parse(req, res, pathSegments, queryParams);
63
+ if (!parsed) return true;
64
+
65
+ const { provider } = parsed.params;
66
+ if (DEDICATED_CALLBACK_PROVIDERS.has(provider)) {
67
+ jsonError(
68
+ res,
69
+ "Provider linear uses the dedicated OAuth flow at /api/trackers/linear/authorize and /api/trackers/linear/callback.",
70
+ 409,
71
+ );
72
+ return true;
73
+ }
74
+
75
+ if (parsed.query.error) {
76
+ jsonError(res, parsed.query.error_description ?? parsed.query.error, 400);
77
+ return true;
78
+ }
79
+
80
+ if (!parsed.query.code || !parsed.query.state) {
81
+ jsonError(res, "Missing code or state parameter", 400);
82
+ return true;
83
+ }
84
+
85
+ const config = getOAuthProviderConfig(provider);
86
+ if (!config) {
87
+ jsonError(res, `OAuth app ${provider} is not configured`, 404);
88
+ return true;
89
+ }
90
+
91
+ try {
92
+ await exchangeCode(config, parsed.query.code, parsed.query.state);
93
+ sendAuthorizedHtml(res, provider);
94
+ } catch (err) {
95
+ const message = err instanceof Error ? err.message : String(err);
96
+ const status = message.includes("Invalid or expired OAuth state") ? 400 : 502;
97
+ jsonError(
98
+ res,
99
+ status === 400 ? "Invalid or expired OAuth state" : `Token exchange failed: ${message}`,
100
+ status,
101
+ );
102
+ }
103
+
104
+ return true;
105
+ }
package/src/http/poll.ts CHANGED
@@ -18,13 +18,14 @@ import {
18
18
  getPendingTaskForAgent,
19
19
  getTaskAttachments,
20
20
  getTaskById,
21
- getUnassignedTaskIds,
21
+ getUnassignedTaskIdsForAgent,
22
22
  getUserById,
23
23
  hasCapacity,
24
24
  recordBudgetRefusalNotification,
25
25
  startTask,
26
26
  upsertChannelActivityCursor,
27
27
  } from "../be/db";
28
+ import { renderIdentity, resolveIdentity } from "../be/identity";
28
29
  import { fetchChannelActivity } from "../slack/channel-activity";
29
30
  import { telemetry } from "../telemetry";
30
31
  import { route } from "./route-def";
@@ -274,11 +275,19 @@ export async function handlePoll(
274
275
  agentId: myAgentId,
275
276
  });
276
277
 
277
- // Resolve requesting user if available
278
+ // Resolve requesting user if available. If the task carries a
279
+ // machine-recorded external id (today: the Slack user field) but
280
+ // no requestedByUserId, render the explicit UNKNOWN sentinel
281
+ // instead of silently omitting the section — never a substituted
282
+ // human (Rule 33 / provenance-or-silence).
278
283
  const requestedByUser = pendingTask.requestedByUserId
279
284
  ? getUserById(pendingTask.requestedByUserId)
280
285
  : undefined;
281
286
  const requestedByNotes = getRequesterNotes(requestedByUser?.notes);
287
+ const requestedByUnknownName =
288
+ !requestedByUser && pendingTask.slackUserId
289
+ ? renderIdentity(resolveIdentity("slack", pendingTask.slackUserId))
290
+ : undefined;
282
291
 
283
292
  return {
284
293
  trigger: {
@@ -297,6 +306,9 @@ export async function handlePoll(
297
306
  notes: requestedByNotes,
298
307
  },
299
308
  }),
309
+ ...(requestedByUnknownName && {
310
+ requestedBy: { name: requestedByUnknownName },
311
+ }),
300
312
  },
301
313
  };
302
314
  }
@@ -333,8 +345,13 @@ export async function handlePoll(
333
345
  // one worker wins if multiple poll simultaneously.
334
346
  // This ensures session logs are correctly associated with the real task ID
335
347
  // from the start (no reassociation needed).
348
+ //
349
+ // Routing affinity: `getUnassignedTaskIdsForAgent` (not the plain
350
+ // `getUnassignedTaskIds`) pre-filters candidates through
351
+ // `isAgentEligibleForTask`, so an ineligible task is never even
352
+ // offered to the budget gate below or the claim loop.
336
353
  if (hasCapacity(myAgentId)) {
337
- const unassignedIds = getUnassignedTaskIds(5);
354
+ const unassignedIds = getUnassignedTaskIdsForAgent(myAgentId, 5);
338
355
  // Budget admission gate (Phase 3). Pool path is workers-only —
339
356
  // per-agent budgets matter most here, but we still check global.
340
357
  // Only run the gate when there's at least one candidate task; an