@desplega.ai/agent-swarm 1.129.0 → 1.131.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 (226) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-kfv2dawe.js → actions-w53mbns9.js} +11 -11
  3. package/dist/{anthropic-messages-3xgme8my.js → anthropic-messages-rjmd2e7n.js} +5 -5
  4. package/dist/{app-q8vjk6e8.js → app-m9gwqsqv.js} +8 -8
  5. package/dist/{artifact-hg4t1nx7.js → artifact-jysj3wrg.js} +2 -2
  6. package/dist/{assistant-g037r345.js → assistant-bp65m6f3.js} +13 -13
  7. package/dist/{azure-openai-responses-xa4sh9f6.js → azure-openai-responses-c0yjmbfd.js} +8 -5
  8. package/dist/{boot-reembed-18pzsrxa.js → boot-reembed-62p0at2k.js} +6 -6
  9. package/dist/{boot-reembed-wpks2cj8.js → boot-reembed-xmfp6wdh.js} +7 -7
  10. package/dist/{boot-scrub-logs-n7szd57r.js → boot-scrub-logs-rr8q2dms.js} +5 -5
  11. package/dist/{claude-adapter-qz4r0ajt.js → claude-adapter-2y47mrkd.js} +9 -9
  12. package/dist/{cli-mhjnbgb6.js → cli-0rcyq6d9.js} +450 -186
  13. package/dist/{cli-ttkmehy0.js → cli-2q72qz2s.js} +274 -25
  14. package/dist/{cli-gztgc3hz.js → cli-2wadx7p2.js} +2 -2
  15. package/dist/cli-3fj60etf.js +153 -0
  16. package/dist/{cli-7ecgm5v0.js → cli-3kf51j15.js} +1 -1
  17. package/dist/{cli-vgexpfsr.js → cli-57khth04.js} +6 -3
  18. package/dist/{cli-2mbe7vk0.js → cli-5dsbcjjw.js} +1 -1
  19. package/dist/{cli-etr62zy6.js → cli-5kx36evn.js} +2 -2
  20. package/dist/{cli-v2tr6amf.js → cli-5n6e5yfn.js} +30 -8
  21. package/dist/{cli-hx02s9zq.js → cli-5v4rw6yd.js} +2 -2
  22. package/dist/{cli-v37yr6mf.js → cli-5ze9rv97.js} +4 -4
  23. package/dist/{cli-ch7t5v7n.js → cli-6map995v.js} +9 -4
  24. package/dist/{cli-h0457a67.js → cli-70xqj0z3.js} +3 -3
  25. package/dist/{cli-kk4f9r17.js → cli-72q8m8gk.js} +8 -6
  26. package/dist/{cli-7padk69a.js → cli-78xwvkhj.js} +2 -2
  27. package/dist/{cli-dxtxacy7.js → cli-9dr5qzc0.js} +45 -8
  28. package/dist/{cli-17zxdgsq.js → cli-a40rnf71.js} +6 -6
  29. package/dist/{cli-t53vnnxz.js → cli-a4jzdpa3.js} +2514 -1055
  30. package/dist/{cli-w10fkrf1.js → cli-akdvwc9v.js} +354 -115
  31. package/dist/{cli-1qkk5er0.js → cli-bmze986d.js} +1 -1
  32. package/dist/{cli-ga12dc1d.js → cli-bqw0mt1n.js} +3 -3
  33. package/dist/{cli-0sf92efp.js → cli-cdeg08wx.js} +1 -1
  34. package/dist/{cli-a8ynhkyj.js → cli-cvgjwnh1.js} +17 -15
  35. package/dist/{cli-9fxg3v5m.js → cli-d2tfkcga.js} +4 -4
  36. package/dist/{cli-nhcd7x43.js → cli-d3x3x1c9.js} +2 -2
  37. package/dist/{cli-6nh2qxvh.js → cli-darcwax3.js} +1 -1
  38. package/dist/{cli-yjbgmysx.js → cli-e7ckcg4v.js} +12 -12
  39. package/dist/{cli-4n2gcjpd.js → cli-efxfnq6q.js} +4 -3
  40. package/dist/{cli-z9xqqhfk.js → cli-er3a4bb0.js} +1 -1
  41. package/dist/{cli-xg2eezyv.js → cli-f1am9szv.js} +1 -1
  42. package/dist/{cli-xy4cwsm9.js → cli-fywms69x.js} +1 -1
  43. package/dist/{cli-33jj76y8.js → cli-g6b8x1h8.js} +1 -1
  44. package/dist/{cli-epsvdckx.js → cli-gm9cbhbq.js} +20 -10
  45. package/dist/{cli-rkndnn90.js → cli-k9qkf1hw.js} +6 -4
  46. package/dist/{cli-zejvt395.js → cli-kh3ba2zf.js} +1 -1
  47. package/dist/{cli-gb25p1h2.js → cli-nm5gtp1x.js} +2 -2
  48. package/dist/{cli-akxnpx17.js → cli-pm6xbh84.js} +1829 -1629
  49. package/dist/{cli-2k0ztbfp.js → cli-psh3c4va.js} +1 -1
  50. package/dist/{cli-39vgrsep.js → cli-qv61hp37.js} +2 -2
  51. package/dist/{cli-hv2fndhm.js → cli-r6kqqbj0.js} +6 -4
  52. package/dist/{cli-1y1cm570.js → cli-rzbpmxdr.js} +1 -1
  53. package/dist/{cli-mmrk00vp.js → cli-t1xze5qh.js} +1 -1
  54. package/dist/{cli-g252a9ky.js → cli-t55yw3a1.js} +2 -2
  55. package/dist/{cli-7bz8dzqa.js → cli-t76vw5db.js} +1184 -1522
  56. package/dist/{cli-4n4mdv21.js → cli-xpxde7rm.js} +7 -3
  57. package/dist/{cli-fpaa5k2q.js → cli-yw8vghx2.js} +54 -18
  58. package/dist/cli-zsspvvm2.js +490 -0
  59. package/dist/cli.js +27 -25
  60. package/dist/{codex-adapter-7v6t7ff6.js → codex-adapter-d24q75wk.js} +7 -7
  61. package/dist/{codex-hook-amndawyb.js → codex-hook-hq54t9ft.js} +2 -2
  62. package/dist/{codex-session-runner-e7j4khe2.js → codex-session-runner-g50vcsex.js} +7 -7
  63. package/dist/{commands-ex0k5tbf.js → commands-c3nrc5hg.js} +5 -5
  64. package/dist/{db-df7jatfc.js → db-nt7fg3tw.js} +5 -5
  65. package/dist/{google-generative-ai-y42hnw9p.js → google-generative-ai-hysrd8qx.js} +7 -5
  66. package/dist/{google-vertex-4sg66gb6.js → google-vertex-p5arrga0.js} +7 -5
  67. package/dist/{handlers-1j2cfqt2.js → handlers-fkr6swat.js} +13 -13
  68. package/dist/{hook-nx9a0mj3.js → hook-77wpvnjc.js} +7 -7
  69. package/dist/{hook-w6yaf6sy.js → hook-g019yehy.js} +8 -8
  70. package/dist/{http-4tz5h8wg.js → http-jewj0syk.js} +2993 -716
  71. package/dist/{index-0bpkzb09.js → index-070v4eyb.js} +3 -3
  72. package/dist/{index-0s67qv7h.js → index-6gm5he8h.js} +11 -11
  73. package/dist/{index-35gtv6gj.js → index-7n2xpbd4.js} +310 -40
  74. package/dist/{index-ker3gccb.js → index-9znhar3x.js} +4 -4
  75. package/dist/{index-5n2c8yay.js → index-hase6zbe.js} +2 -2
  76. package/dist/{index-kcf3tm9h.js → index-hfvggd0z.js} +12 -12
  77. package/dist/{index-7c0jqna0.js → index-hp1kdbjj.js} +13 -13
  78. package/dist/{keepalive-3j4a9168.js → keepalive-j0fjvqfd.js} +7 -7
  79. package/dist/{lead-6kpevymb.js → lead-1n8x30xe.js} +33 -33
  80. package/dist/{maintenance-m1k82ean.js → maintenance-gvev11sa.js} +7 -7
  81. package/dist/{mistral-conversations-bjkdfb6z.js → mistral-conversations-81fwav0j.js} +6 -6
  82. package/dist/{oauth-refresh-sweep-881k8e4q.js → oauth-refresh-sweep-s9ecsvh0.js} +6 -6
  83. package/dist/{onboard-c0ykffjn.js → onboard-pvm5b38w.js} +2 -2
  84. package/dist/{openai-codex-responses-xetseqvt.js → openai-codex-responses-5g27pd6f.js} +43 -23
  85. package/dist/{openai-completions-5r2g5axa.js → openai-completions-3vkmfz2a.js} +55 -11
  86. package/dist/{openai-responses-ce67hd23.js → openai-responses-dg4t7j7a.js} +8 -5
  87. package/dist/{openrouter-images-pzfp22jb.js → openrouter-images-g3gm8931.js} +1 -1
  88. package/dist/{otel-impl-sqagjerv.js → otel-impl-p6jvgd9c.js} +6 -4
  89. package/dist/{otel-3mp81ppd.js → otel-nqa12wf3.js} +2 -2
  90. package/dist/{pi-mono-adapter-k584k16g.js → pi-mono-adapter-t091s27g.js} +17117 -7517
  91. package/dist/{pricing-refresh-y82whbf9.js → pricing-refresh-k49rcyed.js} +7 -7
  92. package/dist/{rbac-roles-nvkqmzyd.js → rbac-roles-30pspvy7.js} +7 -7
  93. package/dist/{rbac-roles-4ht8gegc.js → rbac-roles-cr80g96t.js} +6 -6
  94. package/dist/{render-v2-wvqdgvvp.js → render-v2-xm457ng1.js} +8 -8
  95. package/dist/{seed-pricing-fy3rxhde.js → seed-pricing-yhpv35fr.js} +6 -6
  96. package/dist/{setup-ak5cnc6f.js → setup-nytnmwwr.js} +2 -2
  97. package/dist/{worker-d8769q8a.js → worker-msytr2cw.js} +33 -33
  98. package/openapi.json +34039 -11036
  99. package/package.json +8 -6
  100. package/src/apps/definition.ts +368 -26
  101. package/src/apps/row-store.ts +66 -10
  102. package/src/apps/schema-migrate.ts +95 -1
  103. package/src/apps/store.ts +13 -4
  104. package/src/apps/sync-run-as.ts +18 -0
  105. package/src/apps/sync.ts +934 -0
  106. package/src/apps/version.ts +16 -2
  107. package/src/assets/key.ts +8 -1
  108. package/src/be/asset-key-audit.ts +3 -1
  109. package/src/be/db-queries/mcp-oauth.ts +297 -24
  110. package/src/be/db.ts +42 -1
  111. package/src/be/migrations/129_asset_keys_apps_scripts.sql +161 -0
  112. package/src/be/scripts/db.ts +5 -2
  113. package/src/be/scripts/typecheck.ts +1 -0
  114. package/src/be/seed-scripts/catalog/app-sync-run.ts +32 -0
  115. package/src/be/seed-scripts/catalog/github-issues-pull.ts +102 -0
  116. package/src/be/seed-scripts/index.ts +18 -0
  117. package/src/be/seed-skills/index.ts +7 -0
  118. package/src/http/active-sessions.ts +26 -18
  119. package/src/http/agents.ts +94 -27
  120. package/src/http/api-keys.ts +91 -17
  121. package/src/http/approval-requests.ts +87 -10
  122. package/src/http/apps.ts +361 -53
  123. package/src/http/assets.ts +165 -31
  124. package/src/http/budgets.ts +7 -5
  125. package/src/http/codex-oauth-keep-warm.ts +21 -3
  126. package/src/http/config.ts +46 -16
  127. package/src/http/context.ts +33 -6
  128. package/src/http/ecosystem.ts +20 -6
  129. package/src/http/events.ts +24 -9
  130. package/src/http/favorites.ts +20 -6
  131. package/src/http/fs.ts +60 -17
  132. package/src/http/heartbeat.ts +18 -4
  133. package/src/http/inbox-state.ts +12 -6
  134. package/src/http/integrations.ts +25 -5
  135. package/src/http/kv.ts +36 -15
  136. package/src/http/mcp-bridge.ts +5 -1
  137. package/src/http/mcp-oauth.ts +332 -17
  138. package/src/http/mcp-servers.ts +56 -18
  139. package/src/http/memory.ts +282 -29
  140. package/src/http/metrics.ts +80 -17
  141. package/src/http/oauth-callback.ts +10 -4
  142. package/src/http/oauth-generic.ts +5 -1
  143. package/src/http/oauth-locks.ts +6 -3
  144. package/src/http/openapi.ts +57 -9
  145. package/src/http/page-proxy.ts +25 -5
  146. package/src/http/pages-public.ts +37 -20
  147. package/src/http/pages.ts +93 -28
  148. package/src/http/poll.ts +89 -5
  149. package/src/http/pricing.ts +9 -6
  150. package/src/http/prompt-templates.ts +85 -21
  151. package/src/http/route-def.ts +67 -4
  152. package/src/http/schedules.ts +63 -14
  153. package/src/http/script-connection-proxy.ts +30 -4
  154. package/src/http/script-connections.ts +393 -43
  155. package/src/http/script-runs.ts +73 -19
  156. package/src/http/scripts.ts +257 -39
  157. package/src/http/session-data.ts +87 -14
  158. package/src/http/sessions.ts +80 -5
  159. package/src/http/skills.ts +115 -35
  160. package/src/http/stats.ts +94 -13
  161. package/src/http/task-templates.ts +6 -4
  162. package/src/http/tasks.ts +230 -61
  163. package/src/http/trackers/jira.ts +90 -30
  164. package/src/http/trackers/linear.ts +46 -12
  165. package/src/http/users.ts +162 -25
  166. package/src/http/utils.ts +12 -2
  167. package/src/http/webhooks.ts +30 -16
  168. package/src/http/workflow-events.ts +18 -5
  169. package/src/http/workflows.ts +128 -39
  170. package/src/http/x.ts +37 -6
  171. package/src/oauth/ensure-mcp-token.ts +16 -1
  172. package/src/oauth/mcp-wrapper.ts +281 -26
  173. package/src/providers/pi-mono-adapter.ts +1 -1
  174. package/src/scripts-runtime/credential-broker/README.md +1 -1
  175. package/src/scripts-runtime/credential-broker/fetch-patch.ts +12 -2
  176. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  177. package/src/scripts-runtime/stdlib/fetch.ts +6 -0
  178. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  179. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  180. package/src/server.ts +2 -0
  181. package/src/tests/apps-rbac.test.ts +110 -0
  182. package/src/tests/apps-spike2.test.ts +4 -1
  183. package/src/tests/apps-sync-engine.test.ts +2540 -0
  184. package/src/tests/apps-sync.test.ts +1627 -0
  185. package/src/tests/asset-key-api.test.ts +48 -2
  186. package/src/tests/asset-key-invariants.test.ts +92 -0
  187. package/src/tests/asset-key-migration.test.ts +174 -8
  188. package/src/tests/asset-key-normalization.test.ts +2 -0
  189. package/src/tests/credential-broker.test.ts +27 -2
  190. package/src/tests/mcp-oauth-dcr-reuse.test.ts +743 -0
  191. package/src/tests/mcp-oauth-ensure-token.test.ts +45 -0
  192. package/src/tests/mcp-oauth-manual-client.test.ts +135 -2
  193. package/src/tests/mcp-oauth-queries.test.ts +153 -0
  194. package/src/tests/mcp-oauth-wrapper.test.ts +432 -0
  195. package/src/tests/memory-http-recall-gating.test.ts +2 -0
  196. package/src/tests/openapi-response-contract.test.ts +138 -0
  197. package/src/tests/rbac-admission-e2e.test.ts +68 -0
  198. package/src/tests/script-connections.test.ts +3 -0
  199. package/src/tests/scripts-runtime-fetch.test.ts +121 -0
  200. package/src/tests/scripts-runtime-secret-egress.test.ts +4 -2
  201. package/src/tests/seed-scripts.test.ts +2 -2
  202. package/src/tests/swarm-tool-result-gate.test.ts +32 -0
  203. package/src/tests/task-working-dir.test.ts +8 -4
  204. package/src/tests/whoami-route.test.ts +127 -0
  205. package/src/tests/workflow-engine-v2.test.ts +80 -7
  206. package/src/tests/workflow-swarm-script.test.ts +104 -0
  207. package/src/tools/app-get.ts +10 -2
  208. package/src/tools/app-rollback.ts +1 -0
  209. package/src/tools/app-sync.ts +112 -0
  210. package/src/tools/script-common.ts +20 -0
  211. package/src/tools/tool-config.ts +2 -1
  212. package/src/tools/utils.ts +11 -0
  213. package/src/tools/workflows/create-workflow.ts +4 -1
  214. package/src/types.ts +1394 -1206
  215. package/src/utils/template.ts +8 -1
  216. package/src/utils/zod-openapi.ts +11 -0
  217. package/src/workflows/engine.ts +27 -4
  218. package/src/workflows/executors/swarm-script.ts +28 -0
  219. package/src/workflows/retry-poller.ts +19 -2
  220. package/templates/skills/apps/content.md +94 -5
  221. package/templates/skills/asset-namespaces/content.md +2 -2
  222. package/templates/skills/scheduled-task-resilience/SKILL.md +102 -0
  223. package/templates/skills/scheduled-task-resilience/config.json +14 -0
  224. package/templates/skills/scheduled-task-resilience/content.md +97 -0
  225. package/dist/cli-7wmybrt5.js +0 -115
  226. package/dist/cli-d91pvmq1.js +0 -14
