@desplega.ai/agent-swarm 1.145.0 → 1.147.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 (221) hide show
  1. package/README.md +2 -1
  2. package/dist/{acp-adapter-580t41c0.js → acp-adapter-9n319wqc.js} +61 -6
  3. package/dist/{actions-m662tjng.js → actions-2vxqvpr9.js} +9 -8
  4. package/dist/{app-zb3rndm8.js → app-w066xfy0.js} +6 -5
  5. package/dist/assistant-kt31dj2h.js +236 -0
  6. package/dist/{boot-reembed-072x00vn.js → boot-reembed-az5rassp.js} +7 -6
  7. package/dist/{boot-reembed-65zz2smt.js → boot-reembed-j3mm3rfz.js} +8 -7
  8. package/dist/{boot-scrub-logs-yz0ams04.js → boot-scrub-logs-b6h54817.js} +4 -4
  9. package/dist/{claude-adapter-paj270y2.js → claude-adapter-w1z04ann.js} +8 -6
  10. package/dist/{claude-managed-setup-w619jncn.js → claude-managed-setup-f5pmsyrp.js} +24 -3
  11. package/dist/{claude-sdk-session-swt80j9z.js → claude-sdk-session-f0cc1kec.js} +8 -6
  12. package/dist/{cli-dt553ktb.js → cli-0b6y7kxm.js} +1 -1
  13. package/dist/{cli-bxdhw615.js → cli-12fz972k.js} +4 -4
  14. package/dist/{cli-s2f20r5w.js → cli-1th1728g.js} +2 -2
  15. package/dist/{cli-rs6ec3b9.js → cli-3tgwnf43.js} +3 -3
  16. package/dist/{cli-gxnzc6wh.js → cli-4nr988h6.js} +1 -1
  17. package/dist/{cli-axs8e6d4.js → cli-53s590z8.js} +1 -1
  18. package/dist/{cli-ygbjp3z8.js → cli-54wwve05.js} +5 -4
  19. package/dist/{cli-v0asr5yv.js → cli-719k9j2c.js} +2 -2
  20. package/dist/{cli-anktweqr.js → cli-89xfd7f9.js} +21 -47
  21. package/dist/{cli-q34a0e10.js → cli-8v0g1yc8.js} +1 -1
  22. package/dist/cli-9d8ntetj.js +42 -0
  23. package/dist/{cli-z9fprcrf.js → cli-atcve9y8.js} +881 -738
  24. package/dist/{cli-jmb7g47h.js → cli-b5bq9crk.js} +14 -31
  25. package/dist/cli-d8brzxjb.js +190 -0
  26. package/dist/{cli-gzky0gkr.js → cli-de14znh8.js} +2 -2
  27. package/dist/{cli-xytwff7f.js → cli-dttph1bw.js} +2 -2
  28. package/dist/cli-ep19k3mb.js +19 -0
  29. package/dist/{cli-m1xa8be7.js → cli-f146mn5s.js} +7 -50
  30. package/dist/{cli-1fj0chdk.js → cli-f7dsy61y.js} +5 -3
  31. package/dist/{cli-f567d5c1.js → cli-ftht3zzj.js} +2782 -2589
  32. package/dist/{cli-a0g1gbt5.js → cli-hbv0kq6w.js} +2 -2
  33. package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
  34. package/dist/{cli-7fnkj1hj.js → cli-kpv03zhj.js} +21 -17
  35. package/dist/{cli-m4j6trn4.js → cli-m8yrsg97.js} +2 -2
  36. package/dist/cli-m9sxbkhm.js +779 -0
  37. package/dist/cli-mmemxxdc.js +823 -0
  38. package/dist/{cli-tqtvj5pt.js → cli-n8508nre.js} +1 -1
  39. package/dist/{cli-z96t0thj.js → cli-p1d5b073.js} +37 -2
  40. package/dist/{cli-gh8y8f36.js → cli-rw51bq3j.js} +13 -10
  41. package/dist/{cli-59ej1c07.js → cli-smby5m17.js} +7 -6
  42. package/dist/{cli-65vqzz3v.js → cli-tzvk9haz.js} +94 -372
  43. package/dist/{cli-dc9k2ba9.js → cli-w9kb3bk1.js} +1 -1
  44. package/dist/{cli-kpev657q.js → cli-y6b6fb76.js} +25 -3
  45. package/dist/{cli-123q2get.js → cli-yz3djrm0.js} +9 -9
  46. package/dist/{cli-wvp6bvy4.js → cli-zhjmqxzh.js} +1 -1
  47. package/dist/cli.js +13 -13
  48. package/dist/{codex-adapter-k98vaw2r.js → codex-adapter-zw4dwsx7.js} +2 -2
  49. package/dist/{codex-hook-9pf19cks.js → codex-hook-bw8p5nh4.js} +2 -2
  50. package/dist/{codex-session-runner-tngtphx7.js → codex-session-runner-k981s65v.js} +2 -2
  51. package/dist/{commands-y8nbr3fv.js → commands-sykrxx7e.js} +4 -4
  52. package/dist/{db-30p4h2t1.js → db-sfbsc8wt.js} +6 -4
  53. package/dist/handlers-0aspcejm.js +603 -0
  54. package/dist/{hook-dcw4jfbj.js → hook-4nvqj2sb.js} +7 -5
  55. package/dist/{hook-21jexvfx.js → hook-hxccs7m5.js} +24 -49
  56. package/dist/{http-r4kq5g8d.js → http-y06z517d.js} +230 -94
  57. package/dist/{index-q3qja6ta.js → index-hssww7kj.js} +15 -14
  58. package/dist/{index-gay66b9w.js → index-k8znfx0z.js} +13 -12
  59. package/dist/{index-53zyht4z.js → index-tce1rss4.js} +371 -110
  60. package/dist/{index-nqvf26za.js → index-x5j894zf.js} +14 -13
  61. package/dist/{keepalive-xs8fa55y.js → keepalive-w733ax66.js} +6 -6
  62. package/dist/{lead-d9m2t4s0.js → lead-75bjqa9d.js} +29 -27
  63. package/dist/{maintenance-3zh82g6z.js → maintenance-w4f1zjck.js} +8 -7
  64. package/dist/{oauth-refresh-sweep-03snsye5.js → oauth-refresh-sweep-wdetky26.js} +5 -5
  65. package/dist/{onboard-0cg1925x.js → onboard-bb5net8s.js} +2 -2
  66. package/dist/{otel-impl-0ab95z27.js → otel-impl-0w7c14ap.js} +1 -1
  67. package/dist/{pi-mono-adapter-dtkz4e7j.js → pi-mono-adapter-2has4gnp.js} +12 -2
  68. package/dist/{pricing-refresh-e5jhzz4j.js → pricing-refresh-9nzsrgcq.js} +6 -6
  69. package/dist/{rbac-roles-p1mf40z3.js → rbac-roles-r3hqjp09.js} +5 -5
  70. package/dist/{rbac-roles-2w3cmrkc.js → rbac-roles-z1jrffbt.js} +4 -4
  71. package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
  72. package/dist/{render-v2-xhawry3k.js → render-v2-vbr89fnb.js} +6 -5
  73. package/dist/{seed-pricing-t223sw91.js → seed-pricing-g62hy1pk.js} +7 -5
  74. package/dist/{setup-zcs0rce7.js → setup-gnfqnptp.js} +2 -2
  75. package/dist/{worker-qnp2ka62.js → worker-kznxp4qm.js} +29 -27
  76. package/openapi.json +263 -12
  77. package/package.json +1 -1
  78. package/plugin/commands/start-worker.md +4 -9
  79. package/plugin/pi-skills/start-worker/SKILL.md +4 -9
  80. package/src/be/db/agents.ts +556 -0
  81. package/src/be/db/context-versions.ts +117 -0
  82. package/src/be/db/runtime.ts +406 -0
  83. package/src/be/db/tasks/read.ts +1167 -0
  84. package/src/be/db/tasks/write.ts +953 -0
  85. package/src/be/db.ts +584 -3429
  86. package/src/be/migrations/147_scheduled_tasks_parent_task.sql +7 -0
  87. package/src/be/migrations/148_pages_svg.sql +123 -0
  88. package/src/be/migrations/149_session_tokens.sql +24 -0
  89. package/src/be/scripts/typecheck.ts +1 -0
  90. package/src/be/seed-pricing.ts +13 -8
  91. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  92. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  93. package/src/be/seed-scripts/catalog/star-history-refresh.ts +101 -0
  94. package/src/be/seed-scripts/catalog/star-history-renderer.ts +119 -0
  95. package/src/be/seed-scripts/index.ts +17 -0
  96. package/src/be/seed-skills/index.ts +14 -0
  97. package/src/be/swarm-config-guard.ts +5 -2
  98. package/src/be/task-attachment-store.ts +70 -0
  99. package/src/be/users.ts +77 -0
  100. package/src/commands/attachments-section.ts +37 -0
  101. package/src/commands/claude-managed-setup.ts +45 -3
  102. package/src/commands/claude-md-session.ts +408 -0
  103. package/src/commands/context-preamble.ts +15 -3
  104. package/src/commands/profile-sync.ts +205 -5
  105. package/src/commands/provider-credentials.ts +60 -27
  106. package/src/commands/runner.ts +63 -58
  107. package/src/commands/templates.ts +25 -2
  108. package/src/hooks/hook.ts +26 -67
  109. package/src/http/agents.ts +40 -1
  110. package/src/http/auth.ts +13 -1
  111. package/src/http/fs.ts +21 -51
  112. package/src/http/index.ts +2 -0
  113. package/src/http/mcp.ts +32 -0
  114. package/src/http/pages-public.ts +20 -4
  115. package/src/http/pages.ts +11 -3
  116. package/src/http/poll.ts +4 -0
  117. package/src/http/repos.ts +3 -3
  118. package/src/http/sessions.ts +82 -0
  119. package/src/http/tasks.ts +10 -2
  120. package/src/http/utils.ts +79 -1
  121. package/src/prompts/session-templates.ts +12 -8
  122. package/src/providers/acp-adapter.ts +62 -8
  123. package/src/providers/pi-mono-adapter.ts +23 -6
  124. package/src/providers/pricing-sources.md +11 -0
  125. package/src/scheduler/scheduler.test.ts +49 -2
  126. package/src/scheduler/scheduler.ts +4 -0
  127. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  128. package/src/scripts-runtime/types/stdlib.d.ts +8 -0
  129. package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
  130. package/src/server.ts +2 -0
  131. package/src/slack/assistant.ts +87 -46
  132. package/src/slack/handlers.test.ts +1 -3
  133. package/src/slack/handlers.ts +102 -87
  134. package/src/slack/inbound-files.ts +466 -0
  135. package/src/slack/templates.ts +14 -0
  136. package/src/tasks/task-terminal-effects.ts +158 -0
  137. package/src/tests/acp-adapter.test.ts +369 -117
  138. package/src/tests/budget-claim-gate.test.ts +1 -0
  139. package/src/tests/build-pi-skills.test.ts +5 -7
  140. package/src/tests/claude-managed-setup.test.ts +52 -4
  141. package/src/tests/claude-md-session.test.ts +603 -0
  142. package/src/tests/codex-skill-resolver.test.ts +19 -0
  143. package/src/tests/context-versioning.test.ts +107 -0
  144. package/src/tests/cors-allowlist.test.ts +309 -0
  145. package/src/tests/credential-check.test.ts +157 -14
  146. package/src/tests/db-agents.test.ts +91 -0
  147. package/src/tests/db-context-versions.test.ts +37 -0
  148. package/src/tests/db-extraction-contract.test.ts +45 -0
  149. package/src/tests/db-runtime.test.ts +70 -0
  150. package/src/tests/db-task-read-contract.test.ts +624 -0
  151. package/src/tests/db-task-write-contract.test.ts +653 -0
  152. package/src/tests/db-tasks-read.test.ts +183 -0
  153. package/src/tests/db-tasks-write.test.ts +230 -0
  154. package/src/tests/defer-task.test.ts +389 -0
  155. package/src/tests/docs-index-footprint.test.js +2 -2
  156. package/src/tests/e2e-visuals-stage.test.ts +151 -0
  157. package/src/tests/file-lock.test.ts +144 -0
  158. package/src/tests/fixtures/file-lock-holder.ts +13 -0
  159. package/src/tests/fixtures/hold-file-lock.ts +25 -0
  160. package/src/tests/heartbeat-expiry.test.ts +188 -0
  161. package/src/tests/http-api-integration.test.ts +78 -1
  162. package/src/tests/pages-public-html.test.ts +43 -0
  163. package/src/tests/pages-svg-migration.test.ts +42 -0
  164. package/src/tests/preload.ts +4 -1
  165. package/src/tests/profile-sync-warning.test.ts +16 -0
  166. package/src/tests/profile-sync.test.ts +126 -0
  167. package/src/tests/prompt-template-session.test.ts +20 -4
  168. package/src/tests/provider-registration.test.ts +93 -0
  169. package/src/tests/rbac-e2e-helpers.test.ts +53 -0
  170. package/src/tests/rbac-e2e-helpers.ts +19 -2
  171. package/src/tests/reload-config.test.ts +26 -0
  172. package/src/tests/routing-affinity-inventory.test.ts +1 -1
  173. package/src/tests/runner-attachments-section.test.ts +22 -2
  174. package/src/tests/runner-context-preamble.test.ts +30 -0
  175. package/src/tests/runner-deployment-resume.test.ts +107 -0
  176. package/src/tests/runner-repo-autostash.test.ts +12 -0
  177. package/src/tests/script-connections.test.ts +4 -0
  178. package/src/tests/seed-scripts.test.ts +2 -2
  179. package/src/tests/session-tokens.test.ts +279 -0
  180. package/src/tests/slack-file-tools-attachments.test.ts +408 -0
  181. package/src/tests/slack-inbound-files.test.ts +847 -0
  182. package/src/tests/star-history-refresh.test.ts +102 -0
  183. package/src/tests/swarm-config-reserved-keys.test.ts +21 -0
  184. package/src/tests/swarm-repos.test.ts +20 -0
  185. package/src/tests/tool-annotations.test.ts +4 -3
  186. package/src/tests/ui-logs-parser.test.ts +176 -0
  187. package/src/tools/create-page.ts +4 -2
  188. package/src/tools/defer-task.ts +239 -0
  189. package/src/tools/repos/update-repo.ts +2 -2
  190. package/src/tools/schedules/create-schedule.ts +1 -0
  191. package/src/tools/schedules/list-schedules.ts +1 -0
  192. package/src/tools/schedules/patch-schedule.ts +1 -0
  193. package/src/tools/schedules/update-schedule.ts +1 -0
  194. package/src/tools/slack-download-file.ts +60 -29
  195. package/src/tools/slack-read.ts +73 -32
  196. package/src/tools/store-progress.ts +16 -146
  197. package/src/tools/tool-config.ts +1 -0
  198. package/src/tools/utils.ts +21 -0
  199. package/src/types.ts +37 -4
  200. package/src/utils/acp-session-token.ts +56 -0
  201. package/src/utils/file-lock.ts +111 -0
  202. package/src/utils/heartbeat-expiry.ts +78 -0
  203. package/src/utils/request-auth-context.ts +2 -0
  204. package/src/utils/task-attachment-links.ts +22 -0
  205. package/templates/skills/code-quality/content.md +8 -5
  206. package/templates/skills/download-task-attachment/content.md +2 -1
  207. package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
  208. package/templates/skills/review-offered-task/config.json +14 -0
  209. package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
  210. package/templates/skills/scheduling/content.md +4 -0
  211. package/templates/skills/slack-interaction/content.md +2 -2
  212. package/templates/skills/work-on-task/SKILL.md +16 -0
  213. package/templates/skills/work-on-task/config.json +14 -0
  214. package/templates/skills/work-on-task/content.md +11 -0
  215. package/dist/assistant-0qt42nzg.js +0 -212
  216. package/dist/cli-fs1kkqgr.js +0 -317
  217. package/dist/cli-jnf5zghe.js +0 -269
  218. package/dist/cli-n1q8q14d.js +0 -177
  219. package/dist/handlers-yfp1qn81.js +0 -592
  220. package/plugin/commands/work-on-task.md +0 -39
  221. package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
