@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
@@ -3,7 +3,12 @@ import { z } from "zod";
3
3
  import { resolveHttpAuditUserId } from "../be/audit-user";
4
4
  import { getAgentById, recordInlineScriptRun, upsertKv } from "../be/db";
5
5
  import { createEvent } from "../be/events";
6
- import { getScriptApiConnectionDescriptors, getScriptApiTypes } from "../be/script-connections";
6
+ import {
7
+ getScriptApiConnectionDescriptors,
8
+ getScriptApiTypes,
9
+ getScriptMcpConnectionDescriptors,
10
+ getScriptMcpTypes,
11
+ } from "../be/script-connections";
7
12
  import { buildScriptCredentialBindings } from "../be/script-credential-broker";
8
13
  import {
9
14
  createScriptApi,
@@ -23,8 +28,8 @@ import {
23
28
  import { searchScripts } from "../be/scripts/embeddings";
24
29
  import { extractArgsJsonSchema } from "../be/scripts/extract-schema";
25
30
  import {
26
- SCRIPT_STDLIB_TYPES,
27
31
  scriptSdkTypesWithGeneratedApis,
32
+ scriptStdlibTypesWithGeneratedApis,
28
33
  typecheckScript,
29
34
  } from "../be/scripts/typecheck";
30
35
  import { can } from "../rbac";
@@ -131,6 +136,7 @@ const searchRoute = route({
131
136
  200: { description: "Matching scripts" },
132
137
  400: { description: "Validation error" },
133
138
  },
139
+ rbac: { permission: "script.search" },
134
140
  });
135
141
 
136
142
  const deleteRoute = route({
@@ -268,6 +274,7 @@ const createScriptApiRoute = route({
268
274
  400: { description: "Validation error or script has no owning agent" },
269
275
  404: { description: "Script not found" },
270
276
  },
277
+ rbac: { permission: "script.api.create" },
271
278
  });
272
279
 
273
280
  const listScriptApisRoute = route({
@@ -296,6 +303,7 @@ const revealScriptApiSecretRoute = route({
296
303
  200: { description: "Decrypted token (null when authMode is 'none')" },
297
304
  404: { description: "Endpoint not found" },
298
305
  },
306
+ rbac: { permission: "script.api.read.secrets" },
299
307
  });
300
308
 
301
309
  const patchScriptApiRoute = route({
@@ -311,6 +319,7 @@ const patchScriptApiRoute = route({
311
319
  200: { description: "Updated endpoint" },
312
320
  404: { description: "Endpoint not found" },
313
321
  },
322
+ rbac: { permission: "script.api.update" },
314
323
  });
315
324
 
316
325
  const rotateScriptApiRoute = route({
@@ -326,6 +335,7 @@ const rotateScriptApiRoute = route({
326
335
  400: { description: "Endpoint uses 'none' auth — nothing to rotate" },
327
336
  404: { description: "Endpoint not found" },
328
337
  },
338
+ rbac: { permission: "script.api.rotate" },
329
339
  });
330
340
 
331
341
  const deleteScriptApiRoute = route({
@@ -340,6 +350,7 @@ const deleteScriptApiRoute = route({
340
350
  200: { description: "Deleted" },
341
351
  404: { description: "Endpoint not found" },
342
352
  },
353
+ rbac: { permission: "script.api.delete" },
343
354
  });
344
355
 
345
356
  function requireAgent(res: ServerResponse, agentId: string | undefined) {
@@ -511,8 +522,9 @@ export async function handleScripts(
511
522
  args: parsed.body.args,
512
523
  fsMode,
513
524
  agentId: agent.id,
514
- egressSecrets: buildScriptCredentialBindings({ agentId: agent.id }),
525
+ egressSecrets: await buildScriptCredentialBindings({ agentId: agent.id }),
515
526
  apiConnections: getScriptApiConnectionDescriptors({ agentId: agent.id }),
527
+ mcpConnections: getScriptMcpConnectionDescriptors({ agentId: agent.id }),
516
528
  });
517
529
 
518
530
  // Persist output to KV when idempotencyKey is provided and run succeeded
@@ -667,9 +679,11 @@ export async function handleScripts(
667
679
  // Must be matched before getScriptByIdRoute — its ["api", "scripts", null]
668
680
  // pattern would otherwise swallow the literal "type-defs" segment.
669
681
  if (typeDefsRoute.match(req.method, pathSegments)) {
682
+ const apiTypes = getScriptApiTypes();
683
+ const mcpTypes = getScriptMcpTypes();
670
684
  json(res, {
671
- sdkTypes: scriptSdkTypesWithGeneratedApis(getScriptApiTypes()),
672
- stdlibTypes: SCRIPT_STDLIB_TYPES,
685
+ sdkTypes: scriptSdkTypesWithGeneratedApis(apiTypes, mcpTypes),
686
+ stdlibTypes: scriptStdlibTypesWithGeneratedApis(apiTypes, mcpTypes),
673
687
  });
674
688
  return true;
675
689
  }
@@ -718,8 +732,14 @@ export async function handleScripts(
718
732
  json(res, {
719
733
  signature: JSON.parse(script.signatureJson),
720
734
  argsJsonSchema: script.argsJsonSchema ? (JSON.parse(script.argsJsonSchema) as unknown) : null,
721
- sdkTypes: scriptSdkTypesWithGeneratedApis(getScriptApiTypes({ agentId: agent.id })),
722
- stdlibTypes: SCRIPT_STDLIB_TYPES,
735
+ sdkTypes: scriptSdkTypesWithGeneratedApis(
736
+ getScriptApiTypes({ agentId: agent.id }),
737
+ getScriptMcpTypes({ agentId: agent.id }),
738
+ ),
739
+ stdlibTypes: scriptStdlibTypesWithGeneratedApis(
740
+ getScriptApiTypes({ agentId: agent.id }),
741
+ getScriptMcpTypes({ agentId: agent.id }),
742
+ ),
723
743
  });
724
744
  return true;
725
745
  }
@@ -103,6 +103,7 @@ const bulkUpsertSkillFilesRoute = route({
103
103
  summary: "Bulk upsert bundled files for a skill",
104
104
  tags: ["Skills"],
105
105
  auth: { apiKey: true },
106
+ rbac: { permission: "skill.update.any" },
106
107
  params: z.object({ id: z.string() }),
107
108
  body: z.object({
108
109
  files: z.array(skillFileWithPathSchema).max(100),
@@ -137,6 +138,7 @@ const upsertSkillFileRoute = route({
137
138
  summary: "Upsert a bundled skill file",
138
139
  tags: ["Skills"],
139
140
  auth: { apiKey: true },
141
+ rbac: { permission: "skill.update.any" },
140
142
  params: z.object({ id: z.string(), path: z.string() }),
141
143
  body: skillFileBodySchema,
142
144
  responses: {
@@ -154,6 +156,7 @@ const deleteSkillFileRoute = route({
154
156
  summary: "Delete a bundled skill file",
155
157
  tags: ["Skills"],
156
158
  auth: { apiKey: true },
159
+ rbac: { permission: "skill.update.any" },
157
160
  params: z.object({ id: z.string(), path: z.string() }),
158
161
  responses: {
159
162
  200: { description: "Skill file deleted" },
@@ -168,6 +171,7 @@ const createSkillRoute = route({
168
171
  summary: "Create a new skill",
169
172
  tags: ["Skills"],
170
173
  auth: { apiKey: true },
174
+ rbac: { permission: "skill.create.swarm" },
171
175
  body: z.object({
172
176
  content: z.string().min(1),
173
177
  type: z.string().optional(),
@@ -188,6 +192,7 @@ const updateSkillRoute = route({
188
192
  summary: "Update a skill",
189
193
  tags: ["Skills"],
190
194
  auth: { apiKey: true },
195
+ rbac: { permission: "skill.update.any" },
191
196
  params: z.object({ id: z.string() }),
192
197
  body: z.record(z.string(), z.unknown()),
193
198
  responses: {
@@ -204,6 +209,7 @@ const deleteSkillRoute = route({
204
209
  summary: "Delete a skill",
205
210
  tags: ["Skills"],
206
211
  auth: { apiKey: true },
212
+ rbac: { permission: "skill.delete.any" },
207
213
  params: z.object({ id: z.string() }),
208
214
  responses: {
209
215
  200: { description: "Skill deleted" },
@@ -219,6 +225,7 @@ const installSkillRoute = route({
219
225
  summary: "Install skill for an agent",
220
226
  tags: ["Skills"],
221
227
  auth: { apiKey: true },
228
+ rbac: { permission: "skill.install.any" },
222
229
  params: z.object({ id: z.string() }),
223
230
  body: z.object({
224
231
  agentId: z.string(),
@@ -236,6 +243,7 @@ const uninstallSkillRoute = route({
236
243
  summary: "Uninstall skill for an agent",
237
244
  tags: ["Skills"],
238
245
  auth: { apiKey: true },
246
+ rbac: { permission: "skill.uninstall.any" },
239
247
  params: z.object({ id: z.string(), agentId: z.string() }),
240
248
  responses: {
241
249
  200: { description: "Skill uninstalled" },
@@ -249,6 +257,7 @@ const installRemoteRoute = route({
249
257
  summary: "Install a remote skill from GitHub",
250
258
  tags: ["Skills"],
251
259
  auth: { apiKey: true },
260
+ rbac: { permission: "skill.install.global" },
252
261
  body: z.object({
253
262
  sourceRepo: z.string(),
254
263
  sourcePath: z.string().optional(),
@@ -268,6 +277,7 @@ const syncRemoteRoute = route({
268
277
  summary: "Trigger remote skill sync",
269
278
  tags: ["Skills"],
270
279
  auth: { apiKey: true },
280
+ rbac: { permission: "skill.update.any" },
271
281
  body: z.object({
272
282
  skillId: z.string().optional(),
273
283
  force: z.boolean().optional(),
@@ -284,6 +294,7 @@ const syncFilesystemRoute = route({
284
294
  summary: "Sync installed skills to agent filesystem",
285
295
  tags: ["Skills"],
286
296
  auth: { apiKey: true, agentId: true },
297
+ rbac: { ungated: "self-scoped: syncs the caller's own agent FS" },
287
298
  responses: {
288
299
  200: { description: "Filesystem sync results" },
289
300
  },
package/src/http/tasks.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { ensure } from "@desplega.ai/business-use";
3
3
  import { z } from "zod";
4
+ import { resolveHttpAuditUserId } from "../be/audit-user";
4
5
  import {
5
6
  backfillSupersedeTaskResumeTaskId,
6
7
  cancelTask,
@@ -14,7 +15,6 @@ import {
14
15
  getTaskAttachments,
15
16
  getTaskById,
16
17
  getTasksCount,
17
- getUserById,
18
18
  pauseTask,
19
19
  resumeTask,
20
20
  supersedeTask,
@@ -23,6 +23,7 @@ import {
23
23
  updateTaskProgress,
24
24
  updateTaskVcs,
25
25
  } from "../be/db";
26
+ import { findUserById } from "../be/users";
26
27
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
27
28
  import { createResumeFollowUp, createWorkerTaskFollowUp } from "../tasks/worker-follow-up";
28
29
  import {
@@ -37,7 +38,6 @@ import {
37
38
  ResumeReasonSchema,
38
39
  splitLegacyModelAlias,
39
40
  } from "../types";
40
- import { getRequestAuth } from "../utils/request-auth-context";
41
41
  import { route } from "./route-def";
42
42
  import { json, jsonError } from "./utils";
43
43
 
@@ -60,8 +60,12 @@ const listTasks = route({
60
60
  includeHeartbeat: z.enum(["true", "false"]).optional(),
61
61
  /** ISO 8601 — return only tasks created on/after this timestamp. */
62
62
  createdAfter: z.string().datetime().optional(),
63
+ /** ISO 8601 — return only tasks created before this timestamp. */
64
+ createdBefore: z.string().datetime().optional(),
63
65
  /** Comma-separated source filter (e.g. `ui,slack`). Omit to include all. */
64
66
  source: z.string().optional(),
67
+ /** `createdAt` enables stable time-axis paging; default preserves table freshness ordering. */
68
+ orderBy: z.enum(["lastUpdatedAt", "createdAt"]).optional(),
65
69
  limit: z.coerce.number().int().optional(),
66
70
  offset: z.coerce.number().int().optional(),
67
71
  /** `full` restores the legacy shape (full `task` text + all fields); default is slim. */
@@ -343,7 +347,9 @@ export async function handleTasks(
343
347
  search: parsed.query.search || undefined,
344
348
  includeHeartbeat: parsed.query.includeHeartbeat === "true" || undefined,
345
349
  createdAfter: parsed.query.createdAfter || undefined,
350
+ createdBefore: parsed.query.createdBefore || undefined,
346
351
  source,
352
+ orderBy: parsed.query.orderBy,
347
353
  limit: parsed.query.limit,
348
354
  offset: parsed.query.offset,
349
355
  };
@@ -360,17 +366,25 @@ export async function handleTasks(
360
366
  const parsed = await createTask.parse(req, res, pathSegments, queryParams);
361
367
  if (!parsed) return true;
362
368
 
363
- // Tolerant `requestedByUserId`: prevent the deleted-user race from
364
- // becoming a 500 — if the referenced user doesn't exist, log and drop
365
- // the field rather than letting the FK fail at INSERT.
366
- const auth = getRequestAuth(req);
367
- let requestedByUserId =
368
- auth?.kind === "user" ? auth.userId : parsed.body.requestedByUserId || undefined;
369
- if (requestedByUserId && !getUserById(requestedByUserId)) {
370
- console.warn(
371
- `[tasks] requestedByUserId ${requestedByUserId} does not exist coercing to NULL`,
372
- );
373
- requestedByUserId = undefined;
369
+ // Prefer trusted server-side identity: an authenticated request user, or
370
+ // the caller's own ownership-gated task context (`X-Source-Task-Id` /
371
+ // ambient current task) same as every other audited write site. This
372
+ // is the upstream #939 anti-spoofing behavior and stays the default.
373
+ //
374
+ // Fork-specific opt-in: in a single-tenant deployment where every caller
375
+ // shares ONE operator key (so `auth.kind` is never "user" and there's no
376
+ // way to bind a caller to a user), TRUST_BODY_REQUESTED_BY_USER_ID=true
377
+ // re-enables a body-supplied `requestedByUserId` as a last resort, after
378
+ // validating it names a real user. This is safe ONLY because every
379
+ // holder of that shared key is already trusted org-wide — it must stay
380
+ // off (default) anywhere callers of the shared/global key are not all
381
+ // equally trusted, since it lets any such caller attribute a task to any
382
+ // user. Must NOT be upstreamed as a default-on behavior.
383
+ let requestedByUserId = resolveHttpAuditUserId(req, myAgentId) ?? undefined;
384
+ const trustBodyRequestedByUserId = process.env.TRUST_BODY_REQUESTED_BY_USER_ID === "true";
385
+ if (trustBodyRequestedByUserId && !requestedByUserId && parsed.body.requestedByUserId) {
386
+ const candidate = findUserById(parsed.body.requestedByUserId);
387
+ if (candidate) requestedByUserId = candidate.id;
374
388
  }
375
389
 
376
390
  // Default agent for ingress-created tasks: when no explicit `agentId` is
package/src/http/x.ts CHANGED
@@ -2,7 +2,10 @@ import { timingSafeEqual } from "node:crypto";
2
2
  import type { IncomingMessage, ServerResponse } from "node:http";
3
3
  import { z } from "zod";
4
4
  import { recordInlineScriptRun } from "../be/db";
5
- import { getScriptApiConnectionDescriptors } from "../be/script-connections";
5
+ import {
6
+ getScriptApiConnectionDescriptors,
7
+ getScriptMcpConnectionDescriptors,
8
+ } from "../be/script-connections";
6
9
  import { buildScriptCredentialBindings } from "../be/script-credential-broker";
7
10
  import {
8
11
  getScriptApiById,
@@ -204,8 +207,9 @@ export async function handleX(
204
207
  // timeout, so an external caller can't use a long X-Swarm-Timeout-Ms to
205
208
  // burn proportionally more CPU per request.
206
209
  timeoutMs,
207
- egressSecrets: buildScriptCredentialBindings({ agentId: endpoint.agentId }),
210
+ egressSecrets: await buildScriptCredentialBindings({ agentId: endpoint.agentId }),
208
211
  apiConnections: getScriptApiConnectionDescriptors({ agentId: endpoint.agentId }),
212
+ mcpConnections: getScriptMcpConnectionDescriptors({ agentId: endpoint.agentId }),
209
213
  });
210
214
 
211
215
  const ok = output.exitCode === 0 && !output.error && !output.runtimeError;
@@ -2,8 +2,8 @@ import { resolveTemplate } from "@/prompts/resolver";
2
2
  import { createTaskWithSiblingAwareness } from "@/tasks/sibling-awareness";
3
3
  import { workflowEventBus } from "@/workflows/event-bus";
4
4
  import "@/tools/templates";
5
+ import { type IdentityResolution, renderIdentity, resolveIdentity } from "@/be/identity";
5
6
  import { recordUnmappedIdentity } from "@/be/unmapped-identities";
6
- import { findUserByExternalId } from "@/be/users";
7
7
  import { getKapsoNumberMapping, markKapsoMessageSeen } from "./config";
8
8
 
9
9
  const KAPSO_IDENTITY_KIND = "kapso";
@@ -44,11 +44,15 @@ function extractText(message: NonNullable<KapsoWebhookPayload["message"]>): stri
44
44
  function buildTaskDescription(payload: KapsoWebhookPayload): string {
45
45
  const message = payload.message ?? {};
46
46
  const conversation = payload.conversation ?? {};
47
+ const externalId = normalizeKapsoSender(payload);
48
+ const contactDisplay = externalId
49
+ ? renderIdentity(resolveKapsoIdentity(externalId))
50
+ : "(unknown user)";
47
51
  return resolveTemplate("kapso.message.received", {
48
52
  conversation_id: conversation.id ?? "unknown",
49
53
  inbound_wamid: message.id ?? "unknown",
50
54
  sender_phone: message.from ?? conversation.phone_number ?? "unknown",
51
- contact_name: conversation.contact_name ?? "unknown",
55
+ contact_name: contactDisplay,
52
56
  phone_number_id: payload.phone_number_id ?? "unknown",
53
57
  test_note: payload.test ? "\n- test: true (do NOT send a real WhatsApp reply)" : "",
54
58
  message_text: extractText(message),
@@ -61,14 +65,27 @@ function normalizeKapsoSender(payload: KapsoWebhookPayload): string | null {
61
65
  return digits || null;
62
66
  }
63
67
 
68
+ /**
69
+ * Reverse-lookup a phone-number externalId across both identity kinds this
70
+ * integration writes (`kapso`, `whatsapp` — see the dual-kind note on
71
+ * `resolveKapsoRequestedByUserId`). Never a provider profile label.
72
+ */
73
+ function resolveKapsoIdentity(externalId: string): IdentityResolution {
74
+ const kapsoResolution = resolveIdentity(KAPSO_IDENTITY_KIND, externalId);
75
+ if (kapsoResolution.status === "resolved") return kapsoResolution;
76
+ const whatsappResolution = resolveIdentity(WHATSAPP_IDENTITY_KIND, externalId);
77
+ if (whatsappResolution.status === "resolved") return whatsappResolution;
78
+ // Both unknown — report under the `kapso` kind for consistency with
79
+ // resolveKapsoRequestedByUserId's unmapped-tracker recording.
80
+ return kapsoResolution;
81
+ }
82
+
64
83
  function resolveKapsoRequestedByUserId(payload: KapsoWebhookPayload): string | undefined {
65
84
  const externalId = normalizeKapsoSender(payload);
66
85
  if (!externalId) return undefined;
67
86
 
68
- const mapped =
69
- findUserByExternalId(KAPSO_IDENTITY_KIND, externalId) ??
70
- findUserByExternalId(WHATSAPP_IDENTITY_KIND, externalId);
71
- if (mapped) return mapped.id;
87
+ const resolution = resolveKapsoIdentity(externalId);
88
+ if (resolution.status === "resolved") return resolution.userId;
72
89
 
73
90
  recordUnmappedIdentity(KAPSO_IDENTITY_KIND, externalId, {
74
91
  sampleEventType: "kapso.message.received",
package/src/jira/sync.ts CHANGED
@@ -21,6 +21,14 @@ import {
21
21
  getTrackerSyncByExternalId,
22
22
  updateTrackerSyncSwarmId,
23
23
  } from "../be/db-queries/tracker";
24
+ import { renderIdentity, resolveIdentity } from "../be/identity";
25
+ import { recordUnmappedIdentity } from "../be/unmapped-identities";
26
+ import {
27
+ findOrCreateUserByEmail,
28
+ findUserByExternalId,
29
+ type IdentityActor,
30
+ linkIdentity,
31
+ } from "../be/users";
24
32
  import { ensureToken, ensureTokenOrThrow } from "../oauth/ensure-token";
25
33
  import { resolveTemplate } from "../prompts/resolver";
26
34
  import { buildJiraContextKey } from "../tasks/context-key";
@@ -31,6 +39,66 @@ import { getJiraMetadata } from "./metadata";
31
39
  // Side-effect import: registers all Jira event templates in the prompt registry
32
40
  import "./templates";
33
41
 
42
+ // ─── Actor resolution (mirrors resolveLinearActor / resolveSlackUserId) ────
43
+
44
+ /** Audit-trail actor for the Jira auto-link cascade. */
45
+ const JIRA_WEBHOOK_ACTOR: IdentityActor = { kind: "system", id: "webhook:jira" };
46
+
47
+ /**
48
+ * Map a Jira Atlassian `accountId` to a canonical `users.id`:
49
+ * 1. Existing `(jira, accountId)` mapping — fast path.
50
+ * 2. No mapping + an email is present on the payload (privacy settings
51
+ * permitting) → `findOrCreateUserByEmail` + `linkIdentity`.
52
+ * 3. No mapping + no email → record into the kv unmapped tracker; caller
53
+ * proceeds without a `requestedByUserId`.
54
+ *
55
+ * Returns `undefined` when no mapping could be established.
56
+ */
57
+ function resolveJiraActor(
58
+ accountId: string | undefined,
59
+ email: string | undefined,
60
+ name: string | undefined,
61
+ sampleEventType: string,
62
+ sampleContext: string,
63
+ ): string | undefined {
64
+ if (!accountId) return undefined;
65
+
66
+ const existing = findUserByExternalId("jira", accountId);
67
+ if (existing) return existing.id;
68
+
69
+ const trimmedEmail = typeof email === "string" ? email.trim() : "";
70
+ if (trimmedEmail !== "") {
71
+ const { user } = findOrCreateUserByEmail(
72
+ trimmedEmail,
73
+ { name: name?.trim() || undefined },
74
+ JIRA_WEBHOOK_ACTOR,
75
+ );
76
+ try {
77
+ linkIdentity(user.id, "jira", accountId, JIRA_WEBHOOK_ACTOR);
78
+ } catch (error) {
79
+ console.warn(
80
+ `[Jira Sync] linkIdentity('jira', ${accountId}) failed — likely a concurrent enroll`,
81
+ error,
82
+ );
83
+ }
84
+ return user.id;
85
+ }
86
+
87
+ recordUnmappedIdentity("jira", accountId, { sampleEventType, sampleContext });
88
+ return undefined;
89
+ }
90
+
91
+ /**
92
+ * Render a Jira actor for agent-visible text: the resolved canonical name or
93
+ * the explicit UNKNOWN sentinel — never the raw Jira `displayName`. Falls
94
+ * back to a bare "(unknown user)" when the payload carries no `accountId` at
95
+ * all (malformed/legacy webhook shape).
96
+ */
97
+ function renderJiraIdentity(accountId: string | undefined): string {
98
+ if (!accountId) return "(unknown user)";
99
+ return renderIdentity(resolveIdentity("jira", accountId));
100
+ }
101
+
34
102
  // ─── Bot identity (Atlassian accountId) ────────────────────────────────────
35
103
 
36
104
  // Cache the bot's Atlassian accountId on globalThis (not a module-level `let`)
@@ -171,7 +239,7 @@ type IssueShape = {
171
239
  fields?: {
172
240
  summary?: string;
173
241
  description?: unknown;
174
- reporter?: { displayName?: string; accountId?: string } | null;
242
+ reporter?: { displayName?: string; accountId?: string; emailAddress?: string } | null;
175
243
  };
176
244
  };
177
245
 
@@ -218,7 +286,14 @@ export async function handleIssueEvent(event: Record<string, unknown>): Promise<
218
286
  const issueId = issue.id;
219
287
  const issueKey = issue.key;
220
288
  const summary = issue.fields?.summary ?? "(no summary)";
221
- const reporterName = issue.fields?.reporter?.displayName ?? "";
289
+ const reporterAccountId = issue.fields?.reporter?.accountId;
290
+ const requestedByUserId = resolveJiraActor(
291
+ reporterAccountId,
292
+ issue.fields?.reporter?.emailAddress,
293
+ issue.fields?.reporter?.displayName,
294
+ "issue_updated",
295
+ `${issueKey}: ${summary}`,
296
+ );
222
297
  const descriptionText = extractText(issue.fields?.description);
223
298
  const issueUrl = buildIssueUrl(issueKey);
224
299
 
@@ -241,7 +316,8 @@ export async function handleIssueEvent(event: Record<string, unknown>): Promise<
241
316
  await createInitialJiraTask({
242
317
  issueKey,
243
318
  summary,
244
- reporterName,
319
+ reporterAccountId,
320
+ requestedByUserId,
245
321
  descriptionText,
246
322
  issueUrl,
247
323
  syncRowId: claim.sync.id,
@@ -265,7 +341,8 @@ export async function handleIssueEvent(event: Record<string, unknown>): Promise<
265
341
  await createInitialJiraTask({
266
342
  issueKey,
267
343
  summary,
268
- reporterName,
344
+ reporterAccountId,
345
+ requestedByUserId,
269
346
  descriptionText,
270
347
  issueUrl,
271
348
  syncRowId: claim.sync.id,
@@ -280,8 +357,8 @@ export async function handleIssueEvent(event: Record<string, unknown>): Promise<
280
357
  type CommentShape = {
281
358
  id?: string;
282
359
  body?: unknown;
283
- author?: { accountId?: string; displayName?: string } | null;
284
- updateAuthor?: { accountId?: string; displayName?: string } | null;
360
+ author?: { accountId?: string; displayName?: string; emailAddress?: string } | null;
361
+ updateAuthor?: { accountId?: string; displayName?: string; emailAddress?: string } | null;
285
362
  };
286
363
 
287
364
  /**
@@ -345,7 +422,14 @@ export async function handleCommentEvent(event: Record<string, unknown>): Promis
345
422
  const summary = issue.fields?.summary ?? "(no summary)";
346
423
  const descriptionText = extractText(issue.fields?.description);
347
424
  const commentText = extractText(comment.body);
348
- const commentAuthor = comment.author?.displayName ?? "";
425
+ const commentAuthorAccountId = comment.author?.accountId ?? comment.updateAuthor?.accountId;
426
+ const requestedByUserId = resolveJiraActor(
427
+ commentAuthorAccountId,
428
+ comment.author?.emailAddress ?? comment.updateAuthor?.emailAddress,
429
+ comment.author?.displayName ?? comment.updateAuthor?.displayName,
430
+ "comment_created",
431
+ `${issueKey}: ${commentText.slice(0, 80)}`,
432
+ );
349
433
  const issueUrl = buildIssueUrl(issueKey);
350
434
 
351
435
  if (!existing) {
@@ -372,7 +456,8 @@ export async function handleCommentEvent(event: Record<string, unknown>): Promis
372
456
  summary,
373
457
  descriptionText,
374
458
  commentText,
375
- commentAuthor,
459
+ commentAuthorAccountId,
460
+ requestedByUserId,
376
461
  issueUrl,
377
462
  syncRowId: claim.sync.id,
378
463
  });
@@ -384,7 +469,8 @@ export async function handleCommentEvent(event: Record<string, unknown>): Promis
384
469
  summary,
385
470
  issueUrl,
386
471
  commentText,
387
- commentAuthor,
472
+ commentAuthorAccountId,
473
+ requestedByUserId,
388
474
  syncRow: claim.sync,
389
475
  });
390
476
  }
@@ -394,7 +480,8 @@ export async function handleCommentEvent(event: Record<string, unknown>): Promis
394
480
  summary,
395
481
  issueUrl,
396
482
  commentText,
397
- commentAuthor,
483
+ commentAuthorAccountId,
484
+ requestedByUserId,
398
485
  syncRow: existing,
399
486
  });
400
487
  }
@@ -404,7 +491,8 @@ async function routeCommentOnExistingSync(input: {
404
491
  summary: string;
405
492
  issueUrl: string;
406
493
  commentText: string;
407
- commentAuthor: string;
494
+ commentAuthorAccountId: string | undefined;
495
+ requestedByUserId: string | undefined;
408
496
  syncRow: { id: string; swarmId: string };
409
497
  }): Promise<void> {
410
498
  const priorTask = input.syncRow.swarmId ? getTaskById(input.syncRow.swarmId) : null;
@@ -417,15 +505,17 @@ async function routeCommentOnExistingSync(input: {
417
505
  }
418
506
 
419
507
  // Terminal or orphan sync row → follow-up.
508
+ const commentAuthorDisplay = renderJiraIdentity(input.commentAuthorAccountId);
420
509
  await createInitialJiraTask({
421
510
  issueKey: input.issueKey,
422
511
  summary: input.summary,
423
- reporterName: input.commentAuthor,
512
+ reporterAccountId: input.commentAuthorAccountId,
513
+ requestedByUserId: input.requestedByUserId,
424
514
  descriptionText: "",
425
515
  issueUrl: input.issueUrl,
426
516
  syncRowId: input.syncRow.id,
427
517
  followup: true,
428
- followupTrigger: `New comment from ${input.commentAuthor || "user"}`,
518
+ followupTrigger: `New comment from ${commentAuthorDisplay}`,
429
519
  followupMessage: input.commentText,
430
520
  });
431
521
  }
@@ -453,7 +543,8 @@ export async function handleIssueDeleteEvent(event: Record<string, unknown>): Pr
453
543
  async function createInitialJiraTask(input: {
454
544
  issueKey: string;
455
545
  summary: string;
456
- reporterName: string;
546
+ reporterAccountId: string | undefined;
547
+ requestedByUserId: string | undefined;
457
548
  descriptionText: string;
458
549
  issueUrl: string;
459
550
  syncRowId: string;
@@ -479,7 +570,7 @@ async function createInitialJiraTask(input: {
479
570
  issue_key: input.issueKey,
480
571
  issue_summary: input.summary,
481
572
  issue_url: input.issueUrl,
482
- reporter: input.reporterName,
573
+ reporter: renderJiraIdentity(input.reporterAccountId),
483
574
  description_section: descriptionSection,
484
575
  };
485
576
 
@@ -493,6 +584,7 @@ async function createInitialJiraTask(input: {
493
584
  agentId: lead?.id ?? "",
494
585
  source: "jira",
495
586
  taskType: "jira-issue",
587
+ requestedByUserId: input.requestedByUserId,
496
588
  contextKey: buildJiraContextKey(input.issueKey),
497
589
  });
498
590
 
@@ -509,7 +601,8 @@ async function createCommentMentionTask(input: {
509
601
  summary: string;
510
602
  descriptionText: string;
511
603
  commentText: string;
512
- commentAuthor: string;
604
+ commentAuthorAccountId: string | undefined;
605
+ requestedByUserId: string | undefined;
513
606
  issueUrl: string;
514
607
  syncRowId: string;
515
608
  }): Promise<void> {
@@ -522,7 +615,7 @@ async function createCommentMentionTask(input: {
522
615
  issue_key: input.issueKey,
523
616
  issue_summary: input.summary,
524
617
  issue_url: input.issueUrl,
525
- comment_author: input.commentAuthor,
618
+ comment_author: renderJiraIdentity(input.commentAuthorAccountId),
526
619
  description_section: descriptionSection,
527
620
  comment_text: input.commentText,
528
621
  });
@@ -536,6 +629,7 @@ async function createCommentMentionTask(input: {
536
629
  agentId: lead?.id ?? "",
537
630
  source: "jira",
538
631
  taskType: "jira-issue",
632
+ requestedByUserId: input.requestedByUserId,
539
633
  contextKey: buildJiraContextKey(input.issueKey),
540
634
  });
541
635
 
@@ -23,7 +23,11 @@ Reporter: {{reporter}}
23
23
  { name: "issue_key", description: "Jira issue key (e.g. ENG-123)" },
24
24
  { name: "issue_summary", description: "Issue summary / title" },
25
25
  { name: "issue_url", description: "Issue URL on the Jira site" },
26
- { name: "reporter", description: "Reporter display name (or empty string)" },
26
+ {
27
+ name: "reporter",
28
+ description:
29
+ "Reporter identity — resolved canonical name (e.g. 'Luis (jira:<accountId>)') or the UNKNOWN sentinel",
30
+ },
27
31
  {
28
32
  name: "description_section",
29
33
  description: "Description section (extracted from ADF) or empty string",
@@ -46,7 +50,11 @@ Comment:
46
50
  { name: "issue_key", description: "Jira issue key (e.g. ENG-123)" },
47
51
  { name: "issue_summary", description: "Issue summary / title" },
48
52
  { name: "issue_url", description: "Issue URL on the Jira site" },
49
- { name: "comment_author", description: "Comment author display name" },
53
+ {
54
+ name: "comment_author",
55
+ description:
56
+ "Comment author identity — resolved canonical name (e.g. 'Luis (jira:<accountId>)') or the UNKNOWN sentinel",
57
+ },
50
58
  {
51
59
  name: "description_section",
52
60
  description: "Description section (extracted from ADF) or empty string",