@desplega.ai/agent-swarm 2.4.3 → 2.4.5

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 (270) hide show
  1. package/dist/{acp-adapter-1885dpr7.js → acp-adapter-2wdkktv6.js} +1 -2
  2. package/dist/{actions-pn76n32q.js → actions-c40ebcm4.js} +18 -18
  3. package/dist/{amp-adapter-af84rx1d.js → amp-adapter-wb67m95z.js} +2 -2
  4. package/dist/{app-6sv8mhzm.js → app-353vre8g.js} +3 -3
  5. package/dist/{assistant-sc2tf825.js → assistant-sj1njh23.js} +21 -21
  6. package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-b9c3az3c.js} +1 -1
  7. package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-qdkvxeem.js} +1 -1
  8. package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
  9. package/dist/{boot-seeds-37sp626b.js → boot-seeds-j2273s54.js} +4 -4
  10. package/dist/{claude-adapter-rp0jtnwr.js → claude-adapter-m7q0a085.js} +2 -2
  11. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-5ptb6tzs.js} +2 -2
  12. package/dist/{claude-managed-setup-rdt248k7.js → claude-managed-setup-smrqk5j5.js} +3 -3
  13. package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-38xnz21e.js} +8284 -8267
  14. package/dist/{cli-6eyncg0y.js → cli-018r4jq6.js} +34 -6
  15. package/dist/{cli-sds5zzs8.js → cli-1f3qzw6y.js} +78 -12
  16. package/dist/{cli-f0p0f6ja.js → cli-1sp861s4.js} +7 -7
  17. package/dist/{cli-dm8p2dpf.js → cli-1syn2xy1.js} +10 -10
  18. package/dist/{cli-tkf4x6ha.js → cli-37j04k05.js} +11 -11
  19. package/dist/{cli-v87dr6e3.js → cli-3a45zcgm.js} +1 -1
  20. package/dist/{cli-1n9gdg56.js → cli-43x64hv3.js} +4 -4
  21. package/dist/{cli-p0qerd2r.js → cli-4zfqgjc5.js} +2 -2
  22. package/dist/{cli-aa3tb29r.js → cli-7c9qs530.js} +1 -1
  23. package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
  24. package/dist/{cli-1xsraagq.js → cli-94gwyg7m.js} +8 -8
  25. package/dist/{cli-0dxxwfkt.js → cli-9hstj09h.js} +17 -14
  26. package/dist/{cli-1012xb7j.js → cli-9yqgm9nd.js} +22 -22
  27. package/dist/{cli-pygdsgh8.js → cli-a92r1127.js} +1 -1
  28. package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
  29. package/dist/{cli-bxrqvpg5.js → cli-be2161h1.js} +7 -7
  30. package/dist/{cli-npfe9t1s.js → cli-bnes4z6b.js} +2 -2
  31. package/dist/{cli-aypbg9r2.js → cli-cxxe61f6.js} +1 -1
  32. package/dist/{cli-vq9gqarq.js → cli-dk44dxec.js} +1 -1
  33. package/dist/{cli-836ehxmt.js → cli-eww120g8.js} +3 -2
  34. package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
  35. package/dist/{cli-mzkwkbdy.js → cli-f0g3pgvf.js} +1 -1
  36. package/dist/{cli-1ma335fz.js → cli-ftc1g5tj.js} +2 -2
  37. package/dist/{cli-4yef8ghv.js → cli-fv5hepyy.js} +3 -3
  38. package/dist/{cli-58vwn100.js → cli-g35m2xwm.js} +4 -4
  39. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  40. package/dist/{cli-gx0j2khs.js → cli-j99azg3g.js} +3 -3
  41. package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
  42. package/dist/{cli-qp2m4btv.js → cli-kpw04va0.js} +2 -2
  43. package/dist/{cli-8y99kfey.js → cli-ms85309d.js} +33 -15
  44. package/dist/{cli-kgnkyabk.js → cli-n72rg53p.js} +1555 -645
  45. package/dist/{cli-e4d78xsv.js → cli-p14m2cnp.js} +1 -1
  46. package/dist/{cli-1eszr343.js → cli-qfxc84gh.js} +1 -1
  47. package/dist/{cli-wk28hzqn.js → cli-qwhy2qma.js} +1 -1
  48. package/dist/{cli-73ezfhha.js → cli-st8aw1m6.js} +228 -11
  49. package/dist/{cli-zvbb4n0e.js → cli-tw4p90jb.js} +92 -192
  50. package/dist/{cli-038fga5k.js → cli-v0yrfmdh.js} +148 -183
  51. package/dist/{cli-6aen3ezb.js → cli-v8fjakwf.js} +1 -1
  52. package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
  53. package/dist/{cli-vcgscaan.js → cli-wt0jf9hm.js} +217 -80
  54. package/dist/{cli-s9rr4gr2.js → cli-xpa64rek.js} +1065 -305
  55. package/dist/{cli-h0mgxann.js → cli-z7d29rw9.js} +2 -2
  56. package/dist/cli.js +45 -20
  57. package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-tn6k5mkg.js} +1 -1
  58. package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-7vjt7jbw.js} +1 -1
  59. package/dist/{commands-hd574th6.js → commands-t7rz9146.js} +2 -2
  60. package/dist/{controller-pz5mcfg4.js → controller-m2jasma5.js} +2 -2
  61. package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-9ftpex99.js} +19 -19
  62. package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-mjv79e4z.js} +1 -1
  63. package/dist/{db-6nf1207q.js → db-6sfrn9g3.js} +1 -1
  64. package/dist/{db-h06dxp05.js → db-rn673r06.js} +3 -3
  65. package/dist/{dispatcher-zthpy4m1.js → dispatcher-c155xz0p.js} +17 -17
  66. package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-3mmszc1x.js} +1 -1
  67. package/dist/{external-channel-x01t7ke6.js → external-channel-35k6tkv8.js} +2 -2
  68. package/dist/{grok-adapter-h21jv594.js → grok-adapter-tfrq7tg1.js} +2 -3
  69. package/dist/handlers-7gw64dxw.js +39 -0
  70. package/dist/{heartbeat-zzfksckz.js → heartbeat-dyg4npkp.js} +20 -20
  71. package/dist/{hook-z4zhzj1f.js → hook-drspbwpe.js} +1 -1
  72. package/dist/{hook-3xfqhh2s.js → hook-x26b31bg.js} +1 -1
  73. package/dist/{http-q813tmya.js → http-pk972xyw.js} +571 -294
  74. package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
  75. package/dist/{index-x8xbyn1t.js → index-arytgj5q.js} +1 -1
  76. package/dist/{index-wbbs14c5.js → index-bgdyvcnv.js} +17 -17
  77. package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
  78. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  79. package/dist/{index-m0a4ydqj.js → index-gabbcdn2.js} +17 -17
  80. package/dist/{index-220tk9bw.js → index-jwxkkex2.js} +139 -76
  81. package/dist/index-tfb26zhy.js +41 -0
  82. package/dist/{keepalive-y5t1my7m.js → keepalive-7yrng7px.js} +3 -3
  83. package/dist/{lead-2xwtq66b.js → lead-7e02z839.js} +29 -30
  84. package/dist/{maintenance-ybfsaedv.js → maintenance-e6cemdqt.js} +1 -1
  85. package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
  86. package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
  87. package/dist/{onboard-xn6fkkys.js → onboard-0a9y5yrc.js} +2 -2
  88. package/dist/{otel-impl-e6mn0z13.js → otel-impl-trjpkfb7.js} +1 -1
  89. package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-d2b5my0y.js} +3 -3
  90. package/dist/{rbac-roles-mb470p4e.js → rbac-roles-hey3yv9d.js} +2 -2
  91. package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-nxh5ahwk.js} +2 -2
  92. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  93. package/dist/{render-v2-g0306nh1.js → render-v2-rr3p2atg.js} +3 -3
  94. package/dist/{resume-v315hnq6.js → resume-2efkf1s1.js} +5 -5
  95. package/dist/{session-access-2zg285a1.js → session-access-s2mttxpw.js} +6 -4
  96. package/dist/{setup-a4mtf02k.js → setup-vypma5yt.js} +1 -1
  97. package/dist/{work-objects-9fr0fgtn.js → work-objects-rvhckdkp.js} +20 -20
  98. package/dist/{worker-qae22jeb.js → worker-5snytpbd.js} +29 -30
  99. package/dist/worker-token-xf11yyds.js +136 -0
  100. package/openapi.json +303 -13
  101. package/package.json +8 -8
  102. package/src/apps/action-runner.ts +4 -0
  103. package/src/apps/catalog.generated.json +209 -4
  104. package/src/apps/page-validator.ts +21 -3
  105. package/src/be/chunking.ts +42 -9
  106. package/src/be/db/tasks/draft-text.ts +15 -0
  107. package/src/be/db/tasks/read.ts +14 -1
  108. package/src/be/db.ts +5 -1
  109. package/src/be/memory/index-content.ts +36 -17
  110. package/src/be/memory/providers/openai-embedding.ts +115 -61
  111. package/src/be/memory/providers/sqlite-store.ts +69 -23
  112. package/src/be/memory/reranker.ts +31 -1
  113. package/src/be/memory/search-candidates.ts +47 -0
  114. package/src/be/memory/types.ts +14 -0
  115. package/src/be/migrations/232_agent_session_tokens.sql +41 -0
  116. package/src/be/scripts/typecheck.ts +2 -0
  117. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +116 -55
  118. package/src/be/seed-scripts/index.ts +2 -2
  119. package/src/be/swarm-config-guard.ts +40 -1
  120. package/src/be/task-attachment-store.ts +30 -0
  121. package/src/be/users.ts +169 -12
  122. package/src/cli.tsx +24 -0
  123. package/src/commands/worker-token.ts +164 -0
  124. package/src/extensions/catalog.generated.json +4 -4
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/extensions/contract.ts +7 -0
  127. package/src/fs/provider.ts +13 -1
  128. package/src/heartbeat/heartbeat.ts +31 -11
  129. package/src/http/apps.ts +43 -5
  130. package/src/http/codex-oauth-keep-warm.ts +27 -9
  131. package/src/http/config.ts +5 -3
  132. package/src/http/core.ts +2 -0
  133. package/src/http/fs.ts +54 -34
  134. package/src/http/mcp-body.ts +37 -0
  135. package/src/http/mcp-user.ts +3 -5
  136. package/src/http/mcp.ts +6 -7
  137. package/src/http/memory.ts +26 -15
  138. package/src/http/page-proxy.ts +61 -5
  139. package/src/http/pages-public.ts +27 -44
  140. package/src/http/pages.ts +4 -4
  141. package/src/http/script-runs.ts +2 -0
  142. package/src/http/session-access.ts +51 -2
  143. package/src/http/sessions.ts +249 -10
  144. package/src/http/tasks.ts +4 -32
  145. package/src/http/users.ts +12 -1
  146. package/src/integrations/chat-sdk/adapters.ts +7 -3
  147. package/src/integrations/chat-sdk/attachments.ts +90 -12
  148. package/src/integrations/chat-sdk/facts.ts +120 -0
  149. package/src/integrations/chat-sdk/inbound.ts +156 -8
  150. package/src/integrations/chat-sdk/limits.ts +24 -0
  151. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  152. package/src/integrations/chat-sdk/outbound.ts +234 -4
  153. package/src/integrations/chat-sdk/relay.ts +4 -1
  154. package/src/integrations/chat-sdk/router.ts +28 -2
  155. package/src/integrations/chat-sdk/steering.ts +9 -0
  156. package/src/integrations/chat-sdk/templates.ts +14 -0
  157. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  158. package/src/mcp-limits.ts +20 -0
  159. package/src/pages/browser-sdk.ts +8 -3
  160. package/src/pages/feedback-overlay.ts +1 -1
  161. package/src/providers/acp-adapter.ts +33 -13
  162. package/src/providers/acp-swarm-events.ts +1 -1
  163. package/src/providers/amp-adapter.ts +1 -1
  164. package/src/providers/codex-adapter.ts +106 -119
  165. package/src/providers/codex-agents-md.ts +35 -229
  166. package/src/providers/codex-oauth/storage.ts +1 -1
  167. package/src/providers/cursor-adapter.ts +1 -1
  168. package/src/rbac/legacy-policy.ts +3 -1
  169. package/src/rbac/permissions.ts +4 -0
  170. package/src/realtime/transport.ts +1 -2
  171. package/src/script-workflows/workflow-ctx.ts +1 -0
  172. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  173. package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
  174. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  175. package/src/server-user.ts +2 -0
  176. package/src/server.ts +2 -0
  177. package/src/slack/assistant.ts +2 -2
  178. package/src/slack/files.ts +2 -0
  179. package/src/slack/handlers.ts +4 -4
  180. package/src/slack/inbound-files.ts +39 -6
  181. package/src/slack/thread-buffer.ts +2 -1
  182. package/src/tasks/task-terminal-effects.ts +21 -17
  183. package/src/tests/acp-adapter.test.ts +10 -23
  184. package/src/tests/acp-fake-agent.ts +36 -0
  185. package/src/tests/acp-swarm-events.test.ts +1 -1
  186. package/src/tests/agent-session-tokens.test.ts +452 -0
  187. package/src/tests/apps-action-parent-task.test.ts +295 -0
  188. package/src/tests/apps-card-grid.test.ts +197 -0
  189. package/src/tests/apps-catalog-additions.test.ts +120 -0
  190. package/src/tests/apps-tree.test.ts +129 -0
  191. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  192. package/src/tests/chat-sdk-config.test.ts +11 -0
  193. package/src/tests/chat-sdk-facts.test.ts +23 -0
  194. package/src/tests/chat-sdk-inbound.test.ts +558 -4
  195. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  196. package/src/tests/codex-adapter.test.ts +167 -242
  197. package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
  198. package/src/tests/config-api-only-keys.test.ts +9 -0
  199. package/src/tests/config-session-auth.test.ts +14 -0
  200. package/src/tests/cors-allowlist.test.ts +1 -1
  201. package/src/tests/docs-index-footprint.test.js +2 -2
  202. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  203. package/src/tests/grok-adapter.test.ts +3 -3
  204. package/src/tests/heartbeat-checklist.test.ts +33 -0
  205. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  206. package/src/tests/heartbeat-jev-gate.test.ts +265 -1
  207. package/src/tests/kv-page-proxy.test.ts +91 -5
  208. package/src/tests/launch-password-rejection.test.ts +2 -2
  209. package/src/tests/mcp-input-caps.test.ts +294 -0
  210. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  211. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  212. package/src/tests/memory.test.ts +14 -0
  213. package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
  214. package/src/tests/page-guest-session.test.ts +19 -8
  215. package/src/tests/page-proxy-authed.test.ts +1 -1
  216. package/src/tests/page-proxy.test.ts +17 -10
  217. package/src/tests/page-session.test.ts +102 -1
  218. package/src/tests/pages-authed-mode.test.ts +82 -12
  219. package/src/tests/pages-password-mode.test.ts +5 -5
  220. package/src/tests/rbac-engine.test.ts +1 -0
  221. package/src/tests/realtime-transport.test.ts +13 -7
  222. package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
  223. package/src/tests/script-connections.test.ts +2 -0
  224. package/src/tests/script-runs-http.test.ts +84 -0
  225. package/src/tests/scripts-http.test.ts +10 -1
  226. package/src/tests/session-route-posture.test.ts +2 -0
  227. package/src/tests/swarm-config-export-to-env.test.ts +22 -3
  228. package/src/tests/task-audio-transcription.test.ts +299 -0
  229. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  230. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  231. package/src/tests/transcription.test.ts +299 -0
  232. package/src/tests/worker-token-cli.test.ts +76 -0
  233. package/src/tests/workflow-executors.test.ts +4 -2
  234. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  235. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  236. package/src/tests/workflow-system-one-extraction.test.ts +805 -0
  237. package/src/tools/chat-access.ts +10 -0
  238. package/src/tools/chat-post.ts +31 -11
  239. package/src/tools/chat-reply.ts +32 -10
  240. package/src/tools/memory-search.ts +3 -1
  241. package/src/tools/system-one-decide.ts +17 -4
  242. package/src/types.ts +8 -1
  243. package/src/utils/config-env-export.ts +3 -1
  244. package/src/utils/page-session.ts +133 -20
  245. package/src/utils/request-auth-context.ts +2 -1
  246. package/src/utils/transcription.ts +246 -0
  247. package/src/workflows/definition.ts +7 -1
  248. package/src/workflows/engine.ts +18 -12
  249. package/src/workflows/executors/index.ts +1 -0
  250. package/src/workflows/executors/registry.ts +2 -0
  251. package/src/workflows/executors/system-one-decision.ts +84 -262
  252. package/src/workflows/executors/system-one-extract-wire.ts +57 -0
  253. package/src/workflows/executors/system-one-extraction.ts +830 -0
  254. package/src/workflows/executors/system-one-providers.ts +316 -114
  255. package/src/workflows/executors/system-one-transport.ts +264 -0
  256. package/templates/extensions/slack-reply-gate/README.md +24 -22
  257. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  258. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  259. package/templates/official/lead/HEARTBEAT.md +1 -1
  260. package/templates/skills/apps/SKILL.md +31 -7
  261. package/templates/skills/apps/content.md +31 -7
  262. package/templates/skills/chat-sdk/content.md +6 -2
  263. package/templates/skills/code-quality/content.md +2 -1
  264. package/templates/skills/heartbeat-runbook/content.md +8 -8
  265. package/templates/skills/pages/content.md +6 -4
  266. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  267. package/templates/skills/swarm-scripts/content.md +2 -2
  268. package/dist/cli-v1sjhn52.js +0 -7
  269. package/dist/handlers-2qjw434w.js +0 -39
  270. package/dist/index-d0g18bax.js +0 -41
