@desplega.ai/agent-swarm 2.7.0 → 2.7.1

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 (236) hide show
  1. package/dist/{acp-adapter-ma1js5n7.js → acp-adapter-8dn5tq56.js} +6 -6
  2. package/dist/{actions-1vhmtvg9.js → actions-3r6gw7dy.js} +18 -18
  3. package/dist/{amp-adapter-sxqqvmdk.js → amp-adapter-wjtq413p.js} +3 -3
  4. package/dist/{anthropic-messages-txs190dd.js → anthropic-messages-n4cby1rz.js} +4 -4
  5. package/dist/{app-mwx1d5a9.js → app-d53yhykk.js} +3 -3
  6. package/dist/{assistant-yx1p6g82.js → assistant-ksrawj3k.js} +20 -20
  7. package/dist/{boot-reembed-7h78vmvv.js → boot-reembed-fxfw17m2.js} +10 -3
  8. package/dist/{boot-reembed-5h37a2c2.js → boot-reembed-k5y5xbj0.js} +15 -8
  9. package/dist/{boot-scrub-sweep-8mawd4gw.js → boot-scrub-sweep-se41j9fd.js} +3 -3
  10. package/dist/{boot-seeds-ca84dhnn.js → boot-seeds-r3kwwv8b.js} +4 -4
  11. package/dist/{claude-adapter-wqt4hfkq.js → claude-adapter-ahmyf231.js} +7 -7
  12. package/dist/{claude-managed-adapter-6fjqtr2z.js → claude-managed-adapter-5knamegc.js} +3 -3
  13. package/dist/{claude-managed-setup-rqv98sea.js → claude-managed-setup-mdc7dy6b.js} +2 -2
  14. package/dist/{claude-sdk-session-f29f3b2x.js → claude-sdk-session-n5wen9zk.js} +8728 -8599
  15. package/dist/{cli-m5e802r7.js → cli-0dqtv2jg.js} +2 -2
  16. package/dist/{cli-e1595bgx.js → cli-0tww7dnz.js} +2 -2
  17. package/dist/{cli-2y7v133v.js → cli-0w4e4v58.js} +7 -7
  18. package/dist/{cli-d20qvpr2.js → cli-2sjm3t4r.js} +1 -1
  19. package/dist/{cli-gf2850q7.js → cli-3cywb44q.js} +18 -1
  20. package/dist/{cli-8wkb09q5.js → cli-3k9jej0c.js} +2 -2
  21. package/dist/{cli-twctnkng.js → cli-3v2cyqb8.js} +3 -3
  22. package/dist/{cli-he9gbdx0.js → cli-5qt9vzmq.js} +39 -20
  23. package/dist/{cli-a6ds2epe.js → cli-5rp1w4cr.js} +1 -1
  24. package/dist/{cli-6srv653c.js → cli-6vwjsetx.js} +4 -4
  25. package/dist/{cli-1hhbzkbb.js → cli-6w2yrrc0.js} +8 -8
  26. package/dist/{cli-f12mr20z.js → cli-75tdydgv.js} +1 -1
  27. package/dist/{cli-j3y0x2n4.js → cli-8feswf2v.js} +43 -33
  28. package/dist/{cli-07q03xm2.js → cli-8wd3mbyj.js} +1 -1
  29. package/dist/{cli-ymjpkxgn.js → cli-9csfthbw.js} +8 -6
  30. package/dist/{cli-f1e0ptjj.js → cli-b41p764a.js} +21 -2
  31. package/dist/{cli-kv7sbc2c.js → cli-c1t533hh.js} +3 -3
  32. package/dist/{cli-54sj64fc.js → cli-c28jxpcv.js} +1 -1
  33. package/dist/{cli-8mbc9za9.js → cli-cb50g9cd.js} +1 -1
  34. package/dist/{cli-c668vvcf.js → cli-d8gaphtr.js} +1 -1
  35. package/dist/{cli-86ehn4s5.js → cli-dgtfysne.js} +2 -2
  36. package/dist/{cli-sjctjr48.js → cli-dscnhn18.js} +1 -1
  37. package/dist/{cli-j0ppywdr.js → cli-dv11pgpv.js} +12 -12
  38. package/dist/{cli-1m9x8dr5.js → cli-ekptgk2m.js} +5 -5
  39. package/dist/{cli-9gsm2rzp.js → cli-f3mzy662.js} +17 -17
  40. package/dist/{cli-s0mgx214.js → cli-fk7k8s9x.js} +2 -2
  41. package/dist/{cli-vbhgkw9h.js → cli-h3e7bhvn.js} +3 -3
  42. package/dist/{cli-yd2nreqw.js → cli-hkxqdmnb.js} +2 -2
  43. package/dist/{cli-8wmj18cz.js → cli-hpkvgdad.js} +7 -4
  44. package/dist/{cli-wqr7cea4.js → cli-hq2v4rr5.js} +37 -9
  45. package/dist/{cli-sgqm2582.js → cli-k1zb7raw.js} +5 -5
  46. package/dist/{cli-4rvr7zfx.js → cli-k4awgvzm.js} +5 -0
  47. package/dist/{cli-ay40dnj3.js → cli-k7d3hvya.js} +2 -2
  48. package/dist/{cli-j9bpmykh.js → cli-kbtdftbw.js} +5 -5
  49. package/dist/{cli-x37r7qwf.js → cli-kj69b9tw.js} +25 -5
  50. package/dist/{cli-gsfk58nw.js → cli-kk9jy4nm.js} +1 -1
  51. package/dist/{cli-39aet655.js → cli-m52t4cta.js} +1 -1
  52. package/dist/{cli-h5jeg46p.js → cli-mdadewqp.js} +5 -5
  53. package/dist/{cli-a89zdqz9.js → cli-nf6k60sn.js} +1 -1
  54. package/dist/{cli-v6shg7na.js → cli-nzdf09js.js} +1 -1
  55. package/dist/{cli-rr7zx19j.js → cli-prcyzecg.js} +12 -12
  56. package/dist/{cli-6a9qfa8b.js → cli-pzafyqtk.js} +118 -48
  57. package/dist/{cli-48gxjmcz.js → cli-qvaa1gjp.js} +87 -24
  58. package/dist/{cli-qj442fws.js → cli-rg4b2g6j.js} +1 -1
  59. package/dist/{cli-aypd09nf.js → cli-rm66vqdz.js} +1 -1
  60. package/dist/{cli-9pshx6rd.js → cli-s504c3w7.js} +1 -1
  61. package/dist/{cli-ptfq202x.js → cli-swkx9jm2.js} +3 -3
  62. package/dist/{cli-m30k9777.js → cli-thsaq4pd.js} +16 -16
  63. package/dist/{cli-60sty27p.js → cli-twjnyvs8.js} +2 -2
  64. package/dist/{cli-rd5m70gj.js → cli-typdpe8g.js} +4 -4
  65. package/dist/{cli-yxbsmx56.js → cli-vg8pwmsd.js} +6 -6
  66. package/dist/{cli-s09j9vp5.js → cli-vhpckzvp.js} +1 -1
  67. package/dist/{cli-dv97kdzv.js → cli-wdxgsdz5.js} +2 -2
  68. package/dist/{cli-7a6p4e65.js → cli-wtjg6je3.js} +1 -1
  69. package/dist/{cli-b3yzyc57.js → cli-x3cngb1h.js} +44 -12
  70. package/dist/{cli-6mgdba42.js → cli-xbz6mcg4.js} +2 -2
  71. package/dist/{cli-2pvzmas1.js → cli-y4tdxsqb.js} +3 -3
  72. package/dist/{cli-f4dbh7a5.js → cli-yqrsp3ny.js} +7 -7
  73. package/dist/cli.js +27 -19
  74. package/dist/{codex-adapter-zk59x2zh.js → codex-adapter-7wepzq15.js} +5 -5
  75. package/dist/{codex-hook-xyjhvxwd.js → codex-hook-pbdh4xhx.js} +2 -2
  76. package/dist/{codex-session-runner-kh6s4j3n.js → codex-session-runner-02sb49e4.js} +5 -5
  77. package/dist/{commands-68et1qv2.js → commands-3rcspxct.js} +2 -2
  78. package/dist/{controller-fyrbnzg4.js → controller-8ar4avd6.js} +3 -3
  79. package/dist/{controllers-connect-nep9h2xv.js → controllers-connect-ajvh9h0v.js} +19 -19
  80. package/dist/{cursor-adapter-yss3pg7t.js → cursor-adapter-gd0sa7bh.js} +1 -1
  81. package/dist/{db-28gjyvxj.js → db-dv9j7ts5.js} +2 -2
  82. package/dist/{db-8ftwvzba.js → db-z26vgdvj.js} +3 -3
  83. package/dist/{dispatcher-hayexmkr.js → dispatcher-8m0htjq8.js} +17 -17
  84. package/dist/{dsh-adapter-46wawff9.js → dsh-adapter-y8gyk130.js} +1 -1
  85. package/dist/{e2b-fn3zacvt.js → e2b-74vg9b7e.js} +1 -1
  86. package/dist/{e2b-222hsajc.js → e2b-jyay8wzn.js} +1 -1
  87. package/dist/{easter-egg-3d-8d1e0h4m.js → easter-egg-3d-28f880xx.js} +1 -1
  88. package/dist/{execute-cjrfxfnm.js → execute-9seb79vh.js} +1 -1
  89. package/dist/{external-channel-xbmxcg09.js → external-channel-smz7jyth.js} +2 -2
  90. package/dist/{getMachineId-bsd-brn0xym3.js → getMachineId-bsd-whfre6gr.js} +3 -3
  91. package/dist/{getMachineId-darwin-t339sfxm.js → getMachineId-darwin-5zjynb19.js} +3 -3
  92. package/dist/{getMachineId-linux-5eq95d8b.js → getMachineId-linux-8mksrrsq.js} +3 -3
  93. package/dist/{getMachineId-unsupported-qavrtc7j.js → getMachineId-unsupported-fxtqsdt2.js} +3 -3
  94. package/dist/{getMachineId-win-q7fe1wx7.js → getMachineId-win-nzfmz857.js} +3 -3
  95. package/dist/{grok-adapter-ag4wks2a.js → grok-adapter-z0nnvtbn.js} +7 -7
  96. package/dist/handlers-pspryh6g.js +41 -0
  97. package/dist/{heartbeat-psfvcgwp.js → heartbeat-f928aqyp.js} +20 -20
  98. package/dist/{hook-cyq4mryn.js → hook-d7c605r4.js} +7 -7
  99. package/dist/{hook-8pz04697.js → hook-q9hv5xf9.js} +5 -5
  100. package/dist/{http-haj1nd6k.js → http-q6j4k989.js} +934 -180
  101. package/dist/{index-z5h22pes.js → index-5nb266jr.js} +17 -17
  102. package/dist/{index-ysd45kgc.js → index-8fxt5eah.js} +10 -10
  103. package/dist/{index-zgvqzjsw.js → index-cekzwhez.js} +5 -5
  104. package/dist/{index-zyq8ct52.js → index-ch0c1xff.js} +17 -17
  105. package/dist/{index-syvhnvtw.js → index-ebnjhxh1.js} +1 -1
  106. package/dist/{index-y1ajx04a.js → index-ed8dy8rn.js} +20 -20
  107. package/dist/{index-jwt2bj6f.js → index-ejqdkkqx.js} +11 -10
  108. package/dist/{index-dr94zbnh.js → index-farm1zsn.js} +32661 -29443
  109. package/dist/{index-q52f47pe.js → index-yn1yxg85.js} +1 -1
  110. package/dist/{keepalive-xdvsamvj.js → keepalive-zzc3hks1.js} +3 -3
  111. package/dist/{lead-hqg57kdp.js → lead-c0pjcaw4.js} +36 -36
  112. package/dist/{maintenance-4y2xzh3a.js → maintenance-kqsxzpzx.js} +1 -1
  113. package/dist/{oauth-refresh-sweep-jd4r7zbs.js → oauth-refresh-sweep-7qt0g55z.js} +2 -2
  114. package/dist/{onboard-18447q9q.js → onboard-k7x13fqg.js} +118 -5
  115. package/dist/{opencode-adapter-26b2frdf.js → opencode-adapter-tqvrtter.js} +5 -5
  116. package/dist/{otel-impl-f9tgr8yw.js → otel-impl-sqkf6k3b.js} +97 -97
  117. package/dist/{pi-mono-adapter-qn6qbga1.js → pi-mono-adapter-m2qnafxm.js} +63 -9
  118. package/dist/{pricing-refresh-dw2tgb53.js → pricing-refresh-wkpwhgvs.js} +3 -3
  119. package/dist/{rbac-roles-s4nwj2m3.js → rbac-roles-dpn0c0wj.js} +7 -4
  120. package/dist/{rbac-roles-aygm2760.js → rbac-roles-wk7xd6a6.js} +2 -2
  121. package/dist/{render-v2-yfvdfr0x.js → render-v2-ex2m4g2k.js} +3 -3
  122. package/dist/{resume-1bmvjbf3.js → resume-725g9b0b.js} +7 -7
  123. package/dist/{runtime-f7tpzedy.js → runtime-kajas15g.js} +1 -1
  124. package/dist/{session-access-r5garej6.js → session-access-0j2ezjje.js} +8 -4
  125. package/dist/{session-source-backfill-424wzv0r.js → session-source-backfill-62202awe.js} +2 -2
  126. package/dist/{setup-8tygvxkq.js → setup-9sj11ax6.js} +1 -1
  127. package/dist/{virtual-modules-xsw323b3.js → virtual-modules-c6fcq6zr.js} +2 -2
  128. package/dist/{work-objects-ggnghpgf.js → work-objects-0y07495y.js} +20 -20
  129. package/dist/{worker-sqjd1nqf.js → worker-d4q9sqbc.js} +36 -36
  130. package/openapi.json +1070 -9
  131. package/package.json +3 -3
  132. package/src/be/connector-codes.ts +65 -22
  133. package/src/be/memory/boot-reembed.ts +4 -1
  134. package/src/be/memory/constants.ts +6 -0
  135. package/src/be/memory/embedding-recovery.ts +48 -0
  136. package/src/be/memory/index-content.ts +5 -2
  137. package/src/be/memory/providers/openai-embedding.ts +65 -14
  138. package/src/be/memory/task-learning.ts +110 -8
  139. package/src/be/memory/types.ts +13 -2
  140. package/src/be/migrations/246_connector_code_kind.sql +11 -0
  141. package/src/be/onboarding.ts +125 -15
  142. package/src/be/rbac-roles.ts +4 -1
  143. package/src/be/scripts/embeddings.ts +2 -2
  144. package/src/be/seed/agent-fs-provision.ts +75 -14
  145. package/src/be/swarm-config-guard.ts +3 -0
  146. package/src/cli.tsx +9 -0
  147. package/src/comb/config.ts +6 -1
  148. package/src/commands/onboard/agent-fs-version.ts +5 -0
  149. package/src/commands/onboard/compose-generator.ts +112 -1
  150. package/src/commands/onboard/env-generator.ts +9 -0
  151. package/src/commands/onboard/tei-image.ts +12 -0
  152. package/src/commands/onboard/types.ts +7 -0
  153. package/src/commands/onboard.tsx +11 -1
  154. package/src/commands/runner.ts +14 -1
  155. package/src/http/agentfs-proxy.ts +369 -0
  156. package/src/http/all-routes.ts +1 -0
  157. package/src/http/codex-oauth-device.ts +8 -1
  158. package/src/http/fs.ts +64 -10
  159. package/src/http/inbox-state.ts +27 -1
  160. package/src/http/index.ts +4 -1
  161. package/src/http/memory.ts +7 -4
  162. package/src/http/onboarding.ts +139 -11
  163. package/src/http/session-access.ts +24 -0
  164. package/src/http/sessions.ts +10 -6
  165. package/src/http/tasks.ts +85 -11
  166. package/src/http/telemetry-ui.ts +1 -1
  167. package/src/http/user-roles.ts +4 -1
  168. package/src/http/users.ts +272 -3
  169. package/src/http/utils.ts +22 -3
  170. package/src/prompts/base-prompt.ts +24 -9
  171. package/src/prompts/session-templates.ts +26 -2
  172. package/src/providers/amp-adapter.ts +1 -1
  173. package/src/providers/codex-adapter.ts +6 -1
  174. package/src/providers/cursor-adapter.ts +1 -1
  175. package/src/providers/pi-mono-adapter.ts +58 -3
  176. package/src/rbac/legacy-policy.ts +1 -0
  177. package/src/rbac/permissions.ts +4 -0
  178. package/src/tests/agent-fs-provision-seeder.test.ts +261 -92
  179. package/src/tests/agentfs-proxy.test.ts +1106 -0
  180. package/src/tests/base-prompt.test.ts +69 -2
  181. package/src/tests/codex-adapter.test.ts +46 -8
  182. package/src/tests/codex-oauth-device.test.ts +12 -1
  183. package/src/tests/comb-config-guard.test.ts +20 -0
  184. package/src/tests/comb-links.test.ts +79 -1
  185. package/src/tests/comb-status.test.ts +44 -0
  186. package/src/tests/docs-index-footprint.test.js +3 -3
  187. package/src/tests/fixtures/onboard/baseline-states.ts +68 -0
  188. package/src/tests/fixtures/onboard/content.compose.yml.txt +190 -0
  189. package/src/tests/fixtures/onboard/content.env.txt +29 -0
  190. package/src/tests/fixtures/onboard/dev.compose.yml.txt +155 -0
  191. package/src/tests/fixtures/onboard/dev.env.txt +28 -0
  192. package/src/tests/fixtures/onboard/full-all.compose.yml.txt +582 -0
  193. package/src/tests/fixtures/onboard/full-all.env.txt +59 -0
  194. package/src/tests/fixtures/onboard/full.compose.yml.txt +435 -0
  195. package/src/tests/fixtures/onboard/full.env.txt +36 -0
  196. package/src/tests/fixtures/onboard/research.compose.yml.txt +155 -0
  197. package/src/tests/fixtures/onboard/research.env.txt +28 -0
  198. package/src/tests/fixtures/onboard/solo.compose.yml.txt +85 -0
  199. package/src/tests/fixtures/onboard/solo.env.txt +26 -0
  200. package/src/tests/http-log-scrubbing.test.ts +34 -1
  201. package/src/tests/http-task-write-ownership.test.ts +5 -1
  202. package/src/tests/invites-http.test.ts +464 -0
  203. package/src/tests/memory-embedding-input-cap.test.ts +122 -1
  204. package/src/tests/memory-embedding-recovery.test.ts +137 -0
  205. package/src/tests/onboard-compose.test.ts +142 -0
  206. package/src/tests/onboard-env.test.ts +24 -0
  207. package/src/tests/onboard-non-interactive.test.ts +29 -0
  208. package/src/tests/onboard-tei-image.test.ts +36 -0
  209. package/src/tests/onboarding.test.ts +451 -4
  210. package/src/tests/openai-embedding-provider.test.ts +105 -2
  211. package/src/tests/pi-mono-session-cost-on-abort.test.ts +270 -0
  212. package/src/tests/preload-env.test.ts +4 -0
  213. package/src/tests/preload.ts +4 -0
  214. package/src/tests/prompt-template-session.test.ts +19 -3
  215. package/src/tests/rbac-admission-e2e.test.ts +22 -6
  216. package/src/tests/rbac-admission.test.ts +342 -4
  217. package/src/tests/rbac-engine.test.ts +1 -0
  218. package/src/tests/rbac-roles.test.ts +9 -1
  219. package/src/tests/send-task-requester-gate.test.ts +64 -3
  220. package/src/tests/sessions-move.test.ts +39 -1
  221. package/src/tests/store-progress-learning.test.ts +95 -2
  222. package/src/tests/whoami-route.test.ts +41 -0
  223. package/src/tools/inject-learning.ts +1 -1
  224. package/src/tools/memory-edit.ts +1 -1
  225. package/src/tools/memory-search.ts +1 -1
  226. package/src/tools/store-progress.ts +5 -1
  227. package/src/tools/utils.ts +12 -1
  228. package/src/utils/constants.ts +39 -0
  229. package/src/utils/task-attachment-links.ts +15 -3
  230. package/templates/skills/swarm-docs/content.md +1 -1
  231. package/dist/handlers-tzmevqb5.js +0 -41
  232. package/dist/{azure-openai-responses-9kfwhmn7.js → azure-openai-responses-43asa23s.js} +0 -0
  233. package/dist/{node-vxx896fa.js → node-t28cc56b.js} +0 -0
  234. package/dist/{openai-codex-responses-79s8xnbh.js → openai-codex-responses-jhr3sjtm.js} +3 -3
  235. package/dist/{openai-completions-tdbyts0x.js → openai-completions-gs6yn37k.js} +3 -3
  236. /package/dist/{openai-responses-dm3t5vqp.js → openai-responses-jqm9vwwk.js} +0 -0
