@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.113.0",
3
+ "version": "1.115.0",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -83,9 +83,9 @@
83
83
  "lead": "bun src/cli.tsx lead",
84
84
  "lead:yolo": "bun src/cli.tsx lead --yolo",
85
85
  "dev": "bun --hot src/stdio.ts",
86
- "dev:http": "portless api.swarm bun --hot src/http.ts",
86
+ "dev:http": "NODE_ENV=development portless api.swarm bun --hot src/http.ts",
87
87
  "start": "bun src/stdio.ts",
88
- "start:http": "bun --expose-gc src/http.ts",
88
+ "start:http": "NODE_ENV=development bun --expose-gc src/http.ts",
89
89
  "start:portless": "portless api.swarm bun --expose-gc src/http.ts",
90
90
  "inspector": "bunx @modelcontextprotocol/inspector --transport stdio bun src/stdio.ts",
91
91
  "inspector:http": "bunx @modelcontextprotocol/inspector --transport http https://api.swarm.localhost:1355/mcp",
@@ -4,6 +4,7 @@ import {
4
4
  getAgentMailInboxMapping,
5
5
  getAllAgents,
6
6
  } from "../be/db";
7
+ import { renderIdentity, resolveIdentityByEmail } from "../be/identity";
7
8
  import { findOrCreateUserByEmail } from "../be/users";
8
9
  import { resolveTemplate } from "../prompts/resolver";
9
10
  import { createIngressBuffer } from "../tasks/additive-ingress";
@@ -184,6 +185,15 @@ export async function handleMessageReceived(
184
185
  );
185
186
  requestedByUserId = user.id;
186
187
  }
188
+ // Render the resolved canonical name (or the UNKNOWN sentinel) for
189
+ // agent-visible text — never the raw From header. `findOrCreateUserByEmail`
190
+ // above auto-creates on a real email, so `senderEmail` present implies
191
+ // resolved; a missing/unparseable From header has no machine-carried
192
+ // identifier at all, so it renders the same non-name sentinel shape via
193
+ // the identity primitive rather than echoing the raw header string.
194
+ const senderDisplay = senderEmail
195
+ ? renderIdentity(resolveIdentityByEmail(senderEmail))
196
+ : renderIdentity({ status: "unknown", kind: "email", externalId: "unknown" });
187
197
  const preview = body.length > 500 ? `${body.substring(0, 500)}...` : body;
188
198
 
189
199
  // Emit workflow trigger event