@@ -1,5 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { z } from "zod";
3
+ import { getApp } from "../apps/store";
3
4
  import { auditAssetKeys } from "../be/asset-key-audit";
4
5
  import { AssetKeyAuthorizationError, authorizeAssetKeyWrite } from "../be/asset-key-auth";
5
6
  import { resolveHttpAuditUserId } from "../be/audit-user";
@@ -11,11 +12,41 @@ import {
11
12
  moveAssetKey,
12
13
  upsertAssetKeyMapping,
13
14
  } from "../be/db";
15
+ import { getScriptById } from "../be/scripts/db";
14
16
  import { can, type RbacPrincipal, type RbacResource } from "../rbac";
15
- import { type AssetEntityType, AssetEntityTypeSchema, AssetKeySchema } from "../types";
17
+ import {
18
+ type AssetEntityType,
19
+ AssetEntityTypeSchema,
20
+ AssetKeyMappingSchema,
21
+ AssetKeySchema,
22
+ AssetSummarySchema,
23
+ } from "../types";
16
24
  import { getRequestAuth } from "../utils/request-auth-context";
17
25
  import { route } from "./route-def";
18
- import { json, jsonError } from "./utils";
26
+ import { jsonError } from "./utils";
27
+
28
+ const AssetKeyAuditIssueSchema = z.object({
29
+ severity: z.enum(["fatal", "warning"]),
30
+ code: z.enum([
31
+ "missing-key",
32
+ "noncanonical-key",
33
+ "unknown-personal-user",
34
+ "missing-provider-mapping",
35
+ "provider-mapping-drift",
36
+ ]),
37
+ entityType: AssetEntityTypeSchema,
38
+ entityId: z.string(),
39
+ message: z.string(),
40
+ });
41
+
42
+ const AssetKeyAuditResultSchema = z.object({
43
+ ok: z.boolean(),
44
+ structuralValid: z.boolean(),
45
+ checked: z.number().int().min(0),
46
+ fatalCount: z.number().int().min(0),
47
+ warningCount: z.number().int().min(0),
48
+ issues: z.array(AssetKeyAuditIssueSchema),
49
+ });
19
50
 