@@ -0,0 +1,295 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import {
4
+ createServer as createHttpServer,
5
+ type IncomingMessage,
6
+ type Server,
7
+ type ServerResponse,
8
+ } from "node:http";
9
+ import { parseAppDefinition } from "../apps/definition";
10
+ import { createApp } from "../apps/store";
11
+ import {
12
+ closeDb,
13
+ createAgent,
14
+ createTask,
15
+ createTaskExtended,
16
+ createUser,
17
+ getTaskById,
18
+ initDb,
19
+ } from "../be/db";
20
+ import { type IdentityActor, mintToken } from "../be/users";
21
+ import { handleApps } from "../http/apps";
22
+ import { resolveHttpRequestAuth } from "../http/auth";
23
+ import { SESSION_WRITE_DENIED } from "../http/session-access";
24
+ import { getPathSegments, parseQueryParams } from "../http/utils";
25
+ import { setRequestAuth } from "../utils/request-auth-context";
26
+ import { listenOnFreePort } from "./test-net";
27
+
28
+ const TEST_DB_PATH = `/private/tmp/test-apps-action-parent-task-${process.pid}.sqlite`;
29
+ const API_KEY = "example-apps-action-parent-task-key";
30
+ const OPERATOR_ACTOR: IdentityActor = { kind: "operator", id: "apps-action-parent-task-test" };
31
+ /** Non-lead worker: the default caller. */
32
+ const AGENT_ID = crypto.randomUUID();
33
+ const LEAD_ID = crypto.randomUUID();
34
+ /** Owns the parent task the worker has no context for. */
35
+ const OTHER_AGENT_ID = crypto.randomUUID();
36
+ const TASK_ACTION = "generateScreen";
37
+ const SYNC_ACTION = "syncNotes";
38
+ const CHAIN_ISSUE = "$row, $rowIndex, $form, and $chain bindings are only allowed in action params";
39
+
40
+ let server: Server;
41
+ let base = "";
42
+ /** A user with no share on anyone else's session. */
43
+ let userToken = "";
44
+ let previousApiKey: string | undefined;
45
+
46
+ function createTestServer(): Server {
47
+ return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
48
+ setRequestAuth(req, await resolveHttpRequestAuth(req, API_KEY));
49
+ res.setHeader("Content-Type", "application/json");
50
+ const pathSegments = getPathSegments(req.url || "");
51
+ const queryParams = parseQueryParams(req.url || "");
52
+ const myAgentId = req.headers["x-agent-id"] as string | undefined;
53
+ const handled = await handleApps(req, res, pathSegments, queryParams, myAgentId);
54
+ if (!handled) {
55
+ res.writeHead(404);
56
+ res.end(JSON.stringify({ error: "not found" }));
57
+ }
58
+ });
59
+ }
60
+
61
+ /** Runs an action as an agent (default: the worker) or, with `bearer`, as a token holder. */
62
+ async function runAction<T>(
63
+ appId: string,
64
+ body: Record<string, unknown>,
65
+ options: { agentId?: string; bearer?: string; action?: string } = {},
66
+ ): Promise<{ status: number; body: T }> {
67
+ const action = options.action ?? TASK_ACTION;
68
+ const response = await fetch(`${base}/api/apps/${appId}/actions/${action}`, {
69
+ method: "POST",
70
+ headers: {
71
+ "Content-Type": "application/json",
72
+ ...(options.bearer
73
+ ? { Authorization: `Bearer ${options.bearer}` }
74
+ : { "X-Agent-ID": options.agentId ?? AGENT_ID }),
75
+ },
76
+ body: JSON.stringify(body),
77
+ });
78
+ return { status: response.status, body: (await response.json()) as T };
79
+ }
80
+
81
+ /** A page whose Button chains a task-kind action under the chain scope's leaf task. */
82
+ function chainedButtonPage() {
83
+ return {
84
+ root: "root",
85
+ elements: {
86
+ root: { type: "Container", props: {}, children: ["send"] },
87
+ send: {
88
+ type: "Button",
89
+ props: { label: "Send" },
90
+ on: {
91
+ press: [
92
+ {
93
+ action: "app.action",
94
+ params: {
95
+ name: TASK_ACTION,
96
+ input: { prompt: "next step" },
97
+ parentTaskId: { $chain: "leafTaskId" },
98
+ },
99
+ },
100
+ ],
101
+ },
102
+ },
103
+ },
104
+ };
105
+ }
106
+
107
+ function definitionWith(page: Record<string, unknown>) {
108
+ return {
109
+ models: {
110
+ note: {
111
+ columns: { title: { kind: "string" } },
112
+ // The sync action needs one (model x source) pair to be valid.
113
+ sources: { tracker: { connector: "swarm-tasks", joinKey: "title" } },
114
+ },
115
+ },
116
+ queries: { allNotes: { model: "note" } },
117
+ actions: {
118
+ [TASK_ACTION]: { kind: "task", prompt: "Generate the next screen" },
119
+ // A non-task action, to show parentTaskId is rejected rather than dropped.
120
+ [SYNC_ACTION]: { kind: "sync" },
121
+ },
122
+ pages: { main: page },
123
+ defaultPage: "main",
124
+ };
125
+ }
126
+
127
+ /** Stores the chained-Button app the way the API does: validated first. */
128
+ async function createChainedApp(): Promise<string> {
129
+ const parsed = await parseAppDefinition(definitionWith(chainedButtonPage()));
130
+ if (!parsed.success) throw new Error(JSON.stringify(parsed.issues));
131
+ const app = await createApp({
132
+ name: `Chained ${crypto.randomUUID()}`,
133
+ definition: parsed.definition,
134
+ });
135
+ return app.id;
136
+ }
137
+
138
+ beforeAll(async () => {
139
+ for (const suffix of ["", "-wal", "-shm"]) {
140
+ try {
141
+ await unlink(`${TEST_DB_PATH}${suffix}`);
142
+ } catch {}
143
+ }
144
+ // Safe to mutate: CI runs one process per test file (`bun test --parallel=4`)
145
+ // and afterAll restores the previous value.
146
+ previousApiKey = process.env.AGENT_SWARM_API_KEY;
147
+ process.env.AGENT_SWARM_API_KEY = API_KEY;
148
+ initDb(TEST_DB_PATH);
149
+ const user = await createUser({ name: "Parent Task User" });
150
+ userToken = (await mintToken(user.id, "apps-action-parent-task", OPERATOR_ACTOR)).plaintext;
151
+ await createAgent({ id: AGENT_ID, name: "parent-task-worker", isLead: false, status: "idle" });
152
+ await createAgent({ id: LEAD_ID, name: "parent-task-lead", isLead: true, status: "idle" });
153
+ await createAgent({
154
+ id: OTHER_AGENT_ID,
155
+ name: "parent-task-other",
156
+ isLead: false,
157
+ status: "idle",
158
+ });
159
+ server = createTestServer();
160
+ const port = await listenOnFreePort(server, "127.0.0.1");
161
+ base = `http://127.0.0.1:${port}`;
162
+ });
163
+
164
+ afterAll(async () => {
165
+ await new Promise<void>((resolve) => server.close(() => resolve()));
166
+ closeDb();
167
+ if (previousApiKey === undefined) delete process.env.AGENT_SWARM_API_KEY;
168
+ else process.env.AGENT_SWARM_API_KEY = previousApiKey;
169
+ for (const suffix of ["", "-wal", "-shm"]) {
170
+ try {
171
+ await unlink(`${TEST_DB_PATH}${suffix}`);
172
+ } catch {}
173
+ }
174
+ });
175
+
176
+ describe("$chain sentinel in the page validator", () => {
177
+ test("accepts parentTaskId: { $chain } inside an app.action step's params", async () => {
178
+ const parsed = await parseAppDefinition(definitionWith(chainedButtonPage()));
179
+ expect(parsed.success).toBe(true);
180
+ });
181
+
182
+ test("accepts a $chain sentinel nested deeper inside app.action params", async () => {
183
+ const page = chainedButtonPage();
184
+ page.elements.send.on.press[0]!.params = {
185
+ name: TASK_ACTION,
186
+ input: { context: { parentTaskId: { $chain: "leafTaskId" } } },
187
+ };
188
+ const parsed = await parseAppDefinition(definitionWith(page));
189
+ expect(parsed.success).toBe(true);
190
+ });
191
+
192
+ test("rejects a $chain sentinel used as a plain prop value", async () => {
193
+ const page = chainedButtonPage();
194
+ page.elements = {
195
+ ...page.elements,
196
+ echo: { type: "Text", props: { content: { $chain: "leafTaskId" } } },
197
+ } as typeof page.elements;
198
+ page.elements.root.children = ["send", "echo"];
199
+ const parsed = await parseAppDefinition(definitionWith(page));
200
+ expect(parsed.success).toBe(false);
201
+ if (parsed.success) return;
202
+ expect(parsed.issues.some((entry) => entry.message === CHAIN_ISSUE)).toBe(true);
203
+ });
204
+ });
205
+
206
+ describe("POST /api/apps/:id/actions/:name with parentTaskId", () => {
207
+ test("chains the created task under the given parent", async () => {
208
+ const appId = await createChainedApp();
209
+ const parent = await createTask(AGENT_ID, "Previous turn of the chain");
210
+
211
+ const started = await runAction<{ ok: boolean; taskId: string; status: string }>(appId, {
212
+ input: { prompt: "next step" },
213
+ parentTaskId: parent.id,
214
+ });
215
+ expect(started.status).toBe(200);
216
+ expect(started.body).toMatchObject({ ok: true, status: "pending" });
217
+
218
+ const created = await getTaskById(started.body.taskId);
219
+ expect(created?.parentTaskId).toBe(parent.id);
220
+ });
221
+
222
+ test("answers 404 for an unknown parent instead of storing a dangling id", async () => {
223
+ const appId = await createChainedApp();
224
+ const missing = await runAction<{ error: string }>(appId, {
225
+ input: {},
226
+ parentTaskId: crypto.randomUUID(),
227
+ });
228
+ expect(missing.status).toBe(404);
229
+ expect(missing.body.error).toBe("parent task not found");
230
+ });
231
+
232
+ test("denies an agent caller without context for the parent, as POST /api/tasks does", async () => {
233
+ const appId = await createChainedApp();
234
+ const foreign = await createTask(OTHER_AGENT_ID, "Someone else's thread");
235
+
236
+ const denied = await runAction<{ error: string }>(appId, {
237
+ input: {},
238
+ parentTaskId: foreign.id,
239
+ });
240
+ expect(denied.status).toBe(403);
241
+ expect(denied.body.error).toBe(`You don't have context for parent task ${foreign.id}.`);
242
+
243
+ // A lead keeps its `task.parent.attach` grant: the gate is about identity, not the route.
244
+ const asLead = await runAction<{ taskId: string }>(
245
+ appId,
246
+ { input: {}, parentTaskId: foreign.id },
247
+ { agentId: LEAD_ID },
248
+ );
249
+ expect(asLead.status).toBe(200);
250
+ expect((await getTaskById(asLead.body.taskId))?.parentTaskId).toBe(foreign.id);
251
+ });
252
+
253
+ test("denies a user caller without a write share on the parent's session", async () => {
254
+ const appId = await createChainedApp();
255
+ const owner = await createUser({ name: "Session Owner" });
256
+ const theirs = await createTaskExtended("Someone else's session", {
257
+ agentId: OTHER_AGENT_ID,
258
+ requestedByUserId: owner.id,
259
+ });
260
+
261
+ const denied = await runAction<{ error: string }>(
262
+ appId,
263
+ { input: {}, parentTaskId: theirs.id },
264
+ { bearer: userToken },
265
+ );
266
+ expect(denied.status).toBe(403);
267
+ expect(denied.body.error).toBe(SESSION_WRITE_DENIED);
268
+ });
269
+
270
+ test("rejects parentTaskId on a non-task action instead of dropping it", async () => {
271
+ const appId = await createChainedApp();
272
+ const parent = await createTask(AGENT_ID, "Previous turn of the chain");
273
+
274
+ const rejected = await runAction<{ error: string }>(
275
+ appId,
276
+ { input: {}, parentTaskId: parent.id },
277
+ { action: SYNC_ACTION },
278
+ );
279
+ expect(rejected.status).toBe(400);
280
+ expect(rejected.body.error).toBe("parentTaskId is only supported for task-kind actions");
281
+ });
282
+
283
+ test("rejects an empty parentTaskId", async () => {
284
+ const appId = await createChainedApp();
285
+ const rejected = await runAction<{ error: string }>(appId, { input: {}, parentTaskId: "" });
286
+ expect(rejected.status).toBe(400);
287
+ });
288
+
289
+ test("still creates a parentless task when parentTaskId is omitted", async () => {
290
+ const appId = await createChainedApp();
291
+ const started = await runAction<{ ok: boolean; taskId: string }>(appId, { input: {} });
292
+ expect(started.status).toBe(200);
293
+ expect((await getTaskById(started.body.taskId))?.parentTaskId ?? null).toBeNull();
294
+ });
295
+ });
@@ -0,0 +1,197 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { parseAppDefinition } from "../apps/definition";
3
+
4
+ /**
5
+ * Validator coverage for `CardGrid` and the Table column `format` template.
6
+ * Pure `parseAppDefinition` calls — no DB, no HTTP — mirroring
7
+ * `apps-catalog-additions.test.ts`.
8
+ */
9
+
10
+ type Element = { type: string; props?: Record<string, unknown>; children?: string[] };
11
+
12
+ function appWith(elements: Record<string, Element>) {
13
+ return {
14
+ models: {
15
+ doc: {
16
+ columns: {
17
+ title: { kind: "string", required: true },
18
+ author: { kind: "string" },
19
+ cover: { kind: "string" },
20
+ status: { kind: "enum", enum: ["draft", "published"], default: "draft" },
21
+ },
22
+ },
23
+ },
24
+ queries: {
25
+ docs: { model: "doc" },
26
+ docById: { model: "doc", filter: { id: { $param: "doc" } } },
27
+ },
28
+ pages: {
29
+ main: { title: "Main", root: "root", elements },
30
+ detail: {
31
+ title: "Detail",
32
+ params: { doc: { kind: "string" }, position: { kind: "number" } },
33
+ root: "detailRoot",
34
+ elements: { detailRoot: { type: "Text", props: { content: "Detail" } } },
35
+ },
36
+ },
37
+ defaultPage: "main",
38
+ };
39
+ }
40
+
41
+ async function issuesFor(input: unknown) {
42
+ const parsed = await parseAppDefinition(input);
43
+ return parsed.success ? [] : parsed.issues;
44
+ }
45
+
46
+ const el = (id: string) => `pages.main.elements.${id}`;
47
+
48
+ function cardGrid(props: Record<string, unknown>) {
49
+ return appWith({
50
+ root: { type: "Stack", props: {}, children: ["cards"] },
51
+ cards: {
52
+ type: "CardGrid",
53
+ props: {
54
+ data: { $state: "/queries/docs/data" },
55
+ loading: { $state: "/queries/docs/loading" },
56
+ error: { $state: "/queries/docs/error" },
57
+ titleKey: "title",
58
+ ...props,
59
+ },
60
+ },
61
+ });
62
+ }
63
+
64
+ const navigateToDetail = [
65
+ { action: "app.navigate", params: { page: "detail", params: { doc: { $row: "id" } } } },
66
+ ];
67
+
68
+ describe("CardGrid", () => {
69
+ test("onClick running app.navigate to a declared page with $row params passes", async () => {
70
+ const issues = await issuesFor(
71
+ cardGrid({
72
+ imageKey: "cover",
73
+ fields: [
74
+ { key: "author" },
75
+ { key: "createdAt", kind: "date" },
76
+ { key: "status", kind: "badge", tones: { published: "success" } },
77
+ ],
78
+ columns: { base: 1, md: 2, lg: 4 },
79
+ emptyMessage: "No documents yet",
80
+ onClick: navigateToDetail,
81
+ }),
82
+ );
83
+ if (issues.length > 0) console.error(issues);
84
+ expect(issues).toEqual([]);
85
+ });
86
+
87
+ test("$row and $rowIndex are accepted inside the onClick chain", async () => {
88
+ const issues = await issuesFor(
89
+ cardGrid({
90
+ onClick: [
91
+ { action: "app.refresh", params: { query: "docs" } },
92
+ {
93
+ action: "app.navigate",
94
+ params: {
95
+ page: "detail",
96
+ params: { doc: { $row: "id" }, position: { $rowIndex: true } },
97
+ },
98
+ },
99
+ ],
100
+ }),
101
+ );
102
+ if (issues.length > 0) console.error(issues);
103
+ expect(issues).toEqual([]);
104
+ });
105
+
106
+ test("$row outside the onClick chain is rejected", async () => {
107
+ const issues = await issuesFor(cardGrid({ emptyMessage: { $row: "title" } }));
108
+ expect(issues).toContainEqual({
109
+ path: `${el("cards")}.props.emptyMessage`,
110
+ message: "$row, $rowIndex, $form, and $chain bindings are only allowed in action params",
111
+ });
112
+ });
113
+
114
+ test("onClick naming an unknown action fails", async () => {
115
+ const issues = await issuesFor(cardGrid({ onClick: [{ action: "app.nope", params: {} }] }));
116
+ expect(issues).toContainEqual({
117
+ path: `${el("cards")}.props.onClick.0.action`,
118
+ message: 'unknown action type "app.nope"',
119
+ });
120
+ });
121
+
122
+ test("onClick navigating to an undeclared page fails", async () => {
123
+ const issues = await issuesFor(
124
+ cardGrid({ onClick: [{ action: "app.navigate", params: { page: "nope" } }] }),
125
+ );
126
+ expect(issues).toContainEqual({
127
+ path: `${el("cards")}.props.onClick.0.params.page`,
128
+ message: 'unknown page "nope"',
129
+ });
130
+ });
131
+
132
+ test("titleKey is required", async () => {
133
+ const copy = cardGrid({});
134
+ delete copy.pages.main.elements.cards!.props!.titleKey;
135
+ const issues = await issuesFor(copy);
136
+ expect(issues.some((i) => i.path.startsWith(`${el("cards")}.props`))).toBe(true);
137
+ });
138
+
139
+ test("data bound to a $state path passes", async () => {
140
+ const issues = await issuesFor(
141
+ appWith({
142
+ root: { type: "Stack", props: {}, children: ["cards"] },
143
+ cards: {
144
+ type: "CardGrid",
145
+ props: { data: { $state: "/queries/docs/data" }, titleKey: "title" },
146
+ },
147
+ }),
148
+ );
149
+ if (issues.length > 0) console.error(issues);
150
+ expect(issues).toEqual([]);
151
+ });
152
+
153
+ test("an unknown field kind is rejected", async () => {
154
+ const issues = await issuesFor(cardGrid({ fields: [{ key: "author", kind: "status" }] }));
155
+ expect(issues.some((i) => i.path.startsWith(`${el("cards")}.props.fields.0.kind`))).toBe(true);
156
+ });
157
+ });
158
+
159
+ describe("Table column format", () => {
160
+ test("a column with a {{field}} format template passes", async () => {
161
+ const issues = await issuesFor(
162
+ appWith({
163
+ root: { type: "Stack", props: {}, children: ["table"] },
164
+ table: {
165
+ type: "Table",
166
+ props: {
167
+ data: { $state: "/queries/docs/data" },
168
+ columns: [
169
+ { key: "title", label: "Document", format: "{{title}} ({{status}})" },
170
+ { key: "author" },
171
+ ],
172
+ },
173
+ },
174
+ }),
175
+ );
176
+ if (issues.length > 0) console.error(issues);
177
+ expect(issues).toEqual([]);
178
+ });
179
+
180
+ test("format must be a string", async () => {
181
+ const issues = await issuesFor(
182
+ appWith({
183
+ root: { type: "Stack", props: {}, children: ["table"] },
184
+ table: {
185
+ type: "Table",
186
+ props: {
187
+ data: { $state: "/queries/docs/data" },
188
+ columns: [{ key: "title", format: 42 }],
189
+ },
190
+ },
191
+ }),
192
+ );
193
+ expect(issues.some((i) => i.path.startsWith(`${el("table")}.props.columns.0.format`))).toBe(
194
+ true,
195
+ );
196
+ });
197
+ });
@@ -0,0 +1,120 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { parseAppDefinition } from "../apps/definition";
3
+
4
+ /**
5
+ * Validator coverage for the small catalog additions: `NumberInput`, Select
6
+ * `defaultValue`, and `fill` on Stack / Split. Pure `parseAppDefinition`
7
+ * calls — no DB, no HTTP — mirroring `apps-catalog-inputs.test.ts`.
8
+ */
9
+
10
+ type Element = { type: string; props?: Record<string, unknown>; children?: string[] };
11
+
12
+ function appWith(elements: Record<string, Element>) {
13
+ return {
14
+ models: {},
15
+ pages: { main: { title: "Main", root: "root", elements } },
16
+ defaultPage: "main",
17
+ };
18
+ }
19
+
20
+ async function issuesFor(input: unknown) {
21
+ const parsed = await parseAppDefinition(input);
22
+ return parsed.success ? [] : parsed.issues;
23
+ }
24
+
25
+ const el = (id: string) => `pages.main.elements.${id}`;
26
+
27
+ describe("NumberInput", () => {
28
+ test("a NumberInput's /ui/<id>/value read by a bound Text validates", async () => {
29
+ const issues = await issuesFor(
30
+ appWith({
31
+ root: { type: "Stack", props: {}, children: ["width", "echo"] },
32
+ width: {
33
+ type: "NumberInput",
34
+ props: { id: "x", label: "Width", placeholder: "px", min: 1, max: 4096 },
35
+ },
36
+ echo: { type: "Text", props: { content: { $state: "/ui/x/value" } } },
37
+ }),
38
+ );
39
+ if (issues.length > 0) console.error(issues);
40
+ expect(issues).toEqual([]);
41
+ });
42
+
43
+ test("a /ui/<id>/value read with no UI control of that id is rejected", async () => {
44
+ const issues = await issuesFor(
45
+ appWith({
46
+ root: { type: "Stack", props: {}, children: ["echo"] },
47
+ echo: { type: "Text", props: { content: { $state: "/ui/x/value" } } },
48
+ }),
49
+ );
50
+ expect(issues).toContainEqual({
51
+ path: `${el("echo")}.props.content`,
52
+ message: 'state reference targets unknown UI control "x"',
53
+ });
54
+ });
55
+
56
+ test("id is required and min/max must be numbers", async () => {
57
+ const missingId = await issuesFor(
58
+ appWith({
59
+ root: { type: "Stack", props: {}, children: ["width"] },
60
+ width: { type: "NumberInput", props: { label: "Width" } },
61
+ }),
62
+ );
63
+ expect(missingId.map((issue) => issue.path)).toContain(`${el("width")}.props.id`);
64
+
65
+ const badBounds = await issuesFor(
66
+ appWith({
67
+ root: { type: "Stack", props: {}, children: ["width"] },
68
+ width: { type: "NumberInput", props: { id: "x", min: "1", max: "10" } },
69
+ }),
70
+ );
71
+ const paths = badBounds.map((issue) => issue.path);
72
+ expect(paths).toContain(`${el("width")}.props.min`);
73
+ expect(paths).toContain(`${el("width")}.props.max`);
74
+ });
75
+ });
76
+
77
+ describe("Select defaultValue", () => {
78
+ const select = (props: Record<string, unknown>) =>
79
+ appWith({
80
+ root: { type: "Stack", props: {}, children: ["device", "echo"] },
81
+ device: {
82
+ type: "Select",
83
+ props: { id: "device", options: ["phone", "tablet", "desktop"], ...props },
84
+ },
85
+ echo: { type: "Text", props: { content: { $state: "/ui/device/value" } } },
86
+ });
87
+
88
+ test("a Select with defaultValue validates", async () => {
89
+ const issues = await issuesFor(select({ defaultValue: "tablet" }));
90
+ if (issues.length > 0) console.error(issues);
91
+ expect(issues).toEqual([]);
92
+ });
93
+
94
+ test("defaultValue must be a string", async () => {
95
+ const paths = (await issuesFor(select({ defaultValue: 2 }))).map((issue) => issue.path);
96
+ expect(paths).toContain(`${el("device")}.props.defaultValue`);
97
+ });
98
+ });
99
+
100
+ describe("Stack and Split fill", () => {
101
+ const layout = (stackFill: unknown, splitFill: unknown) =>
102
+ appWith({
103
+ root: { type: "Stack", props: { fill: stackFill }, children: ["split"] },
104
+ split: { type: "Split", props: { fill: splitFill, ratio: "1-1" }, children: ["a", "b"] },
105
+ a: { type: "Text", props: { content: "left" } },
106
+ b: { type: "Text", props: { content: "right" } },
107
+ });
108
+
109
+ test("fill: true validates on both", async () => {
110
+ const issues = await issuesFor(layout(true, true));
111
+ if (issues.length > 0) console.error(issues);
112
+ expect(issues).toEqual([]);
113
+ });
114
+
115
+ test("fill must be a boolean", async () => {
116
+ const paths = (await issuesFor(layout("yes", 1))).map((issue) => issue.path);
117
+ expect(paths).toContain(`${el("root")}.props.fill`);
118
+ expect(paths).toContain(`${el("split")}.props.fill`);
119
+ });
120
+ });