@@ -4,10 +4,12 @@ import {
4
4
  closeDb,
5
5
  createAgent,
6
6
  createUser,
7
+ deleteSwarmConfigByKey,
7
8
  getSwarmConfigs,
8
9
  initDb,
9
10
  upsertSwarmConfig,
10
11
  } from "../be/db";
12
+ import { attachRole, detachRole, ensureRbacSeedsSynced } from "../be/rbac-roles";
11
13
  import { runSeeder } from "../be/seed";
12
14
  import {
13
15
  agentFsProvisionSeeder,
@@ -62,13 +64,38 @@ function restoreEnv(): void {
62
64
  }
63
65
  }
64
66
 
67
+ async function makeRbacViewer(userId: string): Promise<void> {
68
+ await attachRole(userId, "viewer");
69
+ await detachRole(userId, "admin");
70
+ }
71
+
72
+ async function seedSharedConfig(): Promise<void> {
73
+ await upsertSwarmConfig({
74
+ scope: "global",
75
+ key: "API_AGENT_FS_API_KEY",
76
+ value: "example-afs-admin-key",
77
+ isSecret: true,
78
+ });
79
+ await upsertSwarmConfig({ scope: "global", key: "AGENT_FS_DEFAULT_ORG_ID", value: "shared-org" });
80
+ await upsertSwarmConfig({
81
+ scope: "global",
82
+ key: "AGENT_FS_DEFAULT_DRIVE_ID",
83
+ value: "shared-drive",
84
+ });
85
+ }
86
+
65
87
  async function configValue(key: string): Promise<string | undefined> {
66
88
  return (await getSwarmConfigs({ scope: "global", key }))[0]?.value;
67
89
  }