20
51
  const keyAuditRoute = route({
21
52
  method: "get",
@@ -26,7 +57,7 @@ const keyAuditRoute = route({
26
57
  "Operator-only check for structural key validity, personal-user references, and logical provider mapping drift. Repeated logical keys are valid and are never reported as conflicts.",
27
58
  tags: ["Assets"],
28
59
  responses: {
29
- 200: { description: "Asset namespace audit result" },
60
+ 200: { description: "Asset namespace audit result", schema: AssetKeyAuditResultSchema },
30
61
  403: { description: "Operator access required" },
31
62
  },
32
63
  });
@@ -41,11 +72,17 @@ const listAssetsRoute = route({
41
72
  tags: ["Assets"],
42
73
  query: z.object({
43
74
  keyPrefix: AssetKeySchema.optional(),
44
- types: z.string().optional().describe("Comma-separated task,workflow,schedule,page,file list"),
75
+ types: z
76
+ .string()
77
+ .optional()
78
+ .describe("Comma-separated task,workflow,schedule,page,app,script,file list"),
45
79
  limit: z.coerce.number().int().min(1).max(1000).optional(),
46
80
  }),
47
81
  responses: {
48
- 200: { description: "Lightweight asset summary list" },
82
+ 200: {
83
+ description: "Lightweight asset summary list",
84
+ schema: z.object({ assets: z.array(AssetSummarySchema), count: z.number().int() }),
85
+ },
49
86
  400: { description: "Invalid entity type" },
50
87
  },
51
88
  });
@@ -66,7 +103,7 @@ const registerMappingRoute = route({
66
103
  key: AssetKeySchema.optional(),
67
104
  }),
68
105
  responses: {
69
- 200: { description: "Mapping registered" },
106
+ 200: { description: "Mapping registered", schema: AssetKeyMappingSchema },
70
107
  400: { description: "Invalid provider tuple or namespace" },
71
108
  403: { description: "Operator access required or personal namespace not authorized" },
72
109
  },
@@ -81,6 +118,46 @@ function ensureOperator(req: IncomingMessage, res: ServerResponse): boolean {
81
118
  return false;
82
119
  }
83
120
 
121
+ const moveAssetBodySchema = z.object({ key: AssetKeySchema });
122
+ const moveAssetSuccessSchema = z.object({
123
+ entityType: AssetEntityTypeSchema,
124
+ id: z.string(),
125
+ key: AssetKeySchema,
126
+ });
127
+ const moveAssetResponses = {
128
+ 200: { description: "Asset namespace updated", schema: moveAssetSuccessSchema },
129
+ 400: { description: "Invalid namespace" },
130
+ 403: { description: "Move not authorized" },
131
+ 404: { description: "Asset not found" },
132
+ 409: { description: "Moves blocked until audit warnings are repaired" },
133
+ } as const;
134
+
135
+ // Register permission-specific paths before the polymorphic fallback so RBAC
136
+ // admission can preserve each resource's mutation posture for scoped roles.
137
+ const moveAppAssetRoute = route({
138
+ method: "patch",
139
+ path: "/api/assets/app/{id}/key",
140
+ pattern: ["api", "assets", "app", null, "key"],
141
+ summary: "Move an app to another logical namespace",
142
+ tags: ["Assets"],
143
+ params: z.object({ id: z.string().min(1) }),
144
+ body: moveAssetBodySchema,
145
+ responses: moveAssetResponses,
146
+ rbac: { permission: "app.manage" },
147
+ });
148
+
149
+ const moveScriptAssetRoute = route({
150
+ method: "patch",
151
+ path: "/api/assets/script/{id}/key",
152
+ pattern: ["api", "assets", "script", null, "key"],
153
+ summary: "Move a script to another logical namespace",
154
+ tags: ["Assets"],
155
+ params: z.object({ id: z.string().min(1) }),
156
+ body: moveAssetBodySchema,
157
+ responses: moveAssetResponses,
158
+ rbac: { permission: "script.global.write" },
159
+ });
160
+
84
161
  const moveAssetRoute = route({
85
162
  method: "patch",
86
163
  path: "/api/assets/{entityType}/{id}/key",
@@ -90,20 +167,26 @@ const moveAssetRoute = route({
90
167
  "Updates namespace metadata only. Provider-backed files keep the same provider key, org, and drive; no remote move occurs. Personal keys are labels, not a privacy guarantee.",
91
168
  tags: ["Assets"],
92
169
  params: z.object({ entityType: AssetEntityTypeSchema, id: z.string().min(1) }),
93
- body: z.object({ key: AssetKeySchema }),
94
- responses: {
95
- 200: { description: "Asset namespace updated" },
96
- 400: { description: "Invalid namespace" },
97
- 403: { description: "Move not authorized" },
98
- 404: { description: "Asset not found" },
99
- 409: { description: "Moves blocked until audit warnings are repaired" },
100
- },
170
+ body: moveAssetBodySchema,
171
+ responses: moveAssetResponses,
101
172
  rbac: {
102
173
  ungated:
103
- "preserves each entity's current mutation posture; task moves additionally use task.fs.mutate, file moves require operator authentication, and personal destinations require a matching trusted user",
174
+ "preserves each entity's current mutation posture; task moves use task.fs.mutate, app moves use app.manage, scripts preserve agent ownership/global-lead rules, file moves require operator authentication, and personal destinations require a matching trusted user",
104
175
  },
105
176
  });
106
177
 
178
+ function assetMovePrincipal(
179
+ req: IncomingMessage,
180
+ myAgentId: string | undefined,
181
+ ): RbacPrincipal | null {
182
+ const auth = getRequestAuth(req);
183
+ if (auth?.kind === "operator") return { kind: "operator" };
184
+ if (auth?.kind === "user") return { kind: "user", userId: auth.userId };
185
+ if (!myAgentId) return null;
186
+ const agent = getAgentById(myAgentId);
187
+ return { kind: "agent", agentId: myAgentId, isLead: agent?.isLead ?? false };
188
+ }
189
+
107
190
  function canMutateTaskNamespace(
108
191
  task: { id: string; agentId: string | null; creatorAgentId?: string },
109
192
  myAgentId: string | undefined,
@@ -115,20 +198,42 @@ function canMutateTaskNamespace(
115
198
  agentId: task.agentId,
116
199
  creatorAgentId: task.creatorAgentId,
117
200
  };
118
- const auth = getRequestAuth(req);
119
- let principal: RbacPrincipal;
120
- if (auth?.kind === "operator") {
121
- principal = { kind: "operator" };
122
- } else if (auth?.kind === "user") {
123
- principal = { kind: "user", userId: auth.userId };
124
- } else {
125
- if (!myAgentId) return false;
126
- const agent = getAgentById(myAgentId);
127
- principal = { kind: "agent", agentId: myAgentId, isLead: agent?.isLead ?? false };
128
- }
201
+ const principal = assetMovePrincipal(req, myAgentId);
202
+ if (!principal) return false;
129
203
  return can({ principal, verb: "task.fs.mutate", resource, source: "http" }).allow;
130
204
  }
131
205
 
206
+ function canManageAppNamespace(
207
+ id: string,
208
+ req: IncomingMessage,
209
+ myAgentId: string | undefined,
210
+ ): boolean {
211
+ const principal = assetMovePrincipal(req, myAgentId);
212
+ return (
213
+ !!principal &&
214
+ can({ principal, verb: "app.manage", resource: { kind: "app", appId: id }, source: "http" })
215
+ .allow
216
+ );
217
+ }
218
+
219
+ function canManageScriptNamespace(
220
+ script: NonNullable<ReturnType<typeof getScriptById>>,
221
+ req: IncomingMessage,
222
+ myAgentId: string | undefined,
223
+ ): boolean {
224
+ const principal = assetMovePrincipal(req, myAgentId);
225
+ if (!principal) return false;
226
+ if (principal.kind === "operator") return true;
227
+ if (principal.kind !== "agent") return false;
228
+ if (script.scope === "agent") return script.scopeId === principal.agentId;
229
+ return can({
230
+ principal,
231
+ verb: "script.global.write",
232
+ resource: { kind: "owned", scope: "global" },
233
+ source: "http",
234
+ }).allow;
235
+ }
236
+
132
237
  export async function handleAssets(
133
238
  req: IncomingMessage,
134
239
  res: ServerResponse,
@@ -140,7 +245,7 @@ export async function handleAssets(
140
245
  const parsed = await keyAuditRoute.parse(req, res, pathSegments, queryParams);
141
246
  if (!parsed) return true;
142
247
  if (!ensureOperator(req, res)) return true;
143
- json(res, auditAssetKeys(getDb()));
248
+ keyAuditRoute.respond(res, 200, auditAssetKeys(getDb()));
144
249
  return true;
145
250
  }
146
251
 
@@ -162,7 +267,7 @@ export async function handleAssets(
162
267
  types: types.length > 0 ? types : undefined,
163
268
  limit: parsed.query.limit,
164
269
  });
165
- json(res, { assets, count: assets.length });
270
+ listAssetsRoute.respond(res, 200, { assets, count: assets.length });
166
271
  return true;
167
272
  }
168
273
 
@@ -182,7 +287,7 @@ export async function handleAssets(
182
287
  createdBy: actor ?? undefined,
183
288
  updatedBy: actor ?? undefined,
184
289
  });
185
- json(res, mapping);
290
+ registerMappingRoute.respond(res, 200, mapping);
186
291
  } catch (error) {
187
292
  if (error instanceof AssetKeyAuthorizationError) {
188
293
  jsonError(res, error.message, error.statusCode);
@@ -193,7 +298,11 @@ export async function handleAssets(
193
298
  return true;
194
299
  }
195
300
 
196
- if (moveAssetRoute.match(req.method, pathSegments)) {
301
+ if (
302
+ moveAppAssetRoute.match(req.method, pathSegments) ||
303
+ moveScriptAssetRoute.match(req.method, pathSegments) ||
304
+ moveAssetRoute.match(req.method, pathSegments)
305
+ ) {
197
306
  const parsed = await moveAssetRoute.parse(req, res, pathSegments, queryParams);
198
307
  if (!parsed) return true;
199
308
  if (parsed.params.entityType === "file" && !ensureOperator(req, res)) return true;
@@ -208,6 +317,27 @@ export async function handleAssets(
208
317
  return true;
209
318
  }
210
319
  }
320
+ if (parsed.params.entityType === "app") {
321
+ if (!getApp(parsed.params.id)) {
322
+ jsonError(res, "Asset not found", 404);
323
+ return true;
324
+ }
325
+ if (!canManageAppNamespace(parsed.params.id, req, myAgentId)) {
326
+ jsonError(res, "Not authorized to move this app namespace", 403);
327
+ return true;
328
+ }
329
+ }
330
+ if (parsed.params.entityType === "script") {
331
+ const script = getScriptById(parsed.params.id);
332
+ if (!script) {
333
+ jsonError(res, "Asset not found", 404);
334
+ return true;
335
+ }
336
+ if (!canManageScriptNamespace(script, req, myAgentId)) {
337
+ jsonError(res, "Not authorized to move this script namespace", 403);
338
+ return true;
339
+ }
340
+ }
211
341
 
212
342
  try {
213
343
  const actor = resolveHttpAuditUserId(req, myAgentId);
@@ -222,7 +352,11 @@ export async function handleAssets(
222
352
  jsonError(res, "Asset not found", 404);
223
353
  return true;
224
354
  }
225
- json(res, { entityType: parsed.params.entityType, id: parsed.params.id, key });
355
+ moveAssetRoute.respond(res, 200, {
356
+ entityType: parsed.params.entityType,
357
+ id: parsed.params.id,
358
+ key,
359
+ });
226
360
  } catch (error) {
227
361
  if (error instanceof AssetKeyAuthorizationError) {
228
362
  jsonError(res, error.message, error.statusCode);
@@ -20,7 +20,7 @@ import {
20
20
  import { BudgetRefusalNotificationSchema, BudgetSchema, BudgetScopeSchema } from "../types";
21
21
  import { scrubSecrets } from "../utils/secret-scrubber";
22
22
  import { route } from "./route-def";
23
- import { json, jsonError } from "./utils";
23
+ import { jsonError } from "./utils";
24
24
 
25
25
  // ─── Helpers ─────────────────────────────────────────────────────────────────
26
26
 
@@ -128,7 +128,7 @@ export async function handleBudgets(
128
128
  if (listBudgets.match(req.method, pathSegments)) {
129
129
  const parsed = await listBudgets.parse(req, res, pathSegments, queryParams);
130
130
  if (!parsed) return true;
131
- json(res, { budgets: getBudgets() });
131
+ listBudgets.respond(res, 200, { budgets: getBudgets() });
132
132
  return true;
133
133
  }
134
134
 
@@ -139,7 +139,9 @@ export async function handleBudgets(
139
139
  const parsed = await listBudgetRefusals.parse(req, res, pathSegments, queryParams);
140
140
  if (!parsed) return true;
141
141
  const limit = parsed.query.limit ?? 50;
142
- json(res, { refusals: getRecentBudgetRefusalNotifications(limit) });
142
+ listBudgetRefusals.respond(res, 200, {
143
+ refusals: getRecentBudgetRefusalNotifications(limit),
144
+ });
143
145
  return true;
144
146
  }
145
147
 
@@ -161,7 +163,7 @@ export async function handleBudgets(
161
163
  jsonError(res, "Budget not configured", 404);
162
164
  return true;
163
165
  }
164
- json(res, row);
166
+ getBudgetByScope.respond(res, 200, row);
165
167
  return true;
166
168
  }
167
169
 
@@ -184,7 +186,7 @@ export async function handleBudgets(
184
186
  },
185
187
  });
186
188
 
187
- json(res, updated);
189
+ upsertBudgetRoute.respond(res, 200, updated);
188
190
  return true;
189
191
  }
190
192
 
@@ -23,12 +23,13 @@
23
23
  */
24
24
 
25
25
  import type { IncomingMessage, ServerResponse } from "node:http";
26
+ import { z } from "zod";
26
27
  import { getKv } from "../be/db";
27
28
  import { deriveCodexKeySuffix } from "../providers/codex-oauth/auth-json.js";
28
29
  import { getValidCodexOAuth, loadAllCodexOAuthSlots } from "../providers/codex-oauth/storage.js";
29
30
  import { getApiKey } from "../utils/api-key";
30
31
  import { route } from "./route-def";
31
- import { deriveApiBaseUrl, json } from "./utils";
32
+ import { deriveApiBaseUrl } from "./utils";
32
33
 
33
34
  /** ~weekly refresh cadence, comfortably inside OpenAI's ~8-day staleness window given the 10-day TTL. */
34
35
  const KEEP_WARM_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
@@ -42,6 +43,20 @@ type SlotOutcome =
42
43
  | { slot: number; outcome: "no-credentials" }
43
44
  | { slot: number; keySuffix: string; outcome: "failed"; reason: string };
44
45
 
46
+ /** Mirrors the `SlotOutcome` union above — split so each branch has a single discriminant literal. */
47
+ const slotOutcomeSchema = z.discriminatedUnion("outcome", [
48
+ z.object({ slot: z.number(), keySuffix: z.string(), outcome: z.literal("warm") }),
49
+ z.object({ slot: z.number(), keySuffix: z.string(), outcome: z.literal("refreshed") }),
50
+ z.object({ slot: z.number(), keySuffix: z.string(), outcome: z.literal("skipped-benched") }),
51
+ z.object({ slot: z.number(), outcome: z.literal("no-credentials") }),
52
+ z.object({
53
+ slot: z.number(),
54
+ keySuffix: z.string(),
55
+ outcome: z.literal("failed"),
56
+ reason: z.string(),
57
+ }),
58
+ ]);
59
+
45
60
  const keepWarmRoute = route({
46
61
  method: "post",
47
62
  path: "/api/oauth/keep-warm/codex",
@@ -51,7 +66,10 @@ const keepWarmRoute = route({
51
66
  "Enumerates codex_oauth_* slots and refreshes any older than ~7 days through the same locked getValidCodexOAuth path used at task time. Skips slots already benched by codex-auth-expiry-watch.",
52
67
  tags: ["OAuth"],
53
68
  responses: {
54
- 200: { description: "Per-slot keep-warm outcomes" },
69
+ 200: {
70
+ description: "Per-slot keep-warm outcomes",
71
+ schema: z.object({ results: z.array(slotOutcomeSchema) }),
72
+ },
55
73
  },
56
74
  });
57
75
 
@@ -102,6 +120,6 @@ export async function handleCodexOAuthKeepWarm(
102
120
  await new Promise((resolve) => setTimeout(resolve, 100 + Math.random() * 200));
103
121
  }
104
122
 
105
- json(res, { results });
123
+ keepWarmRoute.respond(res, 200, { results });
106
124
  return true;
107
125
  }
@@ -17,11 +17,12 @@ import {
17
17
  validateConfigValue,
18
18
  } from "../be/swarm-config-guard";
19
19
  import { can, isRbacEnabled, type PermissionVerb } from "../rbac";
20
+ import { SwarmConfigSchema } from "../types";
20
21
  import { getRequestAuth } from "../utils/request-auth-context";
21
22
  import { registerVolatileSecret } from "../utils/secret-scrubber";
22
23
  import { reloadGlobalConfigsAndIntegrations, scheduleIntegrationsReload } from "./core";
23
24
  import { route } from "./route-def";
24
- import { json, jsonError } from "./utils";
25
+ import { jsonError } from "./utils";
25
26
 
26
27
  const MAX_ENV_PRESENCE_KEYS = 200;
27
28
  const SECRETS_FORCE_MASK_NOTE =
@@ -119,7 +120,13 @@ const getResolvedConfigRoute = route({
119
120
  includeSecrets: z.enum(["true", "false"]).optional(),
120
121
  }),
121
122
  responses: {
122
- 200: { description: "Resolved config entries" },
123
+ 200: {
124
+ description: "Resolved config entries",
125
+ schema: z.object({
126
+ configs: z.array(SwarmConfigSchema),
127
+ message: z.string().optional(),
128
+ }),
129
+ },
123
130
  },
124
131
  });
125
132
 
@@ -134,7 +141,10 @@ const envPresence = route({
134
141
  keys: z.string().min(1),
135
142
  }),
136
143
  responses: {
137
- 200: { description: "Map of key -> boolean (true iff set in process.env)" },
144
+ 200: {
145
+ description: "Map of key -> boolean (true iff set in process.env)",
146
+ schema: z.object({ presence: z.record(z.string(), z.boolean()) }),
147
+ },
138
148
  400: { description: "Validation error" },
139
149
  },
140
150
  });
@@ -148,7 +158,15 @@ const reloadConfigRoute = route({
148
158
  tags: ["Config"],
149
159
  body: z.object({}).optional(),
150
160
  responses: {
151
- 200: { description: "Reload result" },
161
+ 200: {
162
+ description: "Reload result",
163
+ schema: z.object({
164
+ success: z.literal(true),
165
+ configsLoaded: z.number(),
166
+ keysUpdated: z.array(z.string()),
167
+ integrationsReinitialized: z.array(z.string()),
168
+ }),
169
+ },
152
170
  500: { description: "Reload failed" },
153
171
  },
154
172
  });
@@ -164,7 +182,10 @@ const getConfigById = route({
164
182
  includeSecrets: z.enum(["true", "false"]).optional(),
165
183
  }),
166
184
  responses: {
167
- 200: { description: "Config entry" },
185
+ 200: {
186
+ description: "Config entry",
187
+ schema: SwarmConfigSchema.extend({ message: z.string().optional() }),
188
+ },
168
189
  404: { description: "Config not found" },
169
190
  },
170
191
  });
