@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
@@ -5,7 +5,9 @@ import {
5
5
  type Server,
6
6
  type ServerResponse,
7
7
  } from "node:http";
8
- import { closeDb, createAgent, createTaskExtended, createUser, initDb } from "../be/db";
8
+ import { createApp } from "../apps/store";
9
+ import { closeDb, createAgent, createTaskExtended, createUser, getDb, initDb } from "../be/db";
10
+ import { insertScript } from "../be/scripts/db";
9
11
  import { handleAssets } from "../http/assets";
10
12
  import { handlePages } from "../http/pages";
11
13
  import { handleSchedules } from "../http/schedules";
@@ -146,9 +148,44 @@ describe("asset namespace REST contract", () => {
146
148
  expect(page.status).toBe(201);
147
149
  expect(page.body.key).toBe("shared/team/");
148
150
 
151
+ const app = createApp({
152
+ name: "Namespaced app",
153
+ definition: { models: {}, pages: {}, defaultPage: "main" } as never,
154
+ });
155
+ const script = insertScript({
156
+ name: `namespaced-script-${Date.now()}`,
157
+ scope: "agent",
158
+ scopeId: agentId,
159
+ source: "export default async function () { return { ok: true }; }",
160
+ description: "Namespaced script",
161
+ intent: "Exercise asset namespace moves",
162
+ signatureJson: "{}",
163
+ agentId,
164
+ embeddingMode: "skip",
165
+ });
166
+ expect(
167
+ await api("PATCH", `/api/assets/app/${app.id}/key`, { key: "shared/team/" }, { agentId }),
168
+ ).toMatchObject({ status: 200, body: { entityType: "app", key: "shared/team/" } });
169
+ expect(
170
+ await api(
171
+ "PATCH",
172
+ `/api/assets/script/${script.id}/key`,
173
+ { key: "shared/team/" },
174
+ { agentId },
175
+ ),
176
+ ).toMatchObject({ status: 200, body: { entityType: "script", key: "shared/team/" } });
177
+ expect(
178
+ await api(
179
+ "PATCH",
180
+ `/api/assets/script/${script.id}/key`,
181
+ { key: "shared/other/" },
182
+ { agentId: secondAgentId },
183
+ ),
184
+ ).toMatchObject({ status: 403 });
185
+
149
186
  const aggregate = await api(
150
187
  "GET",
151
- "/api/assets?keyPrefix=shared/team&types=task,workflow,schedule,page",
188
+ "/api/assets?keyPrefix=shared/team&types=task,workflow,schedule,page,app,script",
152
189
  );
153
190
  expect(aggregate.status).toBe(200);
154
191
  expect(aggregate.body.assets).toEqual(
@@ -165,8 +202,17 @@ describe("asset namespace REST contract", () => {
165
202
  key: "shared/team/",
166
203
  }),
167
204
  expect.objectContaining({ entityType: "page", id: page.body.id, key: "shared/team/" }),
205
+ expect.objectContaining({ entityType: "app", id: app.id, key: "shared/team/" }),
206
+ expect.objectContaining({ entityType: "script", id: script.id, key: "shared/team/" }),
168
207
  ]),
169
208
  );
209
+ expect(
210
+ getDb()
211
+ .prepare<{ entity_type: string }, [string, string]>(
212
+ "SELECT entity_type FROM asset_key_history WHERE entity_id IN (?, ?) ORDER BY entity_type",
213
+ )
214
+ .all(app.id, script.id),
215
+ ).toEqual([{ entity_type: "app" }, { entity_type: "script" }]);
170
216
  expect(JSON.stringify(aggregate.body)).not.toContain("namespaced task");
171
217
  expect(JSON.stringify(aggregate.body)).not.toContain("private content omitted");
172
218
 
@@ -1,4 +1,5 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { createApp } from "../apps/store";
2
3
  import { auditAssetKeys } from "../be/asset-key-audit";