68
90
 
69
91
  function createFetchStub(
70
92
  records: RequestRecord[],
71
- options: { members?: StubMember[]; registerConflictEmail?: string } = {},
93
+ options: {
94
+ members?: StubMember[];
95
+ registerConflictEmail?: string;
96
+ /** A body to send, or a function of the invited email (undefined: the default). */
97
+ inviteResponse?: Record<string, unknown> | ((email: string) => Response | undefined);
98
+ } = {},
72
99
  ): typeof fetch {
73
100
  return (async (input, init) => {
74
101
  const url = new URL(String(input));
@@ -135,7 +162,10 @@ function createFetchStub(
135
162
  return Response.json({ members: options.members ?? [] });
136
163
  }
137
164
  if (url.pathname === "/orgs/shared-org/members/invite" && method === "POST") {
138
- return Response.json({ ok: true });
165
+ const invite = options.inviteResponse;
166
+ if (typeof invite !== "function") return Response.json(invite ?? { ok: true });
167
+ const email = body && typeof body === "object" && "email" in body ? String(body.email) : "";
168
+ return invite(email) ?? Response.json({ ok: true });
139
169
  }
140
170
 
141
171
  return Response.json(
@@ -149,6 +179,7 @@ describe("agent-fs provisioning seeder", () => {
149
179
  beforeAll(async () => {
150
180
  await removeDbFiles(TEST_DB_PATH);
151
181
  initDb(TEST_DB_PATH);
182
+ ensureRbacSeedsSynced({ quiet: true });
152
183
  });
153
184
 
154
185
  afterEach(() => {
@@ -197,11 +228,12 @@ describe("agent-fs provisioning seeder", () => {
197
228
  email: `operator-${suffix}@example.test`,
198
229
  role: "Operator",
199
230
  });
200
- await createUser({
231
+ const viewer = await createUser({
201
232
  name: "Explicit Viewer User",
202
233
  email: `viewer-${suffix}@example.test`,
203
234
  role: "read-only contractor",
204
235
  });
236
+ await makeRbacViewer(viewer.id);
205
237
  const records: RequestRecord[] = [];
206
238
  setAgentFsProvisionFetchForTests(createFetchStub(records));
207
239
 
@@ -249,46 +281,35 @@ describe("agent-fs provisioning seeder", () => {
249
281
  expect(records).toEqual([]);
250
282
  });
251
283
 
252
- test("classifies arbitrary non-viewer human roles as agent-fs editors", async () => {
284
+ test("follows the RBAC grant, not the free-text profile role", async () => {
253
285
  const suffix = crypto.randomUUID().slice(0, 8);
254
286
  process.env.AGENT_FS_API_URL = "https://agent-fs.example.test/";
255
287
  process.env.AGENT_FS_REGISTER_EMAIL = "admin@example.test";
256
- await upsertSwarmConfig({
257
- scope: "global",
258
- key: "API_AGENT_FS_API_KEY",
259
- value: "example-afs-admin-key",
260
- isSecret: true,
261
- });
262
- await upsertSwarmConfig({
263
- scope: "global",
264
- key: "AGENT_FS_DEFAULT_ORG_ID",
265
- value: "shared-org",
266
- });
267
- await upsertSwarmConfig({
268
- scope: "global",
269
- key: "AGENT_FS_DEFAULT_DRIVE_ID",
270
- value: "shared-drive",
271
- });
288
+ await seedSharedConfig();
272
289
 
290
+ // Default trigger role is admin (grant-all): editor even with a "Viewer" profile role.
273
291
  await createUser({
274
- name: "Taras Founder",
275
- email: `taras-founder-${suffix}@example.test`,
276
- role: "co-founder, CTO",
292
+ name: "Admin With Viewer Title",
293
+ email: `admin-viewer-title-${suffix}@example.test`,
294
+ role: "Viewer",
277
295
  });
278
- await createUser({
279
- name: "Eze Founder",
280
- email: `eze-founder-${suffix}@example.test`,
281
- role: "co-founder, CEO",
296
+ const requester = await createUser({
297
+ name: "Requester Human",
298
+ email: `requester-${suffix}@example.test`,
299
+ role: "guest",
282
300
  });
283
- await createUser({
284
- name: "Designer Human",
285
- email: `designer-${suffix}@example.test`,
286
- role: "designer",
301
+ await attachRole(requester.id, "requester");
302
+ await detachRole(requester.id, "admin");
303
+ const viewer = await createUser({
304
+ name: "RBAC Viewer",
305
+ email: `rbac-viewer-${suffix}@example.test`,
306
+ role: "co-founder, CTO",
287
307
  });
308
+ await makeRbacViewer(viewer.id);
288
309
  await createUser({
289
- name: "Unknown Human",
290
- email: `whatever-${suffix}@example.test`,
291
- role: "whatever",
310
+ name: "Padded Email",
311
+ email: ` padded-${suffix}@example.test `,
312
+ role: "designer",
292
313
  });
293
314
 
294
315
  const records: RequestRecord[] = [];
@@ -301,59 +322,31 @@ describe("agent-fs provisioning seeder", () => {
301
322
  .filter((r) => r.path === "/orgs/shared-org/members/invite")
302
323
  .map((r) => r.body);
303
324
  expect(invites).toContainEqual({
304
- email: `taras-founder-${suffix}@example.test`,
305
- role: "editor",
306
- });
307
- expect(invites).toContainEqual({
308
- email: `eze-founder-${suffix}@example.test`,
309
- role: "editor",
310
- });
311
- expect(invites).toContainEqual({
312
- email: `designer-${suffix}@example.test`,
313
- role: "editor",
314
- });
315
- expect(invites).toContainEqual({
316
- email: `whatever-${suffix}@example.test`,
325
+ email: `admin-viewer-title-${suffix}@example.test`,
317
326
  role: "editor",
318
327
  });
328
+ expect(invites).toContainEqual({ email: `requester-${suffix}@example.test`, role: "editor" });
329
+ expect(invites).toContainEqual({ email: `rbac-viewer-${suffix}@example.test`, role: "viewer" });
330
+ expect(invites).toContainEqual({ email: `padded-${suffix}@example.test`, role: "editor" });
319
331
  });
320
332
 
321
- test("classifies explicitly viewer human roles as agent-fs viewers", async () => {
333
+ test("does not invite invited or suspended users", async () => {
322
334
  const suffix = crypto.randomUUID().slice(0, 8);
323
335
  process.env.AGENT_FS_API_URL = "https://agent-fs.example.test/";
324
336
  process.env.AGENT_FS_REGISTER_EMAIL = "admin@example.test";
325
- await upsertSwarmConfig({
326
- scope: "global",
327
- key: "API_AGENT_FS_API_KEY",
328
- value: "example-afs-admin-key",
329
- isSecret: true,
330
- });
331
- await upsertSwarmConfig({
332
- scope: "global",
333
- key: "AGENT_FS_DEFAULT_ORG_ID",
334
- value: "shared-org",
335
- });
336
- await upsertSwarmConfig({
337
- scope: "global",
338
- key: "AGENT_FS_DEFAULT_DRIVE_ID",
339
- value: "shared-drive",
340
- });
337
+ await seedSharedConfig();
341
338
 
342
339
  await createUser({
343
- name: "Viewer Human",
344
- email: `viewer-${suffix}@example.test`,
345
- role: "Viewer",
346
- });
347
- await createUser({
348
- name: "Read Only Human",
349
- email: `read-only-${suffix}@example.test`,
350
- role: "read only",
340
+ name: "Pending",
341
+ email: `pending-${suffix}@example.test`,
342
+ status: "invited",
351
343
  });
352
344
  await createUser({
353
- name: "Guest Human",
354
- email: `guest-${suffix}@example.test`,
355
- role: "guest",
345
+ name: "Suspended",
346
+ email: `suspended-${suffix}@example.test`,
347
+ status: "suspended",
356
348
  });
349
+ await createUser({ name: "Active", email: `active-${suffix}@example.test` });
357
350
 
358
351
  const records: RequestRecord[] = [];
359
352
  setAgentFsProvisionFetchForTests(createFetchStub(records));
@@ -361,21 +354,106 @@ describe("agent-fs provisioning seeder", () => {
361
354
  const result = await runSeeder(agentFsProvisionSeeder, { quiet: true });
362
355
 
363
356
  expect(result.failed).toEqual([]);
364
- const invites = records
357
+ const invited = records
365
358
  .filter((r) => r.path === "/orgs/shared-org/members/invite")
366
- .map((r) => r.body);
367
- expect(invites).toContainEqual({
368
- email: `viewer-${suffix}@example.test`,
369
- role: "viewer",
370
- });
371
- expect(invites).toContainEqual({
372
- email: `read-only-${suffix}@example.test`,
373
- role: "viewer",
374
- });
375
- expect(invites).toContainEqual({
376
- email: `guest-${suffix}@example.test`,
377
- role: "viewer",
378
- });
359
+ .map((r) => (r.body as { email: string }).email);
360
+ expect(invited).toContain(`active-${suffix}@example.test`);
361
+ expect(invited).not.toContain(`pending-${suffix}@example.test`);
362
+ expect(invited).not.toContain(`suspended-${suffix}@example.test`);
363
+ });
364
+
365
+ test("a transient invite failure keeps org and drive rows and retries on the next boot", async () => {
366
+ const suffix = crypto.randomUUID().slice(0, 8);
367
+ const flaky = `flaky-${suffix}@example.test`;
368
+ process.env.AGENT_FS_API_URL = "https://agent-fs.example.test/";
369
+ process.env.AGENT_FS_REGISTER_EMAIL = "admin@example.test";
370
+ delete process.env.AGENT_FS_DEFAULT_ORG_ID;
371
+ delete process.env.AGENT_FS_SHARED_ORG_ID;
372
+ delete process.env.AGENT_FS_DEFAULT_DRIVE_ID;
373
+ // Fresh-install shape: no hash and no org or drive rows yet.
374
+ for (const key of [
375
+ "AGENT_FS_PROVISION_HASH",
376
+ "AGENT_FS_DEFAULT_ORG_ID",
377
+ "AGENT_FS_SHARED_ORG_ID",
378
+ "AGENT_FS_DEFAULT_DRIVE_ID",
379
+ ]) {
380
+ await deleteSwarmConfigByKey("global", null, key);
381
+ }
382
+ await createUser({ name: "Flaky", email: flaky });
383
+
384
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
385
+ try {
386
+ const records: RequestRecord[] = [];
387
+ setAgentFsProvisionFetchForTests(
388
+ createFetchStub(records, {
389
+ inviteResponse: (email) =>
390
+ email === flaky ? Response.json({ error: "busy" }, { status: 503 }) : undefined,
391
+ }),
392
+ );
393
+
394
+ const failed = await runSeeder(agentFsProvisionSeeder, { quiet: true });
395
+
396
+ expect(failed.failed).toHaveLength(1);
397
+ expect(failed.failed[0]?.error).toContain("will retry on next boot");
398
+ expect(await configValue("AGENT_FS_DEFAULT_ORG_ID")).toBe("shared-org");
399
+ expect(await configValue("AGENT_FS_SHARED_ORG_ID")).toBe("shared-org");
400
+ expect(await configValue("AGENT_FS_DEFAULT_DRIVE_ID")).toBe("shared-drive");
401
+ const [item] = await agentFsProvisionSeeder.items();
402
+ expect(await configValue("AGENT_FS_PROVISION_HASH")).not.toBe(item?.contentHash);
403
+
404
+ records.length = 0;
405
+ setAgentFsProvisionFetchForTests(createFetchStub(records));
406
+ const retried = await runSeeder(agentFsProvisionSeeder, { quiet: true });
407
+
408
+ expect(retried.failed).toEqual([]);
409
+ expect(
410
+ records.filter((r) => r.path === "/orgs/shared-org/members/invite").map((r) => r.body),
411
+ ).toContainEqual({ email: flaky, role: "editor" });
412
+ expect(await configValue("AGENT_FS_PROVISION_HASH")).toBe(item?.contentHash);
413
+
414
+ records.length = 0;
415
+ const unchanged = await runSeeder(agentFsProvisionSeeder, { quiet: true });
416
+ expect(unchanged.failed).toEqual([]);
417
+ expect(unchanged.skippedUnchanged).toBe(1);
418
+ expect(records).toEqual([]);
419
+ } finally {
420
+ warn.mockRestore();
421
+ }
422
+ });
423
+
424
+ test("an invite for an unregistered email (500 not found) still stores the hash", async () => {
425
+ const suffix = crypto.randomUUID().slice(0, 8);
426
+ const unregistered = `unregistered-${suffix}@example.test`;
427
+ process.env.AGENT_FS_API_URL = "https://agent-fs.example.test/";
428
+ process.env.AGENT_FS_REGISTER_EMAIL = "admin@example.test";
429
+ await seedSharedConfig();
430
+ await createUser({ name: "Unregistered", email: unregistered });
431
+
432
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
433
+ try {
434
+ const records: RequestRecord[] = [];
435
+ setAgentFsProvisionFetchForTests(
436
+ createFetchStub(records, {
437
+ inviteResponse: (email) =>
438
+ email === unregistered
439
+ ? Response.json({ error: `User with email ${email} not found` }, { status: 500 })
440
+ : undefined,
441
+ }),
442
+ );
443
+
444
+ const result = await runSeeder(agentFsProvisionSeeder, { quiet: true });
445
+
446
+ expect(result.failed).toEqual([]);
447
+ const [item] = await agentFsProvisionSeeder.items();
448
+ expect(await configValue("AGENT_FS_PROVISION_HASH")).toBe(item?.contentHash);
449
+
450
+ records.length = 0;
451
+ const second = await runSeeder(agentFsProvisionSeeder, { quiet: true });
452
+ expect(second.skippedUnchanged).toBe(1);
453
+ expect(records).toEqual([]);
454
+ } finally {
455
+ warn.mockRestore();
456
+ }
379
457
  });
380
458
 
381
459
  test("does not invite existing members when provisioning would keep or lower their role", async () => {
@@ -406,7 +484,12 @@ describe("agent-fs provisioning seeder", () => {
406
484
  await createUser({ name: "Existing Admin", email: existingAdmin, role: "Customer" });
407
485
  await createUser({ name: "Existing Editor", email: existingEditor, role: "Designer" });
408
486
  await createUser({ name: "Existing Viewer", email: existingViewer, role: "Whatever" });
409
- await createUser({ name: "Explicit Viewer", email: explicitViewer, role: "Guest" });
487
+ const explicitViewerUser = await createUser({
488
+ name: "Explicit Viewer",
489
+ email: explicitViewer,
490
+ role: "Guest",
491
+ });
492
+ await makeRbacViewer(explicitViewerUser.id);
410
493
 
411
494
  const records: RequestRecord[] = [];
412
495
  setAgentFsProvisionFetchForTests(
@@ -645,7 +728,8 @@ describe("agent-fs provisioning seeder", () => {
645
728
 
646
729
  const result = await inviteEmailToSharedOrg("Customer@Example.test ", "admin");
647
730
 
648
- expect(result).toEqual({ orgId: "shared-org", invited: true });
731
+ // Default stub answers like pre-0.18.9 agent-fs: a bare `{ ok: true }`.
732
+ expect(result).toEqual({ orgId: "shared-org", invited: true, status: "added" });
649
733
  const invite = records.find((r) => r.path === "/orgs/shared-org/members/invite");
650
734
  expect(invite?.method).toBe("POST");
651
735
  expect(invite?.body).toEqual({ email: "Customer@Example.test", role: "admin" });
@@ -665,7 +749,92 @@ describe("agent-fs provisioning seeder", () => {
665
749
 
666
750
  const result = await inviteEmailToSharedOrg("customer@example.test", "editor");
667
751
 
668
- expect(result).toEqual({ orgId: "shared-org", invited: false });
752
+ expect(result).toEqual({ orgId: "shared-org", invited: false, status: "already_member" });
669
753
  expect(records.filter((r) => r.path === "/orgs/shared-org/members/invite")).toEqual([]);
670
754
  });
755
+
756
+ test("reports an email with an agent-fs account as added, with no accept link", async () => {
757
+ process.env.AGENT_FS_API_URL = "https://agent-fs.example.test/";
758
+ process.env.AGENT_FS_REGISTER_EMAIL = "admin@example.test";
759
+
760
+ const records: RequestRecord[] = [];
761
+ setAgentFsProvisionFetchForTests(
762
+ createFetchStub(records, {
763
+ inviteResponse: {
764
+ ok: true,
765
+ status: "added",
766
+ userId: "customer-user",
767
+ email: "customer@example.test",
768
+ drives: [{ driveId: "shared-drive", role: "editor" }],
769
+ },
770
+ }),
771
+ );
772
+
773
+ const result = await inviteEmailToSharedOrg("customer@example.test", "editor");
774
+
775
+ expect(result).toEqual({ orgId: "shared-org", invited: true, status: "added" });
776
+ });
777
+
778
+ test("returns the pending invite and accept link for an email with no agent-fs account", async () => {
779
+ process.env.AGENT_FS_API_URL = "https://agent-fs.example.test/";
780
+ process.env.AGENT_FS_REGISTER_EMAIL = "admin@example.test";
781
+
782
+ const token = "a".repeat(43);
783
+ const records: RequestRecord[] = [];
784
+ setAgentFsProvisionFetchForTests(
785
+ createFetchStub(records, {
786
+ inviteResponse: {
787
+ ok: true,
788
+ status: "pending",
789
+ inviteId: "invite-1",
790
+ email: "new@example.test",
791
+ token,
792
+ expiresAt: "2026-11-03T00:00:00.000Z",
793
+ drives: [{ driveId: "shared-drive", role: "editor" }],
794
+ acceptUrl: `https://live.agent-fs.example.test/invite/${token}`,
795
+ },
796
+ }),
797
+ );
798
+
799
+ const result = await inviteEmailToSharedOrg("new@example.test", "editor");
800
+
801
+ expect(result).toEqual({
802
+ orgId: "shared-org",
803
+ invited: true,
804
+ status: "pending",
805
+ inviteId: "invite-1",
806
+ expiresAt: "2026-11-03T00:00:00.000Z",
807
+ acceptUrl: `https://live.agent-fs.example.test/invite/${token}`,
808
+ });
809
+ });
810
+
811
+ test("omits acceptUrl on a pending invite when agent-fs has no app URL", async () => {
812
+ process.env.AGENT_FS_API_URL = "https://agent-fs.example.test/";
813
+ process.env.AGENT_FS_REGISTER_EMAIL = "admin@example.test";
814
+
815
+ const records: RequestRecord[] = [];
816
+ setAgentFsProvisionFetchForTests(
817
+ createFetchStub(records, {
818
+ inviteResponse: {
819
+ ok: true,
820
+ status: "pending",
821
+ inviteId: "invite-2",
822
+ email: "new@example.test",
823
+ token: "b".repeat(43),
824
+ expiresAt: "2026-11-03T00:00:00.000Z",
825
+ drives: [],
826
+ },
827
+ }),
828
+ );
829
+
830
+ const result = await inviteEmailToSharedOrg("new@example.test", "viewer");
831
+
832
+ expect(result).toEqual({
833
+ orgId: "shared-org",
834
+ invited: true,
835
+ status: "pending",
836
+ inviteId: "invite-2",
837
+ expiresAt: "2026-11-03T00:00:00.000Z",
838
+ });
839
+ });
671
840
  });