@@ -2,7 +2,9 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
3
  import { countSessions, getRootTaskChain, getTaskById, listRecentSessions } from "../be/db";
4
4
  import { getTaskSteeringFields } from "../be/steering";
5
+ import { mintSessionToken, revokeSessionToken } from "../be/users";
5
6
  import { AgentTaskSchema, AgentTaskStatusSchema, SteerModeSchema } from "../types";
7
+ import { getRequestAuth } from "../utils/request-auth-context";
6
8
  import { route } from "./route-def";
7
9
  import { jsonError } from "./utils";
8
10
 
@@ -68,6 +70,57 @@ const TaskWithSteeringSchema = AgentTaskSchema.extend({
68
70
 
69
71
  // ─── Route Definitions ───────────────────────────────────────────────────────
70
72
 
73
+ /** Maximum TTL for an ephemeral session token: 7 days. */
74
+ const MAX_SESSION_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
75
+
76
+ const mintSessionTokenRoute = route({
77
+ method: "post",
78
+ path: "/api/sessions/tokens",
79
+ pattern: ["api", "sessions", "tokens"],
80
+ summary: "Mint an ephemeral session token for an ACP provider session",
81
+ description:
82
+ "Returns a short-lived aseph_ bearer for use by the ACP adapter in place of the full " +
83
+ "operator key. The token expires at the requested TTL and is actively revoked when the " +
84
+ "session ends. Only the operator key may mint session tokens.",
85
+ tags: ["Sessions"],
86
+ body: z.object({
87
+ agentId: z.string().min(1),
88
+ taskId: z.string().min(1),
89
+ ttlMs: z.number().int().positive().max(MAX_SESSION_TOKEN_TTL_MS),
90
+ }),
91
+ responses: {
92
+ 200: {
93
+ description: "Minted token plaintext (returned once) and its stable token ID",
94
+ schema: z.object({
95
+ tokenId: z.string(),
96
+ plaintext: z.string(),
97
+ }),
98
+ },
99
+ 400: { description: "Validation error" },
100
+ 401: { description: "Unauthorized" },
101
+ },
102
+ auth: { apiKey: true },
103
+ rbac: { ungated: "operator-only: minting is gated on the full operator key in auth middleware" },
104
+ });
105
+
106
+ const revokeSessionTokenRoute = route({
107
+ method: "delete",
108
+ path: "/api/sessions/tokens/{tokenId}",
109
+ pattern: ["api", "sessions", "tokens", null],
110
+ summary: "Revoke an ephemeral session token",
111
+ description: "Revokes an aseph_ bearer token by ID. No-op if already revoked.",
112
+ tags: ["Sessions"],
113
+ params: z.object({ tokenId: z.string() }),
114
+ responses: {
115
+ 204: { description: "Token revoked (or was already revoked)" },
116
+ 401: { description: "Unauthorized" },
117
+ },
118
+ auth: { apiKey: true },
119
+ rbac: {
120
+ ungated: "operator-only: revocation is gated on the full operator key in auth middleware",
121
+ },
122
+ });
123
+
71
124
  const listSessions = route({
72
125
  method: "get",
73
126
  path: "/api/sessions",
@@ -136,6 +189,35 @@ export async function handleSessions(
136
189
  pathSegments: string[],
137
190
  queryParams: URLSearchParams,
138
191
  ): Promise<boolean> {
192
+ if (mintSessionTokenRoute.match(req.method, pathSegments)) {
193
+ // Restrict to the full operator key — aseph_ and aswt_ bearers must not be
194
+ // able to mint new session tokens.
195
+ if (getRequestAuth(req)?.kind !== "operator") {
196
+ jsonError(res, "Unauthorized", 401);
197
+ return true;
198
+ }
199
+ const parsed = await mintSessionTokenRoute.parse(req, res, pathSegments, queryParams);
200
+ if (!parsed) return true;
201
+ const { agentId, taskId, ttlMs } = parsed.body;
202
+ const token = await mintSessionToken(agentId, taskId, ttlMs);
203
+ mintSessionTokenRoute.respond(res, 200, { tokenId: token.tokenId, plaintext: token.plaintext });
204
+ return true;
205
+ }
206
+
207
+ if (revokeSessionTokenRoute.match(req.method, pathSegments)) {
208
+ // Restrict revocation to the operator key as well.
209
+ if (getRequestAuth(req)?.kind !== "operator") {
210
+ jsonError(res, "Unauthorized", 401);
211
+ return true;
212
+ }
213
+ const parsed = await revokeSessionTokenRoute.parse(req, res, pathSegments, queryParams);
214
+ if (!parsed) return true;
215
+ await revokeSessionToken(parsed.params.tokenId);
216
+ res.writeHead(204);
217
+ res.end();
218
+ return true;
219
+ }
220
+
139
221
  if (listSessions.match(req.method, pathSegments)) {
140
222
  const parsed = await listSessions.parse(req, res, pathSegments, queryParams);
141
223
  if (!parsed) return true;
package/src/http/tasks.ts CHANGED
@@ -515,7 +515,9 @@ const listPausedTasks = route({
515
515
  responses: {
516
516
  200: {
517
517
  description: "Paused task list",
518
- schema: z.object({ tasks: z.array(AgentTaskSchema) }),
518
+ schema: z.object({
519
+ tasks: z.array(AgentTaskSchema.extend({ attachments: z.array(TaskAttachmentSchema) })),
520
+ }),
519
521
  },
520
522
  },
521
523
  });
@@ -1449,7 +1451,13 @@ export async function handleTasks(
1449
1451
  return true;
1450
1452
  }
1451
1453
  const pausedTasks = await getPausedTasksForAgent(myAgentId);
1452
- listPausedTasks.respond(res, 200, { tasks: pausedTasks });
1454
+ const tasks = await Promise.all(
1455
+ pausedTasks.map(async (task) => ({
1456
+ ...task,
1457
+ attachments: await getTaskAttachments(task.id),
1458
+ })),
1459
+ );
1460
+ listPausedTasks.respond(res, 200, { tasks });
1453
1461
  return true;
1454
1462
  }
1455
1463
 
package/src/http/utils.ts CHANGED
@@ -1,19 +1,97 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { getActiveTaskCount } from "../be/db";
3
3
  import type { SwarmSpan } from "../otel";
4
+ import { isEnvFlagEnabled } from "../utils/env-flag";
4
5
  import { scrubSecrets } from "../utils/secret-scrubber";
5
6
 
7
+ const DEFAULT_ALLOWED_ORIGINS = [
8
+ "https://*.agent-swarm.dev",
9
+ "https://*.agent-swarm.cloud",
10
+ "http://localhost:5274",
11
+ "http://127.0.0.1:5274",
12
+ "http://[::1]:5274",
13
+ "https://ui.swarm.localhost:1355",
14
+ ];
15
+
16
+ let warnedAboutAllowAnyOrigin = false;
17
+
18
+ /** Warn at boot (after config injection), or on first use of the deployment opt-out. */
19
+ export function warnIfCorsAllowsAnyOrigin(): void {
20
+ if (isEnvFlagEnabled("CORS_ALLOW_ANY_ORIGIN", false) && !warnedAboutAllowAnyOrigin) {
21
+ warnedAboutAllowAnyOrigin = true;
22
+ console.warn(
23
+ "[CORS] CORS_ALLOW_ANY_ORIGIN=true: any request origin can receive non-credentialed responses; cookie-authenticated responses still require CORS_ALLOWED_ORIGINS. Set CORS_ALLOWED_ORIGINS and disable CORS_ALLOW_ANY_ORIGIN to restrict access.",
24
+ );
25
+ }
26
+ }
27
+
28
+ /** Re-read env on each request so Configuration reloads take effect immediately. */
29
+ function getAllowedOrigins(): string[] {
30
+ const raw = process.env.CORS_ALLOWED_ORIGINS;
31
+ if (!raw || !raw.trim()) return DEFAULT_ALLOWED_ORIGINS;
32
+ return raw
33
+ .split(",")
34
+ .map((entry) => entry.trim())
35
+ .filter(Boolean);
36
+ }
37
+
38
+ /**
39
+ * Decide whether `origin` may receive credentialed CORS headers.
40
+ * Unset or blank uses the built-in hosted/dev allowlist. Exact entries retain
41
+ * their case-sensitive string comparison. A single leading `*.` matches one or
42
+ * more complete hostname labels, case-insensitively, but never the apex (list
43
+ * it separately). Wildcard schemes and explicit ports must match exactly.
44
+ * Bare `*`, `https://*`, other wildcard positions, and non-origin URLs are ignored.
45
+ */
46
+ export function isOriginAllowedForCredentials(origin: string): boolean {
47
+ const allowed = getAllowedOrigins().some((entry) => {
48
+ if (!entry.includes("*")) return entry === origin;
49
+
50
+ // Fixed parsers, never a regex interpolated from operator-controlled text.
51
+ const pattern = /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/\*\.([a-zA-Z0-9.-]+)(:\d+)?$/.exec(entry);
52
+ const candidate = /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([a-zA-Z0-9.-]+)(:\d+)?$/.exec(origin);
53
+ if (!pattern || !candidate) return false;
54
+ if (pattern[1] !== candidate[1] || pattern[3] !== candidate[3]) return false;
55
+
56
+ const suffix = pattern[2]!.toLowerCase();
57
+ const hostname = candidate[2]!.toLowerCase();
58
+ // Reject empty or malformed labels instead of accepting suffix lookalikes.
59
+ const validLabel = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
60
+ if (![...suffix.split("."), ...hostname.split(".")].every((label) => validLabel.test(label))) {
61
+ return false;
62
+ }
63
+ return hostname.endsWith(`.${suffix}`);
64
+ });
65
+ if (!allowed) {
66
+ console.warn(
67
+ `[CORS] Denied credentialed origin ${scrubSecrets(JSON.stringify(origin.slice(0, 512)))}; add the trusted SPA origin to CORS_ALLOWED_ORIGINS (custom values replace hosted/dev defaults).`,
68
+ );
69
+ }
70
+ return allowed;
71
+ }
72
+
6
73
  export function setCorsHeaders(req: IncomingMessage, res: ServerResponse) {
7
74
  // Echo the request Origin (rather than emitting `*`) so credentialed fetches
8
75
  // — e.g. the SPA's `credentials: 'include'` calls to `/p/:id.json` and the
9
76
  // page-session cookie endpoints — pass the browser's CORS check. A wildcard
10
77
  // would force the browser to reject any credentialed cross-origin response.
78
+ //
79
+ // Only allowlisted origins receive a credentialed CORS grant.
11
80
  const rawOrigin = req.headers.origin;
12
81
  const origin = Array.isArray(rawOrigin) ? rawOrigin[0] : rawOrigin;
82
+ const allowCredentials = origin ? isOriginAllowedForCredentials(origin) : false;
83
+ const allowAnyOrigin = isEnvFlagEnabled("CORS_ALLOW_ANY_ORIGIN", false);
84
+ if (allowAnyOrigin) warnIfCorsAllowsAnyOrigin();
85
+ if (origin && !allowCredentials && !allowAnyOrigin) {
86
+ res.setHeader("Vary", "Origin");
87
+ return;
88
+ }
13
89
  if (origin) {
14
90
  res.setHeader("Access-Control-Allow-Origin", origin);
15
91
  res.setHeader("Vary", "Origin");
16
- res.setHeader("Access-Control-Allow-Credentials", "true");
92
+ // The compatibility flag never grants access to browser-supplied cookies.
93
+ // Bearer clients from unlisted origins must use credentials: "omit".
94
+ if (allowCredentials) res.setHeader("Access-Control-Allow-Credentials", "true");
17
95
  // When credentials are involved the spec disallows wildcards in
18
96
  // Allow-Headers / Allow-Methods / Expose-Headers — they must be
19
97
  // explicit. Echo whatever the preflight asked for (defensive default
@@ -74,7 +74,8 @@ Choose the path by the shape of the work:
74
74
  Store progress with \`store-progress\` at each milestone. A milestone is a result the lead could act on.
75
75
  The task is done when \`store-progress\` carries status \`completed\` and an \`output\` that names the result and every artifact link. On failure, status \`failed\` and a \`failureReason\` that names what you tried.
76
76
  When the task carries an \`outputSchema\`, \`output\` is JSON that matches it.
77
- When you are blocked after real effort, store the blocker with \`store-progress\` and keep working on what you can. When nothing is left to do, fail the task with a \`failureReason\` that names the blocker. The lead reads both.
77
+ When you are blocked after real effort, store the blocker with \`store-progress\` and keep working on what you can.
78
+ The task has four endings. When you are done: \`completed\`. When the answer needs time (a build, a deploy, a reply): \`defer-task\` with a summary of what you did, when to wake up, and what to check. It completes the task now; the wake-up task continues it. When a person must decide: \`request-human-input\`. When nothing else is possible: \`failed\` with the blocker.
78
79
  `,
79
80
  variables: [],
80
81
  category: "system",
@@ -89,9 +90,10 @@ registerTemplate({
89
90
  Your output is delegation and review. Workers implement, research, analyze, and write. Data gathering, even a quick query, goes to a worker. You answer simple factual questions yourself.
90
91
 
91
92
  \`get-swarm\` is the roster. Route by capability and load.
93
+ Delegate with \`send-task\`. Read a child's result with \`get-task-details\`.
92
94
  A task states the goal, the repo URL when there is one, and the constraints. Workers know git, the skills, and \`store-progress\`.
93
95
  Delegate by the shape of the work: a workflow for multi-step or fan-out work, a schedule for recurring work, a script for bulk data, an inline \`script-run\` for a one-off bulk job you can run yourself. The \`workflow-iterate\`, \`scheduling\`, and \`swarm-scripts\` skills build them.
94
- Research or exploration: tell the worker to use \`/researching\`. A large feature: a \`/planning\` task first, then an \`/implementing\` task with \`parentTaskId\`. A small fix: direct implementation.
96
+ Research or exploration: tell the worker to use the \`researching\` skill. A large feature: a task for the \`planning\` skill first, then a task for the \`implementing\` skill with \`parentTaskId\`. A small fix: direct implementation.
95
97
  A follow-up that continues earlier work carries \`parentTaskId\`. The worker receives the prior context.
96
98
  A task whose result depends on the workers' output: wait for the children with the \`wait-for-task\` script, then merge and complete the task yourself. A turn that ends with children still running leaves the task unfinished.
97
99
 
@@ -191,10 +193,11 @@ registerTemplate({
191
193
  defaultBody: `
192
194
  ## Outputs
193
195
 
194
- agent-fs is the shared drive between agents and the people you work with. A file a person will review, edit, or keep goes there. Write with the \`agent-fs\` CLI. See the \`agent-fs\` skill.
195
- A report or summary a person will read: publish a page with \`create_page\`. See the \`pages\` skill.
196
+ Short answers: inline in the task output.
197
+ agent-fs is the shared drive between agents and people. Documents (reports, digests, summaries, specs, research, drafts): Markdown files via its CLI; see \`agent-fs\` skill.
198
+ Pages: only for polished artifacts shared with a wider audience or explicitly requested; see \`pages\` skill.
196
199
  A tool a person will use, with data and actions: build an app. See the \`apps\` skill.
197
- Share links come from env: \`APP_URL\` for pages, \`MCP_BASE_URL\` for the API, \`AGENT_FS_LIVE_URL\` for files. When a variable is missing, say so in your output.
200
+ Link env: \`APP_URL\` for pages, \`MCP_BASE_URL\` for the API, \`AGENT_FS_LIVE_URL\` for files. Report missing variables.
198
201
  `,
199
202
  variables: [],
200
203
  category: "system",
@@ -206,10 +209,11 @@ registerTemplate({
206
209
  defaultBody: `
207
210
  ## Outputs
208
211
 
209
- agent-fs is not configured here. A file a person will review goes to a page or a task attachment.
210
- A report or summary a person will read: publish a page with \`create_page\`. See the \`pages\` skill.
212
+ Short answers: inline in the task output.
213
+ Documents: Markdown task attachments; agent-fs is not configured here.
214
+ Pages: only for polished artifacts shared with a wider audience or explicitly requested; see \`pages\` skill.
211
215
  A tool a person will use, with data and actions: build an app. See the \`apps\` skill.
212
- Share links come from env: \`APP_URL\` for pages, \`MCP_BASE_URL\` for the API. When a variable is missing, say so in your output.
216
+ Link env: \`APP_URL\` for pages, \`MCP_BASE_URL\` for the API. Report missing variables.
213
217
  `,
214
218
  variables: [],
215
219
  category: "system",
@@ -8,9 +8,11 @@ import {
8
8
  type RequestPermissionResponse,
9
9
  type SessionConfigOption,
10
10
  type SessionNotification,
11
+ type Usage,
11
12
  } from "@agentclientprotocol/sdk";
12
13
  import pkg from "../../package.json";
13
14
  import type { AcpSessionConfigOption } from "../types";
15
+ import { mintAcpSessionToken, revokeAcpSessionToken } from "../utils/acp-session-token";
14
16
  import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
15
17
  import {
16
18
  detachedProcessGroup,
@@ -175,6 +177,10 @@ class ACPSession implements ProviderSession {
175
177
  private output = "";
176
178
  private completionPromise: Promise<ProviderResult>;
177
179
  private completionResolve!: (result: ProviderResult) => void;
180
+ /** ID of the ephemeral aseph_ token minted for this session, or null when
181
+ * the fallback operator key is in use (e.g. during tests or if the API is
182
+ * not yet upgraded). Revoked in finish(). */
183
+ private ephemeralTokenId: string | null = null;
178
184
 
179
185
  constructor(
180
186
  private readonly connection: ClientSideConnection,
@@ -182,8 +188,10 @@ class ACPSession implements ProviderSession {
182
188
  private readonly config: ProviderSessionConfig,
183
189
  sessionId: string,
184
190
  providerMeta?: Record<string, unknown>,
191
+ ephemeralTokenId?: string,
185
192
  ) {
186
193
  this.sessionId = sessionId;
194
+ this.ephemeralTokenId = ephemeralTokenId ?? null;
187
195
  this.completionPromise = new Promise((resolve) => {
188
196
  this.completionResolve = resolve;
189
197
  });
@@ -258,8 +266,21 @@ class ACPSession implements ProviderSession {
258
266
  sessionId: this.sessionId,
259
267
  prompt: [{ type: "text", text: this.config.prompt }],
260
268
  });
269
+ this.emit({
270
+ type: "raw_log",
271
+ content: serializeAcpLog({
272
+ type: "custom",
273
+ name: "acp_prompt_response",
274
+ data: {
275
+ sessionId: this.sessionId,
276
+ stopReason: response.stopReason,
277
+ usage: response.usage ?? null,
278
+ _meta: response._meta,
279
+ },
280
+ }),
281
+ });
261
282
  const isError = response.stopReason === "refusal" || response.stopReason === "cancelled";
262
- const cost = this.buildCostData(isError);
283
+ const cost = this.buildCostData(isError, response.usage);
263
284
  result = {
264
285
  exitCode: isError ? 1 : 0,
265
286
  sessionId: this.sessionId,
@@ -302,12 +323,16 @@ class ACPSession implements ProviderSession {
302
323
  }
303
324
  }
304
325
 
305
- private buildCostData(isError: boolean): CostData {
326
+ private buildCostData(isError: boolean, usage?: Usage | null): CostData {
306
327
  return {
307
328
  sessionId: this.sessionId,
308
329
  taskId: this.config.taskId,
309
330
  agentId: this.config.agentId,
310
331
  totalCostUsd: 0,
332
+ inputTokens: usage?.inputTokens,
333
+ outputTokens: usage?.outputTokens,
334
+ cacheReadTokens: usage?.cachedReadTokens ?? undefined,
335
+ cacheWriteTokens: usage?.cachedWriteTokens ?? undefined,
311
336
  durationMs: Date.now() - this.startedAt,
312
337
  numTurns: 1,
313
338
  model: this.config.model,
@@ -320,6 +345,11 @@ class ACPSession implements ProviderSession {
320
345
  if (this.completed) return;
321
346
  this.completed = true;
322
347
  this.completionResolve(result);
348
+ // Revoke the ephemeral token now that the session is done. Best-effort:
349
+ // the token expires on its own, so a failure here is not critical.
350
+ if (this.ephemeralTokenId) {
351
+ void revokeAcpSessionToken(this.config.apiUrl, this.config.apiKey, this.ephemeralTokenId);
352
+ }
323
353
  }
324
354
  }
325
355
 
@@ -355,12 +385,23 @@ export class ACPAdapter implements ProviderAdapter {
355
385
  const stream = ndJsonStream(fileSinkWritableStream(proc.stdin), proc.stdout);
356
386
  const connection = new ClientSideConnection(() => client, stream);
357
387
 
388
+ let ephemeralToken: { tokenId: string; plaintext: string } | null = null;
358
389
  try {
359
390
  await connection.initialize({
360
391
  protocolVersion: PROTOCOL_VERSION,
361
392
  clientInfo: { name: "agent-swarm", version: pkg.version },
362
393
  clientCapabilities: { session: { configOptions: { boolean: {} } } },
363
394
  });
395
+ // Mint a short-lived session-scoped bearer so the ACP target receives
396
+ // an aseph_ token rather than the full operator key. Throws on failure
397
+ // so we fail closed instead of falling back to the operator key.
398
+ ephemeralToken = await mintAcpSessionToken(
399
+ config.apiUrl,
400
+ config.apiKey,
401
+ config.agentId,
402
+ config.taskId,
403
+ );
404
+
364
405
  const installedServers = await fetchInstalledMcpServers(
365
406
  config.apiUrl,
366
407
  config.apiKey,
@@ -370,7 +411,7 @@ export class ACPAdapter implements ProviderAdapter {
370
411
  // The v2 draft (@agentclientprotocol/sdk/experimental/v2) adds a fourth
371
412
  // `{ type: "acp", name, serverId }` variant where the client hosts the MCP
372
413
  // server itself and the agent tunnels MCP over the existing ACP connection
373
- // (mcp/connect, mcp/message, mcp/disconnect) instead of a network hop — the
414
+ // (mcp/connect, mcp/message, mcp/disconnect) instead of a network hop -- the
374
415
  // shape for an ACP agent with no network route to the swarm API. Gated on
375
416
  // `mcpCapabilities.acp` and UNSTABLE; not adopted here.
376
417
  const newSession = await connection.newSession({
@@ -381,7 +422,7 @@ export class ACPAdapter implements ProviderAdapter {
381
422
  name: "swarm",
382
423
  url: `${config.apiUrl.replace(/\/+$/, "")}/mcp`,
383
424
  headers: [
384
- { name: "Authorization", value: `Bearer ${config.apiKey}` },
425
+ { name: "Authorization", value: `Bearer ${ephemeralToken.plaintext}` },
385
426
  { name: "X-Agent-ID", value: config.agentId },
386
427
  { name: "X-Source-Task-Id", value: config.taskId },
387
428
  ],
@@ -395,13 +436,26 @@ export class ACPAdapter implements ProviderAdapter {
395
436
  newSession.configOptions ?? [],
396
437
  target.configuredOptions(config),
397
438
  );
398
- session = new ACPSession(connection, proc, config, newSession.sessionId, {
399
- target: target.target,
400
- configOptions: sanitizeAcpConfigOptions(configOptions),
401
- });
439
+ session = new ACPSession(
440
+ connection,
441
+ proc,
442
+ config,
443
+ newSession.sessionId,
444
+ {
445
+ target: target.target,
446
+ configOptions: sanitizeAcpConfigOptions(configOptions),
447
+ },
448
+ ephemeralToken.tokenId,
449
+ );
402
450
  for (const event of preSessionEvents) session.emitFromAcp(event);
403
451
  return session;
404
452
  } catch (err) {
453
+ // Revoke the ephemeral token before re-throwing if ACPSession has not yet
454
+ // taken ownership of it (i.e. setup failed after mint but before the
455
+ // ACPSession constructor ran).
456
+ if (ephemeralToken && !session) {
457
+ void revokeAcpSessionToken(config.apiUrl, config.apiKey, ephemeralToken.tokenId);
458
+ }
405
459
  await terminateProcessGroup(proc.pid);
406
460
  throw new Error(`ACP target failed during startup: ${scrubSecrets(formatError(err))}`);
407
461
  }
@@ -191,19 +191,36 @@ export async function checkPiMonoCredentials(
191
191
  env: Record<string, string | undefined>,
192
192
  opts: CredCheckOptions = {},
193
193
  ): Promise<CredStatus> {
194
- // Determine Bedrock SDK mode:
195
- // - Explicit: BEDROCK_AUTH_MODE=sdk
194
+ // Determine the Bedrock mode:
195
+ // - Explicit: BEDROCK_AUTH_MODE=sdk — the AWS SDK default credential chain
196
+ // - Explicit: BEDROCK_AUTH_MODE=bearer — an explicit Bedrock API key in
197
+ // AWS_BEARER_TOKEN_BEDROCK, which the AWS SDK picks up as the
198
+ // bearer identity for the Bedrock clients
196
199
  // - Fallback: BEDROCK_AUTH_MODE absent AND MODEL_OVERRIDE starts with
197
200
  // "amazon-bedrock/" (preserves today's prefix-inference semantics)
198
- // BEDROCK_AUTH_MODE=bearer is declared/validated but the full bearer-token
199
- // path is not implemented yet — it falls through to the standard auth check.
201
+ // Both explicit modes share the region check and the enumeration probe; they
202
+ // differ in what has to be present up front and in how readiness is reported.
200
203
  const bedrockAuthMode = env.BEDROCK_AUTH_MODE?.toLowerCase();
204
+ const isBedrockBearer = bedrockAuthMode === "bearer";
201
205
  const isBedrockSdk =
202
206
  bedrockAuthMode === "sdk" ||
203
207
  (bedrockAuthMode === undefined &&
204
208
  env.MODEL_OVERRIDE?.toLowerCase().startsWith("amazon-bedrock/"));
205
209
 
206
- if (isBedrockSdk) {
210
+ if (isBedrockBearer && !env.AWS_BEARER_TOKEN_BEDROCK) {
211
+ // The token is the whole point of this mode; without it there is nothing
212
+ // the probe could authenticate with, and the standard API keys do not
213
+ // apply to Bedrock, so do not fall through to them.
214
+ return {
215
+ ready: false,
216
+ missing: ["AWS_BEARER_TOKEN_BEDROCK"],
217
+ hint: "BEDROCK_AUTH_MODE=bearer requires AWS_BEARER_TOKEN_BEDROCK (a Bedrock API key); set it, or use BEDROCK_AUTH_MODE=sdk to authenticate through the AWS credential chain.",
218
+ bedrockModels: [],
219
+ bedrockRegion: env.AWS_REGION ?? "",
220
+ };
221
+ }
222
+
223
+ if (isBedrockSdk || isBedrockBearer) {
207
224
  const region = env.AWS_REGION;
208
225
  if (!region) {
209
226
  // Do NOT fabricate a region. A guessed `us-east-1` can differ from where
@@ -231,7 +248,7 @@ export async function checkPiMonoCredentials(
231
248
  return {
232
249
  ready: true,
233
250
  missing: [],
234
- satisfiedBy: "sdk-delegated",
251
+ satisfiedBy: isBedrockBearer ? "env" : "sdk-delegated",
235
252
  hint: `Bedrock models invocable in ${region} enumerated (${bedrockModels.length} usable; ListFoundationModels + ListInferenceProfiles).`,
236
253
  bedrockModels,
237
254
  bedrockRegion: region,
@@ -139,6 +139,17 @@ and heartbeat-classified roots are omitted, and the metrics remain separate.
139
139
  (`webSearchRequests`, per-model `costUSD`) degrade per-field without
140
140
  invalidating the entry.
141
141
 
142
+ - **ACP usage:** optional `session/prompt` response usage supplies `inputTokens`,
143
+ `outputTokens`, `cachedReadTokens`, and `cachedWriteTokens`. The adapter maps
144
+ those counters directly; absent counters remain `undefined` only at the adapter
145
+ boundary. The session-cost API and DB coalesce missing input/output/cache
146
+ counters to zero, so persisted costs and UI displays lose unknown-vs-zero. The scrubbed
147
+ `acp_prompt_response` session log preserves the payload and optional `_meta`.
148
+ Context-window `usage_update` totals are not billing token estimates.
149
+ ACP reports no USD amount through `CostData`; unresolved `(acp, model)`
150
+ pricing identity stays `unpriced`. A target may fall back after rejecting
151
+ the requested model, so that requested ID alone cannot justify a rate alias.
152
+
142
153
  ## When a model is missing
143
154
 
144
155
  If `POST /api/session-costs` arrives with a `(provider, model)` pair that has
@@ -1,7 +1,7 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { unlinkSync } from "node:fs";
3
- import { closeDb, createScheduledTask, initDb } from "../be/db";
4
- import { calculateNextRun } from "./scheduler";
3
+ import { closeDb, createAgent, createScheduledTask, createTaskExtended, initDb } from "../be/db";
4
+ import { calculateNextRun, createStandaloneScheduleTask } from "./scheduler";
5
5
 
6
6
  const TEST_DB_PATH = "./test-scheduler.sqlite";
7
7
 
@@ -151,3 +151,50 @@ describe("calculateNextRun", () => {
151
151
  expect(nextRun).toBe("2026-01-19T09:00:00.000Z");
152
152
  });
153
153
  });
154
+
155
+ describe("createStandaloneScheduleTask", () => {
156
+ test("passes the schedule's parentTaskId through to the created task", async () => {
157
+ const agent = await createAgent({
158
+ name: "Deferred Wake-up Worker",
159
+ description: "Agent that owns the deferred task",
160
+ role: "worker",
161
+ isLead: false,
162
+ status: "idle",
163
+ maxTasks: 1,
164
+ capabilities: [],
165
+ });
166
+ const parent = await createTaskExtended("original work that needed time", {
167
+ agentId: agent.id,
168
+ source: "mcp",
169
+ });
170
+
171
+ const schedule = await createScheduledTask({
172
+ name: "test-deferred-parent",
173
+ taskTemplate: `Resume task ${parent.id}: the deploy should be green now`,
174
+ scheduleType: "one_time",
175
+ nextRunAt: new Date(Date.now() + 60_000).toISOString(),
176
+ targetAgentId: agent.id,
177
+ createdByAgentId: agent.id,
178
+ taskType: "deferred",
179
+ tags: ["deferred"],
180
+ parentTaskId: parent.id,
181
+ });
182
+
183
+ const task = await createStandaloneScheduleTask(schedule);
184
+ expect(task.parentTaskId).toBe(parent.id);
185
+ expect(task.taskType).toBe("deferred");
186
+ expect(task.tags).toContain("deferred");
187
+ });
188
+
189
+ test("a schedule without parentTaskId creates a parentless task", async () => {
190
+ const schedule = await createScheduledTask({
191
+ name: "test-no-parent",
192
+ taskTemplate: "Ordinary scheduled work",
193
+ cronExpression: "0 9 * * *",
194
+ timezone: "UTC",
195
+ });
196
+
197
+ const task = await createStandaloneScheduleTask(schedule);
198
+ expect(task.parentTaskId).toBeFalsy();
199
+ });
200
+ });
@@ -73,6 +73,10 @@ export async function createStandaloneScheduleTask(
73
73
  source: "schedule",
74
74
  requestedByUserId: schedule.createdBy,
75
75
  contextKey: scheduleContextKey({ scheduleId: schedule.id }),
76
+ // Set only by `defer-task`. An explicit parent wins over the sibling-awareness
77
+ // auto-wiring (see withSiblingAwareness in src/tasks/sibling-awareness.ts), so
78
+ // the wake-up run continues the deferred task rather than a random sibling.
79
+ parentTaskId: schedule.parentTaskId,
76
80
  });
77
81
  }
78
82
 
@@ -12,6 +12,7 @@ export const SDK_TOOL_NAME_MAP = {
12
12
  task_list: "get-tasks",
13
13
  task_get: "get-task-details",
14
14
  task_storeProgress: "store-progress",
15
+ task_defer: "defer-task",
15
16
  task_poll: "poll-task",
16
17
  task_send: "send-task",
17
18
  task_cancel: "cancel-task", // destructive
@@ -143,6 +143,14 @@ declare module "swarm-sdk" {
143
143
  task_list(args?: Record<string, unknown>): Promise<unknown>;
144
144
  task_get(args: { taskId: string }): Promise<unknown>;
145
145
  task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
146
+ task_defer(args: {
147
+ taskId: string;
148
+ delayMs?: number;
149
+ runAt?: string;
150
+ summary: string;
151
+ note: string;
152
+ checks?: string[];
153
+ }): Promise<unknown>;
146
154
  task_poll(args?: Record<string, unknown>): Promise<unknown>;
147
155
  // --- kv ---
148
156
  kv_get<T = unknown>(args: {
@@ -125,6 +125,14 @@ declare module "swarm-sdk" {
125
125
  task_list(args?: Record<string, unknown>): Promise<unknown>;
126
126
  task_get(args: { taskId: string }): Promise<unknown>;
127
127
  task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
128
+ task_defer(args: {
129
+ taskId: string;
130
+ delayMs?: number;
131
+ runAt?: string;
132
+ summary: string;
133
+ note: string;
134
+ checks?: string[];
135
+ }): Promise<unknown>;
128
136
  task_poll(args?: Record<string, unknown>): Promise<unknown>;
129
137
  // --- kv ---
130
138
  kv_get<T = unknown>(args: {
package/src/server.ts CHANGED
@@ -26,6 +26,7 @@ import { registerCreateMetricTool } from "./tools/create-metric";
26
26
  import { registerCreatePageTool } from "./tools/create-page";
27
27
  import { registerCredentialBindingsTool } from "./tools/credential-bindings";
28
28
  import { registerDbQueryTool } from "./tools/db-query";
29
+ import { registerDeferTaskTool } from "./tools/defer-task";
29
30
  import { registerDeleteChannelTool } from "./tools/delete-channel";
30
31
  import { registerDeletePageTool } from "./tools/delete-page";
31
32
  import { registerGetMetricsTool } from "./tools/get-metrics";
@@ -421,6 +422,7 @@ export async function createServer(opts: { scriptsOnly?: boolean; fullSurface?:
421
422
  if (hasCapability("scheduling")) {
422
423
  registerListSchedulesTool(server);
423
424
  registerCreateScheduleTool(server);
425
+ registerDeferTaskTool(server);
424
426
  registerUpdateScheduleTool(server);
425
427
  registerPatchScheduleTool(server);
426
428
  registerDeleteScheduleTool(server);