3
4
  import {
4
5
  closeDb,
@@ -17,17 +18,35 @@ import {
17
18
  moveAssetKey,
18
19
  upsertAssetKeyMapping,
19
20
  } from "../be/db";
21
+ import { insertScript } from "../be/scripts/db";
20
22
  import { createStandaloneScheduleTask } from "../scheduler/scheduler";
21
23
 
22
24
  const TEST_DB_PATH = "./test-asset-key-invariants.sqlite";
23
25
 
24
26
  let agentId: string;
25
27
  let userId: string;
28
+ let appId: string;
29
+ let scriptId: string;
26
30
 
27
31
  beforeAll(() => {
28
32
  initDb(TEST_DB_PATH);
29
33
  agentId = createAgent({ name: "namespace-worker", isLead: false, status: "idle" }).id;
30
34
  userId = createUser({ name: "Namespace User", email: "namespace@example.com" }).id;
35
+ appId = createApp({
36
+ name: "Default app",
37
+ definition: { models: {}, pages: {}, defaultPage: "main" } as never,
38
+ }).id;
39
+ scriptId = insertScript({
40
+ name: "default-script",
41
+ scope: "agent",
42
+ scopeId: agentId,
43
+ source: "export default async function () { return { ok: true }; }",
44
+ description: "Asset namespace test script",
45
+ intent: "Verify script asset keys",
46
+ signatureJson: "{}",
47
+ agentId,
48
+ embeddingMode: "skip",
49
+ }).id;
31
50
  });
32
51
 
33
52
  afterAll(() => {
@@ -59,9 +78,30 @@ describe("cross-entity asset namespace invariants", () => {
59
78
  `shared/schedule:${schedule.id}/`,
60
79
  `shared/page:${page.id}/`,
61
80
  ]);
81
+ expect(
82
+ getDb()
83
+ .prepare<{ key: string }, [string]>('SELECT "key" AS key FROM apps WHERE id = ?')
84
+ .get(appId)?.key,
85
+ ).toBe(`shared/app:${appId}/`);
86
+ expect(
87
+ getDb()
88
+ .prepare<{ key: string }, [string]>('SELECT "key" AS key FROM scripts WHERE id = ?')
89
+ .get(scriptId)?.key,
90
+ ).toBe(`shared/script:${scriptId}/`);
62
91
  expect(auditAssetKeys(getDb()).fatalCount).toBe(0);
63
92
  });
64
93
 
94
+ test("app and script triggers reject malformed keys", () => {
95
+ for (const [table, id] of [
96
+ ["apps", appId],
97
+ ["scripts", scriptId],
98
+ ] as const) {
99
+ expect(() =>
100
+ getDb().run(`UPDATE ${table} SET "key" = ? WHERE id = ?`, ["Shared/invalid", id]),
101
+ ).toThrow("invalid asset namespace key");
102
+ }
103
+ });
104
+
65
105
  test("children inherit a parent namespace unless explicitly overridden", () => {
66
106
  const parent = createTaskExtended("parent", { agentId, key: "shared/projects/" });
67
107
  const child = createTaskExtended("child", { parentTaskId: parent.id });
@@ -167,11 +207,63 @@ describe("cross-entity asset namespace invariants", () => {
167
207
  expect(summaries.some((asset) => asset.entityType === "workflow")).toBe(true);
168
208
  expect(summaries.some((asset) => asset.entityType === "schedule")).toBe(true);
169
209
  expect(summaries.some((asset) => asset.entityType === "page")).toBe(true);
210
+ expect(summaries.some((asset) => asset.entityType === "app" && asset.id === appId)).toBe(true);
211
+ expect(summaries.some((asset) => asset.entityType === "script" && asset.id === scriptId)).toBe(
212
+ true,
213
+ );
170
214
  expect(summaries.some((asset) => asset.entityType === "file")).toBe(true);
215
+ const expectedChecked = getDb()
216
+ .prepare<{ count: number }, []>(
217
+ `SELECT
218
+ (SELECT COUNT(*) FROM agent_tasks) +
219
+ (SELECT COUNT(*) FROM workflows) +
220
+ (SELECT COUNT(*) FROM scheduled_tasks) +
221
+ (SELECT COUNT(*) FROM pages) +
222
+ (SELECT COUNT(*) FROM apps) +
223
+ (SELECT COUNT(*) FROM scripts) +
224
+ (SELECT COUNT(*) FROM asset_key_mappings) AS count`,
225
+ )
226
+ .get()!.count;
227
+ expect(auditAssetKeys(getDb()).checked).toBe(expectedChecked);
171
228
  expect(JSON.stringify(summaries)).not.toContain("scheduled work");
172
229
  expect(JSON.stringify(summaries)).not.toContain("<p>ok</p>");
173
230
  });
174
231
 
232
+ test("app and script moves update keys and write typed history rows", () => {
233
+ expect(
234
+ moveAssetKey({ entityType: "app", id: appId, key: "shared/products/", changedBy: userId }),
235
+ ).toBe(true);
236
+ expect(
237
+ moveAssetKey({
238
+ entityType: "script",
239
+ id: scriptId,
240
+ key: "shared/automation/",
241
+ changedBy: userId,
242
+ }),
243
+ ).toBe(true);
244
+
245
+ expect(
246
+ getDb()
247
+ .prepare<{ key: string }, [string]>('SELECT "key" AS key FROM apps WHERE id = ?')
248
+ .get(appId),
249
+ ).toEqual({ key: "shared/products/" });
250
+ expect(
251
+ getDb()
252
+ .prepare<{ key: string }, [string]>('SELECT "key" AS key FROM scripts WHERE id = ?')
253
+ .get(scriptId),
254
+ ).toEqual({ key: "shared/automation/" });
255
+ expect(
256
+ getDb()
257
+ .prepare<{ entity_type: string; new_key: string }, [string, string]>(
258
+ "SELECT entity_type, new_key FROM asset_key_history WHERE entity_id IN (?, ?) ORDER BY entity_type",
259
+ )
260
+ .all(appId, scriptId),
261
+ ).toEqual([
262
+ { entity_type: "app", new_key: "shared/products/" },
263
+ { entity_type: "script", new_key: "shared/automation/" },
264
+ ]);
265
+ });
266
+
175
267
  test("prefix filters treat SQL wildcard characters as literal key content", () => {
176
268
  const literal = createTaskExtended("literal wildcard", { agentId, key: "shared/percent%/" });
177
269
  const neighbor = createTaskExtended("wildcard neighbor", { agentId, key: "shared/percentx/" });
@@ -3,6 +3,7 @@ import { closeDb, createTaskExtended, createUser, getDb, initDb } from "../be/db
3
3
 
4
4
  const FRESH_DB = "./test-asset-key-migration-fresh.sqlite";
5
5
  const HISTORICAL_DB = "./test-asset-key-migration-historical.sqlite";
6
+ const HISTORY_REBUILD_DB = "./test-asset-key-migration-history-rebuild.sqlite";
6
7
  const LEGACY_STATUS_DB = "./test-asset-key-migration-legacy-status.sqlite";
7
8
 
8
9
  const globals = globalThis as typeof globalThis & {
@@ -33,15 +34,59 @@ function dropMigration115ForHistoricalFixture(): void {
33
34
  "idx_workflows_asset_key",
34
35
  "idx_scheduled_tasks_asset_key",
35
36
  "idx_pages_asset_key",
37
+ "idx_apps_asset_key",
38
+ "idx_scripts_asset_key",
36
39
  ]) {
37
40
  db.run(`DROP INDEX IF EXISTS "${index}"`);
38
41
  }
39
42
  db.run("DROP TABLE asset_key_history");
40
43
  db.run("DROP TABLE asset_key_mappings");
41
- for (const table of ["agent_tasks", "workflows", "scheduled_tasks", "pages"]) {
44
+ for (const table of ["agent_tasks", "workflows", "scheduled_tasks", "pages", "apps", "scripts"]) {
42
45
  db.run(`ALTER TABLE "${table}" DROP COLUMN "key"`);
43
46
  }
44
- db.run("DELETE FROM _migrations WHERE version = 115");
47
+ db.run("DELETE FROM _migrations WHERE version IN (115, 129)");
48
+ }
49
+
50
+ function dropMigration129ForHistoricalFixture(): void {
51
+ const db = getDb();
52
+ for (const trigger of [
53
+ "validate_apps_asset_key_insert",
54
+ "validate_apps_asset_key_update",
55
+ "validate_scripts_asset_key_insert",
56
+ "validate_scripts_asset_key_update",
57
+ ]) {
58
+ db.run(`DROP TRIGGER "${trigger}"`);
59
+ }
60
+ db.run("DROP INDEX idx_apps_asset_key");
61
+ db.run("DROP INDEX idx_scripts_asset_key");
62
+ db.run('ALTER TABLE apps DROP COLUMN "key"');
63
+ db.run('ALTER TABLE scripts DROP COLUMN "key"');
64
+
65
+ db.run(`
66
+ CREATE TABLE asset_key_history_before_129 (
67
+ id TEXT PRIMARY KEY,
68
+ entity_type TEXT NOT NULL CHECK (entity_type IN ('task', 'workflow', 'schedule', 'page', 'file')),
69
+ entity_id TEXT NOT NULL,
70
+ previous_key TEXT,
71
+ new_key TEXT NOT NULL,
72
+ changed_by TEXT REFERENCES users(id),
73
+ changed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
74
+ )
75
+ `);
76
+ db.run(`
77
+ INSERT INTO asset_key_history_before_129 (
78
+ id, entity_type, entity_id, previous_key, new_key, changed_by, changed_at
79
+ )
80
+ SELECT id, entity_type, entity_id, previous_key, new_key, changed_by, changed_at
81
+ FROM asset_key_history
82
+ `);
83
+ db.run("DROP TABLE asset_key_history");
84
+ db.run("ALTER TABLE asset_key_history_before_129 RENAME TO asset_key_history");
85
+ db.run(`
86
+ CREATE INDEX idx_asset_key_history_entity
87
+ ON asset_key_history(entity_type, entity_id, changed_at DESC)
88
+ `);
89
+ db.run("DELETE FROM _migrations WHERE version = 129");
45
90
  }
46
91
 
47
92
  beforeAll(async () => {
@@ -50,6 +95,7 @@ beforeAll(async () => {
50
95
  closeDb();
51
96
  await removeDb(FRESH_DB);
52
97
  await removeDb(HISTORICAL_DB);
98
+ await removeDb(HISTORY_REBUILD_DB);
53
99
  await removeDb(LEGACY_STATUS_DB);
54
100
  initDb(FRESH_DB);
55
101
  });
@@ -60,12 +106,20 @@ afterAll(async () => {
60
106
  globals.__savedAssetKeyTemplate = undefined;
61
107
  await removeDb(FRESH_DB);
62
108
  await removeDb(HISTORICAL_DB);
109
+ await removeDb(HISTORY_REBUILD_DB);
63
110
  await removeDb(LEGACY_STATUS_DB);
64
111
  });
65
112
 
66
- describe("migration 115 asset namespace keys", () => {
113
+ describe("asset namespace key migrations", () => {
67
114
  test("fresh schema has mandatory defaults, non-unique indexes, triggers, and mapping tables", () => {
68
- for (const table of ["agent_tasks", "workflows", "scheduled_tasks", "pages"]) {
115
+ for (const table of [
116
+ "agent_tasks",
117
+ "workflows",
118
+ "scheduled_tasks",
119
+ "pages",
120
+ "apps",
121
+ "scripts",
122
+ ]) {
69
123
  const column = getDb()
70
124
  .prepare<{ name: string; notnull: number; dflt_value: string | null }, []>(
71
125
  `PRAGMA table_info("${table}")`,
@@ -89,6 +143,8 @@ describe("migration 115 asset namespace keys", () => {
89
143
  "idx_workflows_asset_key",
90
144
  "idx_scheduled_tasks_asset_key",
91
145
  "idx_pages_asset_key",
146
+ "idx_apps_asset_key",
147
+ "idx_scripts_asset_key",
92
148
  ]) {
93
149
  expect(indexes.has(index)).toBe(true);
94
150
  }
@@ -98,7 +154,7 @@ describe("migration 115 asset namespace keys", () => {
98
154
  "SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'trigger' AND name LIKE 'validate_%asset_key%'",
99
155
  )
100
156
  .get()?.count,
101
- ).toBeGreaterThanOrEqual(8);
157
+ ).toBeGreaterThanOrEqual(12);
102
158
  for (const table of ["asset_key_mappings", "asset_key_history"]) {
103
159
  expect(
104
160
  getDb()
@@ -108,6 +164,13 @@ describe("migration 115 asset namespace keys", () => {
108
164
  .get(table)?.present,
109
165
  ).toBe(1);
110
166
  }
167
+ const historySchema = getDb()
168
+ .prepare<{ sql: string }, []>(
169
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'asset_key_history'",
170
+ )
171
+ .get()!.sql;
172
+ expect(historySchema).toContain("'app'");
173
+ expect(historySchema).toContain("'script'");
111
174
  });
112
175
 
113
176
  test("old insert statements may omit key and repeated shared namespaces are valid", () => {
@@ -132,8 +195,27 @@ describe("migration 115 asset namespace keys", () => {
132
195
  `INSERT INTO pages (agentId, slug, title, contentType, authMode, body)
133
196
  VALUES ('legacy-agent', 'legacy-page', 'Legacy page', 'text/html', 'authed', '<p>ok</p>')`,
134
197
  );
198
+ getDb().run(
199
+ `INSERT INTO apps (id, name, definition, created_at, updated_at)
200
+ VALUES (?, 'legacy app', '{}', ?, ?)`,
201
+ [crypto.randomUUID(), now, now],
202
+ );
203
+ getDb().run(
204
+ `INSERT INTO scripts (
205
+ id, name, scope, source, description, intent, signatureJson,
206
+ contentHash, createdAt, updatedAt
207
+ ) VALUES (?, 'legacy-script', 'global', 'export default () => null', '', '', '{}', '', ?, ?)`,
208
+ [crypto.randomUUID(), now, now],
209
+ );
135
210
 
136
- for (const table of ["agent_tasks", "workflows", "scheduled_tasks", "pages"]) {
211
+ for (const table of [
212
+ "agent_tasks",
213
+ "workflows",
214
+ "scheduled_tasks",
215
+ "pages",
216
+ "apps",
217
+ "scripts",
218
+ ]) {
137
219
  const count = getDb()
138
220
  .prepare<{ count: number }, []>(
139
221
  `SELECT COUNT(*) AS count FROM "${table}" WHERE "key" = 'shared/'`,
@@ -143,6 +225,63 @@ describe("migration 115 asset namespace keys", () => {
143
225
  }
144
226
  });
145
227
 
228
+ test("migration 129 preserves existing history rows while widening entity types", () => {
229
+ closeDb();
230
+ initDb(HISTORY_REBUILD_DB);
231
+
232
+ const historyId = crypto.randomUUID();
233
+ const entityId = crypto.randomUUID();
234
+ const user = createUser({ name: "History User", email: "history@example.com" });
235
+ const changedAt = "2026-08-08T12:34:56.789Z";
236
+ getDb().run(
237
+ `INSERT INTO asset_key_history (
238
+ id, entity_type, entity_id, previous_key, new_key, changed_by, changed_at
239
+ ) VALUES (?, 'task', ?, 'shared/inbox/', 'shared/archive/', ?, ?)`,
240
+ [historyId, entityId, user.id, changedAt],
241
+ );
242
+
243
+ dropMigration129ForHistoricalFixture();
244
+ closeDb();
245
+ initDb(HISTORY_REBUILD_DB);
246
+
247
+ try {
248
+ expect(
249
+ getDb()
250
+ .prepare<
251
+ {
252
+ id: string;
253
+ entity_type: string;
254
+ entity_id: string;
255
+ previous_key: string;
256
+ new_key: string;
257
+ changed_by: string;
258
+ changed_at: string;
259
+ },
260
+ [string]
261
+ >("SELECT * FROM asset_key_history WHERE id = ?")
262
+ .get(historyId),
263
+ ).toEqual({
264
+ id: historyId,
265
+ entity_type: "task",
266
+ entity_id: entityId,
267
+ previous_key: "shared/inbox/",
268
+ new_key: "shared/archive/",
269
+ changed_by: user.id,
270
+ changed_at: changedAt,
271
+ });
272
+ expect(
273
+ getDb()
274
+ .prepare<{ count: number }, []>(
275
+ "SELECT COUNT(*) AS count FROM _migrations WHERE version = 129",
276
+ )
277
+ .get()?.count,
278
+ ).toBe(1);
279
+ } finally {
280
+ closeDb();
281
+ initDb(FRESH_DB);
282
+ }
283
+ });
284
+
146
285
  test("triggers reject malformed and unknown-personal keys but allow an existing personal user", () => {
147
286
  const taskId = getDb()
148
287
  .prepare<{ id: string }, []>("SELECT id FROM agent_tasks LIMIT 1")
@@ -166,6 +305,13 @@ describe("migration 115 asset namespace keys", () => {
166
305
  taskId,
167
306
  ]),
168
307
  ).not.toThrow();
308
+
309
+ for (const table of ["apps", "scripts"]) {
310
+ const id = getDb().prepare<{ id: string }, []>(`SELECT id FROM ${table} LIMIT 1`).get()!.id;
311
+ expect(() =>
312
+ getDb().run(`UPDATE ${table} SET "key" = ? WHERE id = ?`, ["INVALID", id]),
313
+ ).toThrow("invalid asset namespace key");
314
+ }
169
315
  });
170
316
 
171
317
  test("upgrades a historical database, backfills attachment mappings, and is repeatable", async () => {
@@ -176,6 +322,8 @@ describe("migration 115 asset namespace keys", () => {
176
322
  const now = new Date().toISOString();
177
323
  const taskId = crypto.randomUUID();
178
324
  const attachmentId = crypto.randomUUID();
325
+ const appId = crypto.randomUUID();
326
+ const scriptId = crypto.randomUUID();
179
327
  getDb().run(
180
328
  `INSERT INTO agent_tasks (id, task, status, source, createdAt, lastUpdatedAt)
181
329
  VALUES (?, 'historical task', 'unassigned', 'api', ?, ?)`,
@@ -188,6 +336,18 @@ describe("migration 115 asset namespace keys", () => {
188
336
  'reports/artifact.md', 'org', 'drive')`,
189
337
  [attachmentId, taskId],
190
338
  );
339
+ getDb().run(
340
+ `INSERT INTO apps (id, name, definition, created_at, updated_at)
341
+ VALUES (?, 'historical app', '{}', ?, ?)`,
342
+ [appId, now, now],
343
+ );
344
+ getDb().run(
345
+ `INSERT INTO scripts (
346
+ id, name, scope, source, description, intent, signatureJson,
347
+ contentHash, createdAt, updatedAt
348
+ ) VALUES (?, 'historical-script', 'global', 'export default () => null', '', '', '{}', '', ?, ?)`,
349
+ [scriptId, now, now],
350
+ );
191
351
  closeDb();
192
352
 
193
353
  initDb(HISTORICAL_DB);
@@ -201,16 +361,22 @@ describe("migration 115 asset namespace keys", () => {
201
361
  )
202
362
  .get("reports/artifact.md"),
203
363
  ).toEqual({ key: "shared/", source_entity_id: attachmentId });
364
+ expect(getDb().query('SELECT "key" FROM apps WHERE id = ?').get(appId)).toEqual({
365
+ key: "shared/",
366
+ });
367
+ expect(getDb().query('SELECT "key" FROM scripts WHERE id = ?').get(scriptId)).toEqual({
368
+ key: "shared/",
369
+ });
204
370
 
205
371
  closeDb();
206
372
  expect(() => initDb(HISTORICAL_DB)).not.toThrow();
207
373
  expect(
208
374
  getDb()
209
375
  .prepare<{ count: number }, []>(
210
- "SELECT COUNT(*) AS count FROM _migrations WHERE version = 115",
376
+ "SELECT COUNT(*) AS count FROM _migrations WHERE version IN (115, 129)",
211
377
  )
212
378
  .get()?.count,
213
- ).toBe(1);
379
+ ).toBe(2);
214
380
  });
215
381
 
216
382
  test("preserves asset keys when upgrading a legacy restrictive task status schema", () => {
@@ -34,6 +34,8 @@ describe("asset namespace key normalization", () => {
34
34
  test("builds resource-specific defaults inside the shared namespace", () => {
35
35
  expect(defaultAssetKey("task", "ABC-123")).toBe("shared/task:abc-123/");
36
36
  expect(defaultAssetKey("workflow", "workflow-id")).toBe("shared/workflow:workflow-id/");
37
+ expect(defaultAssetKey("app", "APP-ID")).toBe("shared/app:app-id/");
38
+ expect(defaultAssetKey("script", "SCRIPT-ID")).toBe("shared/script:script-id/");
37
39
  expect(defaultAssetKey("fs:agent-fs", "mapping-id")).toBe("shared/fs:agent-fs:mapping-id/");
38
40
  });
39
41
 
@@ -328,7 +328,7 @@ describe("credential broker", () => {
328
328
  expect(authorization).toBe("Bearer ghp_secret");
329
329
  });
330
330
 
331
- test("does not substitute placeholders for non-allowlisted hosts", async () => {
331
+ test("drops a placeholder header for non-allowlisted hosts instead of forwarding the literal", async () => {
332
332
  let authorization: string | null = null;
333
333
  globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => {
334
334
  authorization = new Headers(init?.headers).get("authorization");
@@ -352,7 +352,32 @@ describe("credential broker", () => {
352
352
  headers: { Authorization: "Bearer [REDACTED:GITHUB_TOKEN]" },
353
353
  });
354
354
 
355
- expect(authorization).toBe("Bearer [REDACTED:GITHUB_TOKEN]");
355
+ // The literal placeholder is useless to any provider and must never egress.
356
+ expect(authorization).toBeNull();
357
+ });
358
+
359
+ test("drops a placeholder header when no binding resolves (fresh install)", async () => {
360
+ let authorization: string | null = null;
361
+ let accept: string | null = null;
362
+ globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => {
363
+ const headers = new Headers(init?.headers);
364
+ authorization = headers.get("authorization");
365
+ accept = headers.get("accept");
366
+ return Response.json([]);
367
+ }) as typeof fetch;
368
+
369
+ patchFetchWithCredentialBroker([]);
370
+
371
+ await fetch("https://api.github.com/repos/owner/name/issues", {
372
+ headers: {
373
+ Accept: "application/vnd.github+json",
374
+ Authorization: "Bearer [REDACTED:GITHUB_TOKEN]",
375
+ },
376
+ });
377
+
378
+ // No GITHUB_TOKEN configured: auth is omitted so public repos still pull.
379
+ expect(authorization).toBeNull();
380
+ expect(accept).toBe("application/vnd.github+json");
356
381
  });
357
382
 
358
383
  test("substitutes query placeholders for allowlisted hosts", async () => {