@@ -207,7 +217,7 @@ export async function handleMessageReceived(
207
217
  if (
208
218
  agentmailBuffer.enabled &&
209
219
  agentmailBuffer.maybeBuffer(contextKey, siblingInFlight, {
210
- from,
220
+ from: senderDisplay,
211
221
  subject,
212
222
  inboxId: inbox_id,
213
223
  threadId: thread_id,
@@ -226,7 +236,7 @@ export async function handleMessageReceived(
226
236
 
227
237
  // Create a follow-up task with parentTaskId to continue the session
228
238
  const followupResult = resolveTemplate("agentmail.email.followup", {
229
- from,
239
+ from: senderDisplay,
230
240
  subject,
231
241
  inbox_id,
232
242
  thread_id,
@@ -264,7 +274,7 @@ export async function handleMessageReceived(
264
274
  if (agent.isLead) {
265
275
  // Route to lead as task
266
276
  const leadResult = resolveTemplate("agentmail.email.mapped_lead", {
267
- from,
277
+ from: senderDisplay,
268
278
  subject,
269
279
  inbox_id,
270
280
  thread_id,
@@ -295,7 +305,7 @@ export async function handleMessageReceived(
295
305
 
296
306
  // Route to worker as task
297
307
  const workerResult = resolveTemplate("agentmail.email.mapped_worker", {
298
- from,
308
+ from: senderDisplay,
299
309
  subject,
300
310
  inbox_id,
301
311
  thread_id,
@@ -328,7 +338,7 @@ export async function handleMessageReceived(
328
338
  const lead = findLeadAgent();
329
339
  if (lead) {
330
340
  const unmappedResult = resolveTemplate("agentmail.email.unmapped", {
331
- from,
341
+ from: senderDisplay,
332
342
  subject,
333
343
  inbox_id,
334
344
  thread_id,
@@ -359,7 +369,7 @@ export async function handleMessageReceived(
359
369
 
360
370
  // No lead available - create unassigned task
361
371
  const noAgentResult = resolveTemplate("agentmail.email.no_agent", {
362
- from,
372
+ from: senderDisplay,
363
373
  subject,
364
374
  inbox_id,
365
375
  thread_id,
@@ -21,7 +21,11 @@ Thread: {{thread_id}}
21
21
 
22
22
  {{preview}}`,
23
23
  variables: [
24
- { name: "from", description: "Sender address(es)" },
24
+ {
25
+ name: "from",
26
+ description:
27
+ "Sender identity — resolved canonical name (e.g. 'Luis (email:luis@x.com)') or the UNKNOWN sentinel",
28
+ },
25
29
  { name: "subject", description: "Email subject" },
26
30
  { name: "inbox_id", description: "AgentMail inbox ID" },
27
31
  { name: "thread_id", description: "AgentMail thread ID" },
@@ -41,7 +45,11 @@ Message: {{message_id}}
41
45
 
42
46
  {{preview}}`,
43
47
  variables: [
44
- { name: "from", description: "Sender address(es)" },
48
+ {
49
+ name: "from",
50
+ description:
51
+ "Sender identity — resolved canonical name (e.g. 'Luis (email:luis@x.com)') or the UNKNOWN sentinel",
52
+ },
45
53
  { name: "subject", description: "Email subject" },
46
54
  { name: "inbox_id", description: "AgentMail inbox ID" },
47
55
  { name: "thread_id", description: "AgentMail thread ID" },
@@ -61,7 +69,11 @@ Thread: {{thread_id}}
61
69
 
62
70
  {{preview}}`,
63
71
  variables: [
64
- { name: "from", description: "Sender address(es)" },
72
+ {
73
+ name: "from",
74
+ description:
75
+ "Sender identity — resolved canonical name (e.g. 'Luis (email:luis@x.com)') or the UNKNOWN sentinel",
76
+ },
65
77
  { name: "subject", description: "Email subject" },
66
78
  { name: "inbox_id", description: "AgentMail inbox ID" },
67
79
  { name: "thread_id", description: "AgentMail thread ID" },
@@ -81,7 +93,11 @@ Message: {{message_id}}
81
93
 
82
94
  {{preview}}`,
83
95
  variables: [
84
- { name: "from", description: "Sender address(es)" },
96
+ {
97
+ name: "from",
98
+ description:
99
+ "Sender identity — resolved canonical name (e.g. 'Luis (email:luis@x.com)') or the UNKNOWN sentinel",
100
+ },
85
101
  { name: "subject", description: "Email subject" },
86
102
  { name: "inbox_id", description: "AgentMail inbox ID" },
87
103
  { name: "thread_id", description: "AgentMail thread ID" },
@@ -101,7 +117,11 @@ Thread: {{thread_id}}
101
117
 
102
118
  {{preview}}`,
103
119
  variables: [
104
- { name: "from", description: "Sender address(es)" },
120
+ {
121
+ name: "from",
122
+ description:
123
+ "Sender identity — resolved canonical name (e.g. 'Luis (email:luis@x.com)') or the UNKNOWN sentinel",
124
+ },
105
125
  { name: "subject", description: "Email subject" },
106
126
  { name: "inbox_id", description: "AgentMail inbox ID" },
107
127
  { name: "thread_id", description: "AgentMail thread ID" },
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage } from "node:http";
2
2
  import { getRequestAuth } from "../utils/request-auth-context";
3
- import { getTaskById } from "./db";
3
+ import { getAgentCurrentTask, getTaskById } from "./db";
4
+ import { findUserByExternalId } from "./users";
4
5
 
5
6
  /**
6
7
  * Resolve the trusted audit-actor user id for a write to an audited table
@@ -14,6 +15,13 @@ import { getTaskById } from "./db";
14
15
  * attribute a write to a task they already own, whose requester is legitimately
15
16
  * theirs.
16
17
  *
18
+ * Two fallbacks layer on top of the header, both still gated by ownership:
19
+ * 1. No header at all → the caller's own current in-progress task (never a
20
+ * client-supplied id, so this can't be spoofed).
21
+ * 2. The resolved task has no `requestedByUserId` but carries a
22
+ * machine-recorded provider external id (today: the Slack user field) →
23
+ * the same generic reverse lookup used everywhere else.
24
+ *
17
25
  * Returns `null` when no trusted actor can be established — which leaves the
18
26
  * audit column untouched on updates and NULL on inserts.
19
27
  */
@@ -21,13 +29,29 @@ export function resolveTaskAuditUserId(
21
29
  sourceTaskId: string | undefined,
22
30
  callerAgentId: string | undefined,
23
31
  ): string | null {
24
- if (!sourceTaskId || !callerAgentId) return null;
25
- const task = getTaskById(sourceTaskId);
32
+ if (!callerAgentId) return null;
33
+
34
+ let resolvedSourceTaskId = sourceTaskId;
35
+ if (!resolvedSourceTaskId) {
36
+ const currentTask = getAgentCurrentTask(callerAgentId);
37
+ if (currentTask) resolvedSourceTaskId = currentTask.id;
38
+ }
39
+ if (!resolvedSourceTaskId) return null;
40
+
41
+ const task = getTaskById(resolvedSourceTaskId);
26
42
  if (!task) return null;
27
43
  // Bind the header to the caller's own task — otherwise it is just a
28
44
  // client-chosen value and its requester cannot be trusted.
29
45
  if (task.agentId !== callerAgentId) return null;
30
- return task.requestedByUserId ?? null;
46
+
47
+ if (task.requestedByUserId) return task.requestedByUserId;
48
+
49
+ if (task.slackUserId) {
50
+ const user = findUserByExternalId("slack", task.slackUserId);
51
+ if (user) return user.id;
52
+ }
53
+
54
+ return null;
31
55
  }
32
56
 
33
57
  /**
@@ -113,6 +113,7 @@ export function storeOAuthTokens(
113
113
  scope?: string | null;
114
114
  },
115
115
  ): void {
116
+ // TODO(secrets-cipher): encrypt OAuth tokens at rest with src/be/crypto/secrets-cipher.ts.
116
117
  getDb()
117
118
  .query(
118
119
  `INSERT INTO oauth_tokens (provider, accessToken, refreshToken, expiresAt, scope)
@@ -170,6 +171,47 @@ export function updateOAuthTokensAfterRefresh(
170
171
  throw new Error(`OAuth token refresh persistence failed for ${provider}: no rows updated`);
171
172
  }
172
173
 
174
+ /**
175
+ * Presence-flags-only projection of oauth_tokens for the background refresh
176
+ * sweep. Deliberately excludes token values so sweep code can never leak
177
+ * them into logs.
178
+ */
179
+ export type OAuthTokenSweepRow = {
180
+ provider: string;
181
+ hasApp: boolean;
182
+ hasRefreshToken: boolean;
183
+ expiresAt: string;
184
+ updatedAt: string;
185
+ };
186
+
187
+ export function listOAuthTokenSweepRows(): OAuthTokenSweepRow[] {
188
+ const rows = getDb()
189
+ .query(
190
+ `SELECT t.provider,
191
+ CASE WHEN a.provider IS NOT NULL THEN 1 ELSE 0 END AS hasApp,
192
+ CASE WHEN t.refreshToken IS NOT NULL AND t.refreshToken != '' THEN 1 ELSE 0 END AS hasRefreshToken,
193
+ t.expiresAt,
194
+ t.updatedAt
195
+ FROM oauth_tokens t
196
+ LEFT JOIN oauth_apps a ON a.provider = t.provider
197
+ ORDER BY t.provider ASC`,
198
+ )
199
+ .all() as Array<{
200
+ provider: string;
201
+ hasApp: number;
202
+ hasRefreshToken: number;
203
+ expiresAt: string;
204
+ updatedAt: string;
205
+ }>;
206
+ return rows.map((row) => ({
207
+ provider: row.provider,
208
+ hasApp: row.hasApp === 1,
209
+ hasRefreshToken: row.hasRefreshToken === 1,
210
+ expiresAt: normalizeDateRequired(row.expiresAt),
211
+ updatedAt: normalizeDateRequired(row.updatedAt),
212
+ }));
213
+ }
214
+
173
215
  export function deleteOAuthTokens(provider: string): void {
174
216
  getDb().query("DELETE FROM oauth_tokens WHERE provider = ?").run(provider);
175
217
  }