@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
@@ -1,6 +1,15 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
2
3
  import * as z from "zod";
3
4
  import pkg from "../package.json";
5
+ import { enqueueAdmissionRow } from "./be/rbac-audit";
6
+ import { getUserGrant } from "./be/rbac-roles";
7
+ import {
8
+ type AdmissionRbac,
9
+ decideToolAdmission,
10
+ isRbacEnabled,
11
+ type PermissionVerb,
12
+ } from "./rbac";
4
13
  import {
5
14
  cancelTaskHandler,
6
15
  cancelTaskInputSchema,
@@ -22,6 +31,11 @@ import { userCtx } from "./tools/task-tool-ctx";
22
31
  import { createToolRegistrar } from "./tools/utils";
23
32
  import { ModelTierSchema, type User } from "./types";
24
33
 
34
+ type UserToolAdmissionConfig = {
35
+ annotations?: ToolAnnotations;
36
+ rbac?: AdmissionRbac;
37
+ };
38
+
25
39
  const userSendTaskInputSchema = z.object({
26
40
  task: z.string().min(1).describe("The task description to send."),
27
41
  taskType: z.string().max(50).optional().describe("Task type (e.g., 'bug', 'feature', 'review')."),
@@ -38,6 +52,40 @@ const userSendTaskInputSchema = z.object({
38
52
  ),
39
53
  });
40
54
 
55
+ function permission(verb: PermissionVerb): AdmissionRbac {
56
+ return { permission: verb };
57
+ }
58
+
59
+ async function maybeDenyUserToolAdmission(
60
+ user: User,
61
+ toolName: string,
62
+ config: UserToolAdmissionConfig,
63
+ ): Promise<CallToolResult | undefined> {
64
+ if (!isRbacEnabled()) return undefined;
65
+
66
+ const grant = getUserGrant(user.id);
67
+ if (grant.grantsAll) return undefined;
68
+
69
+ const decision = decideToolAdmission({
70
+ rbac: config.rbac,
71
+ readOnly: config.annotations?.readOnlyHint ?? false,
72
+ grant,
73
+ });
74
+ enqueueAdmissionRow({
75
+ userId: user.id,
76
+ decision,
77
+ source: "mcp",
78
+ toolName,
79
+ });
80
+
81
+ if (decision.allow) return undefined;
82
+
83
+ return {
84
+ isError: true,
85
+ content: [{ type: "text", text: `Forbidden: ${decision.reason}` }],
86
+ };
87
+ }
88
+
41
89
  export function createUserServer(user: User): McpServer {
42
90
  const server = new McpServer(
43
91
  {
@@ -54,69 +102,76 @@ export function createUserServer(user: User): McpServer {
54
102
 
55
103
  const registerTool = createToolRegistrar(server);
56
104
 
57
- registerTool(
58
- "send-task",
59
- {
60
- title: "Send a task",
61
- annotations: { destructiveHint: false },
62
- description: "Creates an unassigned task requested by the authenticated user.",
63
- inputSchema: userSendTaskInputSchema,
64
- outputSchema: sendTaskOutputSchema,
65
- },
66
- async (args, info, _meta) =>
67
- sendTaskHandler(userCtx(user, info.sessionId), {
68
- offerMode: false,
69
- allowDuplicate: false,
70
- ...args,
71
- }),
72
- );
105
+ const sendTaskConfig = {
106
+ title: "Send a task",
107
+ annotations: { destructiveHint: false },
108
+ rbac: permission("task.create.own"),
109
+ description: "Creates an unassigned task requested by the authenticated user.",
110
+ inputSchema: userSendTaskInputSchema,
111
+ outputSchema: sendTaskOutputSchema,
112
+ };
113
+ registerTool("send-task", sendTaskConfig, async (args, info, _meta) => {
114
+ const denied = await maybeDenyUserToolAdmission(user, "send-task", sendTaskConfig);
115
+ if (denied) return denied;
116
+ return sendTaskHandler(userCtx(user, info.sessionId), {
117
+ offerMode: false,
118
+ allowDuplicate: false,
119
+ ...args,
120
+ });
121
+ });
73
122
 
74
- registerTool(
75
- "get-tasks",
76
- {
77
- title: "Get tasks",
78
- description: "Returns tasks requested by the authenticated user.",
79
- annotations: { readOnlyHint: true },
80
- inputSchema: getTasksInputSchema,
81
- outputSchema: getTasksOutputSchema,
82
- },
83
- async (args, info, _meta) => getTasksHandler(userCtx(user, info.sessionId), args),
84
- );
123
+ const getTasksConfig = {
124
+ title: "Get tasks",
125
+ description: "Returns tasks requested by the authenticated user.",
126
+ annotations: { readOnlyHint: true },
127
+ inputSchema: getTasksInputSchema,
128
+ outputSchema: getTasksOutputSchema,
129
+ };
130
+ registerTool("get-tasks", getTasksConfig, async (args, info, _meta) => {
131
+ const denied = await maybeDenyUserToolAdmission(user, "get-tasks", getTasksConfig);
132
+ if (denied) return denied;
133
+ return getTasksHandler(userCtx(user, info.sessionId), args);
134
+ });
85
135
 
86
- registerTool(
87
- "get-task-details",
88
- {
89
- title: "Get task details",
90
- description: "Returns detailed information about one of your tasks.",
91
- annotations: { readOnlyHint: true },
92
- inputSchema: getTaskDetailsInputSchema,
93
- outputSchema: getTaskDetailsOutputSchema,
94
- },
95
- async (args, info, _meta) => getTaskDetailsHandler(userCtx(user, info.sessionId), args),
96
- );
136
+ const getTaskDetailsConfig = {
137
+ title: "Get task details",
138
+ description: "Returns detailed information about one of your tasks.",
139
+ annotations: { readOnlyHint: true },
140
+ inputSchema: getTaskDetailsInputSchema,
141
+ outputSchema: getTaskDetailsOutputSchema,
142
+ };
143
+ registerTool("get-task-details", getTaskDetailsConfig, async (args, info, _meta) => {
144
+ const denied = await maybeDenyUserToolAdmission(user, "get-task-details", getTaskDetailsConfig);
145
+ if (denied) return denied;
146
+ return getTaskDetailsHandler(userCtx(user, info.sessionId), args);
147
+ });
97
148
 
98
- registerTool(
99
- "cancel-task",
100
- {
101
- title: "Cancel Task",
102
- description: "Cancel one of your pending or in-progress tasks.",
103
- annotations: { destructiveHint: true },
104
- inputSchema: cancelTaskInputSchema,
105
- outputSchema: cancelTaskOutputSchema,
106
- },
107
- async (args, info, _meta) => cancelTaskHandler(userCtx(user, info.sessionId), args),
108
- );
149
+ const cancelTaskConfig = {
150
+ title: "Cancel Task",
151
+ description: "Cancel one of your pending or in-progress tasks.",
152
+ annotations: { destructiveHint: true },
153
+ rbac: permission("task.cancel.own"),
154
+ inputSchema: cancelTaskInputSchema,
155
+ outputSchema: cancelTaskOutputSchema,
156
+ };
157
+ registerTool("cancel-task", cancelTaskConfig, async (args, info, _meta) => {
158
+ const denied = await maybeDenyUserToolAdmission(user, "cancel-task", cancelTaskConfig);
159
+ if (denied) return denied;
160
+ return cancelTaskHandler(userCtx(user, info.sessionId), args);
161
+ });
109
162
 
110
- registerTool(
111
- "task-action",
112
- {
113
- title: "Task Pool Action",
114
- description: "Move one of your tasks to or from backlog.",
115
- inputSchema: taskActionInputSchema,
116
- outputSchema: taskActionOutputSchema,
117
- },
118
- async (args, info, _meta) => taskActionHandler(userCtx(user, info.sessionId), args),
119
- );
163
+ const taskActionConfig = {
164
+ title: "Task Pool Action",
165
+ description: "Move one of your tasks to or from backlog.",
166
+ rbac: permission("task.action.own"),
167
+ inputSchema: taskActionInputSchema,
168
+ outputSchema: taskActionOutputSchema,
169
+ };
170
+ registerTool("task-action", taskActionConfig, async (args, info, _meta) => {
171
+ const denied = await maybeDenyUserToolAdmission(user, "task-action", taskActionConfig);
172
+ if (denied) return denied;
173
+ return taskActionHandler(userCtx(user, info.sessionId), args);
174
+ });
120
175
 
121
176
  return server;
122
177
  }
package/src/server.ts CHANGED
@@ -2,8 +2,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import pkg from "../package.json";
3
3
  import { initDb } from "./be/db";
4
4
  import { startPricingRefreshLoop } from "./be/pricing-refresh";
5
+ import { ensureRbacSeedsSynced } from "./be/rbac-roles";
5
6
  import { seedPricingFromModelsDev } from "./be/seed-pricing";
6
7
  import { registerGithubTaskReactions } from "./github/task-reactions";
8
+ import { isRbacEnabled } from "./rbac";
7
9
  import { registerCancelTaskTool } from "./tools/cancel-task";
8
10
  import { registerContextDiffTool } from "./tools/context-diff";
9
11
  import { registerContextHistoryTool } from "./tools/context-history";
@@ -13,6 +15,7 @@ import { registerCreatePageTool } from "./tools/create-page";
13
15
  import { registerCredentialBindingsTool } from "./tools/credential-bindings";
14
16
  import { registerDbQueryTool } from "./tools/db-query";
15
17
  import { registerDeleteChannelTool } from "./tools/delete-channel";
18
+ import { registerDeletePageTool } from "./tools/delete-page";
16
19
  import { registerGetMetricsTool } from "./tools/get-metrics";
17
20
  import { registerGetSwarmTool } from "./tools/get-swarm";
18
21
  import { registerGetTaskDetailsTool } from "./tools/get-task-details";
@@ -182,6 +185,14 @@ export function createServer() {
182
185
  // and the manual-override constants for runtime-fee / ACU pricing.
183
186
  seedPricingFromModelsDev();
184
187
  startPricingRefreshLoop();
188
+ try {
189
+ ensureRbacSeedsSynced();
190
+ } catch (err) {
191
+ console.error("[startup] Failed to sync RBAC seed rows:", err);
192
+ // RBAC flag-on must fail closed; flag-off deployments should not be bricked
193
+ // by role-catalog drift for a disabled security feature.
194
+ if (isRbacEnabled()) throw err;
195
+ }
185
196
 
186
197
  // Subscribe API-side integrations to task-lifecycle events. Idempotent.
187
198
  // (Inverts the old be/db → github/task-reactions import; see cycle-break #4.)
@@ -366,6 +377,7 @@ export function createServer() {
366
377
  // `CAPABILITIES=...` env without `pages`.
367
378
  if (hasCapability("pages")) {
368
379
  registerCreatePageTool(server);
380
+ registerDeletePageTool(server);
369
381
  }
370
382
 
371
383
  if (hasCapability("metrics")) {
@@ -3,7 +3,7 @@ import { getAgentWorkingOnThread, getLeadAgent, getMostRecentTaskInThread } from
3
3
  import { resolveTemplate } from "../prompts/resolver";
4
4
  import { slackContextKey } from "../tasks/context-key";
5
5
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
6
- import { resolveSlackUserId } from "./enrich";
6
+ import { resolveSlackUserId, rewriteSlackMentions } from "./enrich";
7
7
  import { wasEventSeen } from "./event-dedup";
8
8
  import { hasOtherUserMention } from "./router";
9
9
  import { bufferThreadMessage } from "./thread-buffer";
@@ -76,6 +76,11 @@ export function createAssistant(): Assistant {
76
76
  const channelId = message.channel;
77
77
  const messageText = (msg.text as string) || "";
78
78
  const userId = (msg.user as string) || "";
79
+ // Any in-body `<@U…>` mention the requester typed is rewritten via
80
+ // the identity primitive before it reaches agent-visible task text —
81
+ // never a raw Slack ID. Bot-mention routing checks below use the
82
+ // raw `messageText`, not this rendered copy.
83
+ const renderedMessageText = rewriteSlackMentions(messageText);
79
84
 
80
85
  // Resolve the bot's own Slack user ID (cached after first call) so we can
81
86
  // check whether this message is actually addressed to us.
@@ -123,7 +128,7 @@ export function createAssistant(): Assistant {
123
128
 
124
129
  // Otherwise, create a follow-up task for the working agent
125
130
  const latestTask = getMostRecentTaskInThread(channelId, threadTs);
126
- createTaskWithSiblingAwareness(messageText, {
131
+ createTaskWithSiblingAwareness(renderedMessageText, {
127
132
  agentId: workingAgent.id,
128
133
  source: "slack",
129
134
  slackChannelId: channelId,
@@ -141,8 +146,11 @@ export function createAssistant(): Assistant {
141
146
  // 2. First message in thread — create new task for lead
142
147
  await safeSetStatus("Processing your request...");
143
148
 
144
- if (messageText) {
145
- const title = messageText.length > 50 ? `${messageText.slice(0, 47)}...` : messageText;
149
+ if (renderedMessageText) {
150
+ const title =
151
+ renderedMessageText.length > 50
152
+ ? `${renderedMessageText.slice(0, 47)}...`
153
+ : renderedMessageText;
146
154
  await safeSetTitle(title);
147
155
  }
148
156
 
@@ -156,7 +164,7 @@ export function createAssistant(): Assistant {
156
164
  const lead = getLeadAgent();
157
165
  if (!lead) {
158
166
  // No lead — still queue the task
159
- createTaskWithSiblingAwareness(messageText + channelContext, {
167
+ createTaskWithSiblingAwareness(renderedMessageText + channelContext, {
160
168
  source: "slack",
161
169
  slackChannelId: channelId,
162
170
  slackThreadTs: threadTs,
@@ -169,7 +177,7 @@ export function createAssistant(): Assistant {
169
177
  return;
170
178
  }
171
179
 
172
- createTaskWithSiblingAwareness(messageText + channelContext, {
180
+ createTaskWithSiblingAwareness(renderedMessageText + channelContext, {
173
181
  agentId: lead.id,
174
182
  source: "slack",
175
183
  slackChannelId: channelId,
@@ -23,6 +23,7 @@
23
23
 
24
24
  import type { WebClient } from "@slack/web-api";
25
25
  import { getKv, upsertKv } from "../be/db";
26
+ import { resolveIdentity } from "../be/identity";
26
27
  import { recordUnmappedIdentity } from "../be/unmapped-identities";
27
28
  import {
28
29
  findOrCreateUserByEmail,
@@ -160,3 +161,19 @@ export async function resolveSlackUserId(
160
161
  recordUnmappedIdentity("slack", slackUserId, eventContext);
161
162
  return undefined;
162
163
  }
164
+
165
+ /**
166
+ * Rewrite every Slack `<@USERID>` mention token in `text` via the identity
167
+ * primitive: `<@USERID|Name>` when resolved, `<@USERID> (unknown user)` when
168
+ * not. Covers both author labels (`<@U…>: message`) and mentions embedded in
169
+ * message bodies — both are just this same token. Pure DB reads via
170
+ * `resolveIdentity`; zero Slack API calls, no cache.
171
+ */
172
+ export function rewriteSlackMentions(text: string): string {
173
+ return text.replace(/<@([A-Z0-9]+)>/g, (_match, userId: string) => {
174
+ const resolution = resolveIdentity("slack", userId);
175
+ return resolution.status === "resolved"
176
+ ? `<@${userId}|${resolution.name}>`
177
+ : `<@${userId}> (unknown user)`;
178
+ });
179
+ }
@@ -12,7 +12,7 @@ import { slackContextKey } from "../tasks/context-key";
12
12
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
13
13
  import { workflowEventBus } from "../workflows/event-bus";
14
14
  import { buildTreeBlocks, type TreeNode } from "./blocks";
15
- import { enrichSlackUserEmail, resolveSlackUserId } from "./enrich";
15
+ import { enrichSlackUserEmail, resolveSlackUserId, rewriteSlackMentions } from "./enrich";
16
16
  import { wasEventSeen } from "./event-dedup";
17
17
  import type { SlackFile } from "./files";
18
18
  import { extractTaskFromMessage, hasOtherUserMention, routeMessage } from "./router";
@@ -250,23 +250,6 @@ async function wasThreadStartedBySwarm(
250
250
  return startedBySwarm;
251
251
  }
252
252
 
253
- // Cache for user display names
254
- const userNameCache = new Map<string, string>();
255
-
256
- async function getUserDisplayName(client: WebClient, userId: string): Promise<string> {
257
- if (userNameCache.has(userId)) {
258
- return userNameCache.get(userId)!;
259
- }
260
- try {
261
- const result = await client.users.info({ user: userId });
262
- const name = result.user?.profile?.display_name || result.user?.real_name || userId;
263
- userNameCache.set(userId, name);
264
- return name;
265
- } catch {
266
- return userId;
267
- }
268
- }
269
-
270
253
  /**
271
254
  * Fetch thread history and format as context for the task.
272
255
  * Returns empty string if not in a thread or no previous messages.
@@ -297,7 +280,10 @@ async function getThreadContext(
297
280
 
298
281
  if (previousMessages.length === 0) return "";
299
282
 
300
- // Format messages with user names or [Agent] for bot messages
283
+ // Format messages with user names or [Agent] for bot messages. Author
284
+ // identity is rendered via the identity primitive (resolved name or the
285
+ // UNKNOWN sentinel) — never a raw Slack API display name, and no
286
+ // `users.info` call.
301
287
  const formattedMessages: string[] = [];
302
288
  for (const m of previousMessages) {
303
289
  // Check if this is a bot/agent message (multiple ways to identify)
@@ -310,12 +296,12 @@ async function getThreadContext(
310
296
  const truncatedText = text.length > 500 ? `${text.slice(0, 500)}...` : text;
311
297
  formattedMessages.push(`[Agent]: ${truncatedText}`);
312
298
  } else {
313
- const userName = m.user ? await getUserDisplayName(client, m.user) : "Unknown";
314
- formattedMessages.push(`${userName}: ${text}`);
299
+ const authorTag = m.user ? `<@${m.user}>` : "Unknown";
300
+ formattedMessages.push(`${authorTag}: ${text}`);
315
301
  }
316
302
  }
317
303
 
318
- return formattedMessages.join("\n");
304
+ return rewriteSlackMentions(formattedMessages.join("\n"));
319
305
  } catch (error) {
320
306
  console.error("[Slack] Failed to fetch thread context:", error);
321
307
  return "";
@@ -1,6 +1,6 @@
1
1
  import type { WebClient } from "@slack/web-api";
2
2
  import { getAgentById, getTaskAttachments } from "../be/db";
3
- import type { Agent, AgentTask } from "../types";
3
+ import type { Agent, AgentTask, TaskAttachment } from "../types";
4
4
  import { getSlackApp } from "./app";
5
5
  import {
6
6
  buildCancelledBlocks,
@@ -10,7 +10,9 @@ import {
10
10
  buildProgressBlocks,
11
11
  formatAttachmentsBlockForSlack,
12
12
  formatDuration,
13
+ getTaskLink,
13
14
  markdownToSlack,
15
+ splitSlackSectionText,
14
16
  } from "./blocks";
15
17
 
16
18
  // Re-export for backward compatibility
@@ -31,6 +33,16 @@ function classifySlackUpdateError(error: unknown): SlackUpdateResult {
31
33
  }
32
34
 
33
35
  const isDev = process.env.ENV === "development";
36
+ const MIN_INLINE_OUTPUT_LENGTH = 20;
37
+ export const MAX_INLINE_OUTPUT_MESSAGE_LENGTH = 4000;
38
+ const INLINE_OUTPUT_FALLBACKS = new Set([
39
+ "done",
40
+ "completed",
41
+ "complete",
42
+ "task completed",
43
+ "process completed successfully",
44
+ "process completed successfully (no output captured)",
45
+ ]);
34
46
 
35
47
  /**
36
48
  * Get the display name for an agent, with (dev) prefix if in development mode.
@@ -39,6 +51,84 @@ function getAgentDisplayName(agent: Agent): string {
39
51
  return isDev ? `(dev) ${agent.name}` : agent.name;
40
52
  }
41
53
 
54
+ export function shouldPostInlineCompletionOutput(
55
+ task: AgentTask,
56
+ attachments: readonly TaskAttachment[],
57
+ ): boolean {
58
+ if (task.status !== "completed") return false;
59
+ if (!task.slackChannelId || !task.slackThreadTs) return false;
60
+ if (task.slackReplySent) return false;
61
+
62
+ const output = task.output?.trim();
63
+ if (!output || output.length < MIN_INLINE_OUTPUT_LENGTH) return false;
64
+ if (INLINE_OUTPUT_FALLBACKS.has(output.toLowerCase())) return false;
65
+
66
+ return !attachments.some((attachment) => attachment.isPrimary);
67
+ }
68
+
69
+ export function formatInlineCompletionOutputText(opts: {
70
+ agentName: string;
71
+ taskId: string;
72
+ output: string;
73
+ }): string {
74
+ const header = `✅ *${opts.agentName}* completed with output (${getTaskLink(opts.taskId)}):\n\n`;
75
+ const tail = `\n\n…(full output in task ${getTaskLink(opts.taskId)})`;
76
+ const slackOutput = markdownToSlack(opts.output.trim());
77
+ const maxBodyLength = MAX_INLINE_OUTPUT_MESSAGE_LENGTH - header.length;
78
+
79
+ if (slackOutput.length <= maxBodyLength) {
80
+ return header + slackOutput;
81
+ }
82
+
83
+ const maxTruncatedBodyLength = Math.max(0, maxBodyLength - tail.length);
84
+ let cut = slackOutput.lastIndexOf("\n", maxTruncatedBodyLength);
85
+ if (cut < maxTruncatedBodyLength / 2) {
86
+ cut = slackOutput.lastIndexOf(" ", maxTruncatedBodyLength);
87
+ }
88
+ if (cut < maxTruncatedBodyLength / 2) {
89
+ cut = maxTruncatedBodyLength;
90
+ }
91
+
92
+ return header + slackOutput.slice(0, cut).trimEnd() + tail;
93
+ }
94
+
95
+ export async function sendInlineTaskOutput(task: AgentTask): Promise<boolean> {
96
+ const app = getSlackApp();
97
+ if (!app || !task.slackChannelId || !task.slackThreadTs || !task.agentId || !task.output) {
98
+ return false;
99
+ }
100
+
101
+ const agent = getAgentById(task.agentId);
102
+ if (!agent) {
103
+ console.error(`[Slack] Agent not found for task ${task.id}`);
104
+ return false;
105
+ }
106
+
107
+ const text = formatInlineCompletionOutputText({
108
+ agentName: agent.name,
109
+ taskId: task.id,
110
+ output: task.output,
111
+ });
112
+
113
+ try {
114
+ await sendWithPersona(app.client, {
115
+ channel: task.slackChannelId,
116
+ thread_ts: task.slackThreadTs,
117
+ text,
118
+ username: getAgentDisplayName(agent),
119
+ icon_emoji: getAgentEmoji(agent),
120
+ blocks: splitSlackSectionText(text).map((chunk) => ({
121
+ type: "section",
122
+ text: { type: "mrkdwn", text: chunk },
123
+ })),
124
+ });
125
+ return true;
126
+ } catch (error) {
127
+ console.error(`[Slack] Failed to send inline output for task ${task.id}:`, error);
128
+ return false;
129
+ }
130
+ }
131
+
42
132
  /**
43
133
  * Send a task completion message to Slack with the agent's persona.
44
134
  */
@@ -4,6 +4,7 @@ import { slackContextKey } from "../tasks/context-key";
4
4
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
5
5
  import { getSlackApp } from "./app";
6
6
  import { buildBufferFlushBlocks } from "./blocks";
7
+ import { rewriteSlackMentions } from "./enrich";
7
8
  import { extractSlackMessageText } from "./message-text";
8
9
  import { registerTreeMessage } from "./watcher";
9
10
 
@@ -107,7 +108,7 @@ async function getThreadContextForBuffer(channelId: string, threadTs: string): P
107
108
  })
108
109
  .join("\n");
109
110
 
110
- return formatted;
111
+ return rewriteSlackMentions(formatted);
111
112
  } catch (error) {
112
113
  console.error("[Slack] Failed to fetch thread context for buffer:", error);
113
114
  return "";
@@ -137,8 +138,10 @@ async function slackFlush(
137
138
 
138
139
  console.log(`[Slack] Flushing buffer: ${key} (${items.length} messages, immediate=${immediate})`);
139
140
 
140
- // Build combined task description
141
- const combinedText = items.map((m) => m.text).join("\n---\n");
141
+ // Build combined task description. Any in-body `<@U…>` mentions the
142
+ // requester typed are rewritten via the identity primitive so the agent
143
+ // sees a name or the explicit UNKNOWN sentinel — never a raw Slack ID.
144
+ const combinedText = rewriteSlackMentions(items.map((m) => m.text).join("\n---\n"));
142
145
  const description = `[Thread follow-up — ${items.length} message(s) buffered]\n\n${combinedText}`;
143
146
 
144
147
  // Find the latest active task in this thread for dependency chaining
@@ -12,8 +12,10 @@ import { getSlackApp } from "./app";
12
12
  import type { TreeNode } from "./blocks";
13
13
  import { buildTreeBlocks, formatDuration } from "./blocks";
14
14
  import {
15
+ sendInlineTaskOutput,
15
16
  sendProgressUpdate,
16
17
  sendTaskResponse,
18
+ shouldPostInlineCompletionOutput,
17
19
  updateProgressInPlace,
18
20
  updateToFinal,
19
21
  updateTreeMessage,
@@ -704,11 +706,11 @@ export function startTaskWatcher(intervalMs = 3000): void {
704
706
  if (lastSent && now - lastSent < MIN_SEND_INTERVAL) continue;
705
707
 
706
708
  // Tasks tracked in a tree are still rendered by processTreeMessages().
707
- // Successful tasks without their own slack-reply also get a full
708
- // threaded completion message so the tree's compact preview is not the
709
- // only place their output appears.
709
+ // For prose-only completions, add one inline output reply so the user
710
+ // sees the actual answer instead of only the compact tree preview.
710
711
  if (taskToTree.has(task.id)) {
711
- if (task.status !== "completed" || task.slackReplySent) {
712
+ const attachments = task.status === "completed" ? getTaskAttachments(task.id) : [];
713
+ if (!shouldPostInlineCompletionOutput(task, attachments)) {
712
714
  notifiedCompletions.set(task.id, now);
713
715
  continue;
714
716
  }
@@ -717,9 +719,10 @@ export function startTaskWatcher(intervalMs = 3000): void {
717
719
  notifiedCompletions.set(task.id, now);
718
720
  lastSendTime.set(completionKey, now);
719
721
  try {
720
- await sendTaskResponse(task);
722
+ const sent = await sendInlineTaskOutput(task);
723
+ if (!sent) throw new Error("sendInlineTaskOutput returned false");
721
724
  console.log(
722
- `[Slack] Sent full tree-tracked completion for task ${task.id.slice(0, 8)}`,
725
+ `[Slack] Sent inline output for tree-tracked completion ${task.id.slice(0, 8)}`,
723
726
  );
724
727
  } catch (error) {
725
728
  notifiedCompletions.delete(task.id);