@@ -181,7 +202,13 @@ const listConfig = route({
181
202
  includeSecrets: z.enum(["true", "false"]).optional(),
182
203
  }),
183
204
  responses: {
184
- 200: { description: "List of config entries" },
205
+ 200: {
206
+ description: "List of config entries",
207
+ schema: z.object({
208
+ configs: z.array(SwarmConfigSchema),
209
+ message: z.string().optional(),
210
+ }),
211
+ },
185
212
  },
186
213
  });
187
214
 
@@ -203,7 +230,7 @@ const upsertConfig = route({
203
230
  description: z.string().nullish(),
204
231
  }),
205
232
  responses: {
206
- 200: { description: "Config entry upserted" },
233
+ 200: { description: "Config entry upserted", schema: SwarmConfigSchema },
207
234
  400: { description: "Validation error" },
208
235
  },
209
236
  });
@@ -218,7 +245,10 @@ const deleteConfig = route({
218
245
  rbac: { permission: "config.delete.any" },
219
246
  params: z.object({ id: z.string() }),
220
247
  responses: {
221
- 200: { description: "Config deleted" },
248
+ 200: {
249
+ description: "Config deleted",
250
+ schema: z.object({ success: z.literal(true) }),
251
+ },
222
252
  404: { description: "Config not found" },
223
253
  },
224
254
  });
@@ -247,7 +277,7 @@ export async function handleConfig(
247
277
  }
248
278
  }
249
279
  }
250
- json(res, {
280
+ getResolvedConfigRoute.respond(res, 200, {
251
281
  configs: result,
252
282
  ...(secretsNote ? { message: `Found ${result.length} config(s).${secretsNote}` } : {}),
253
283
  });
@@ -269,7 +299,7 @@ export async function handleConfig(
269
299
  for (const key of keys) {
270
300
  presence[key] = process.env[key] !== undefined && process.env[key] !== "";
271
301
  }
272
- json(res, { presence });
302
+ envPresence.respond(res, 200, { presence });
273
303
  return true;
274
304
  }
275
305
 
@@ -279,7 +309,7 @@ export async function handleConfig(
279
309
  console.log(
280
310
  `[reload-config] Loaded ${result.configsLoaded} config(s), re-initialized: ${result.integrationsReinitialized.join(", ") || "none"}`,
281
311
  );
282
- json(res, { success: true, ...result });
312
+ reloadConfigRoute.respond(res, 200, { success: true, ...result });
283
313
  } catch (e) {
284
314
  const message = e instanceof Error ? e.message : String(e);
285
315
  console.error("[reload-config] Failed:", message);
@@ -302,7 +332,7 @@ export async function handleConfig(
302
332
  if (effectiveIncludeSecrets && singleResult.isSecret && singleResult.value) {
303
333
  registerVolatileSecret(singleResult.value, `config:${singleResult.key}`);
304
334
  }
305
- json(res, {
335
+ getConfigById.respond(res, 200, {
306
336
  ...singleResult,
307
337
  ...(secretsNote ? { message: `Found 1 config(s).${secretsNote}` } : {}),
308
338
  });
@@ -328,7 +358,7 @@ export async function handleConfig(
328
358
  }
329
359
  }
330
360
  }
331
- json(res, {
361
+ listConfig.respond(res, 200, {
332
362
  configs: listResult,
333
363
  ...(secretsNote ? { message: `Found ${listResult.length} config(s).${secretsNote}` } : {}),
334
364
  });
@@ -380,8 +410,8 @@ export async function handleConfig(
380
410
  if (scope === "global") {
381
411
  scheduleIntegrationsReload();
382
412
  }
383
- const result = includeSecrets || !config.isSecret ? config : maskSecrets([config])[0];
384
- json(res, result);
413
+ const result = includeSecrets || !config.isSecret ? config : maskSecrets([config])[0]!;
414
+ upsertConfig.respond(res, 200, result);
385
415
  } catch (_error) {
386
416
  jsonError(res, "Failed to upsert config", 500);
387
417
  }
@@ -405,7 +435,7 @@ export async function handleConfig(
405
435
  if (existing.scope === "global") {
406
436
  scheduleIntegrationsReload();
407
437
  }
408
- json(res, { success: true });
438
+ deleteConfig.respond(res, 200, { success: true });
409
439
  return true;
410
440
  }
411
441
 
@@ -6,9 +6,36 @@ import {
6
6
  getContextSummaryByTaskId,
7
7
  getTaskById,
8
8
  } from "../be/db";
9
- import { ContextFormulaSchema, ContextSnapshotEventTypeSchema } from "../types";
9
+ import {
10
+ ContextFormulaSchema,
11
+ ContextSnapshotEventTypeSchema,
12
+ ContextSnapshotSchema,
13
+ } from "../types";
10
14
  import { route } from "./route-def";
11
- import { json, jsonError } from "./utils";
15
+ import { jsonError } from "./utils";
16
+
17
+ // ─── Response Schemas ───────────────────────────────────────────────────────
18
+
19
+ const PostContextResponseSchema = z.object({
20
+ ok: z.literal(true),
21
+ snapshotId: z.uuid(),
22
+ });
23
+
24
+ // Mirrors the `ContextSummary` interface returned by
25
+ // `getContextSummaryByTaskId` in src/be/db.ts — not a named entity schema in
26
+ // types.ts, so defined locally.
27
+ const ContextSummarySchema = z.object({
28
+ compactionCount: z.number().int().min(0),
29
+ peakContextPercent: z.number().min(0).max(100).nullable(),
30
+ peakContextTokens: z.number().int().min(0).nullable(),
31
+ contextWindowSize: z.number().int().min(0).nullable(),
32
+ snapshotCount: z.number().int().min(0),
33
+ });
34
+
35
+ const GetContextResponseSchema = z.object({
36
+ snapshots: z.array(ContextSnapshotSchema),
37
+ summary: ContextSummarySchema,
38
+ });
12
39
 
13
40
  // ─── Route Definitions ───────────────────────────────────────────────────────
14
41
 
@@ -34,7 +61,7 @@ const postContext = route({
34
61
  contextFormula: ContextFormulaSchema.optional(),
35
62
  }),
36
63
  responses: {
37
- 200: { description: "Snapshot recorded" },
64
+ 200: { description: "Snapshot recorded", schema: PostContextResponseSchema },
38
65
  400: { description: "Validation error" },
39
66
  404: { description: "Task not found" },
40
67
  },
@@ -52,7 +79,7 @@ const getContext = route({
52
79
  limit: z.coerce.number().int().min(1).max(500).default(100),
53
80
  }),
54
81
  responses: {
55
- 200: { description: "Context snapshot history" },
82
+ 200: { description: "Context snapshot history", schema: GetContextResponseSchema },
56
83
  404: { description: "Task not found" },
57
84
  },
58
85
  auth: { apiKey: true },
@@ -97,7 +124,7 @@ export async function handleContext(
97
124
  contextFormula: parsed.body.contextFormula,
98
125
  });
99
126
 
100
- json(res, { ok: true, snapshotId: snapshot.id });
127
+ postContext.respond(res, 200, { ok: true, snapshotId: snapshot.id });
101
128
  return true;
102
129
  }
103
130
 
@@ -114,7 +141,7 @@ export async function handleContext(
114
141
  const snapshots = getContextSnapshotsByTaskId(parsed.params.id, parsed.query.limit);
115
142
  const summary = getContextSummaryByTaskId(parsed.params.id);
116
143
 
117
- json(res, { snapshots, summary });
144
+ getContext.respond(res, 200, { snapshots, summary });
118
145
  return true;
119
146
  }
120
147