@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
@@ -16,9 +16,15 @@ import {
16
16
  import { closeDb, createAgent, createUser, getDbClient, initDb } from "../be/db";
17
17
  import { flushAuditBuffer } from "../be/rbac-audit";
18
18
  import { attachRole, detachRole, ensureRbacSeedsSynced } from "../be/rbac-roles";
19
+ import {
20
+ resetAgentFsProvisionFetchForTests,
21
+ setAgentFsProvisionFetchForTests,
22
+ } from "../be/seed/agent-fs-provision";
19
23
  import { type IdentityActor, mintToken } from "../be/users";
20
24
  import { handleCore } from "../http/core";
25
+ import { handleDbQuery } from "../http/db-query";
21
26
  import { handleFs } from "../http/fs";
27
+ import { handleInboxState } from "../http/inbox-state";
22
28
  import { handleMcpOAuth } from "../http/mcp-oauth";
23
29
  import { handleRealtimeTicket } from "../http/realtime";
24
30
  import { handleTasks } from "../http/tasks";
@@ -61,6 +67,8 @@ function createTestServer(): Server {
61
67
  if (await handleCore(req, res, myAgentId, API_KEY)) return;
62
68
  if (await handleTasks(req, res, pathSegments, queryParams, myAgentId)) return;
63
69
  if (await handleFs(req, res, pathSegments, queryParams, myAgentId)) return;
70
+ if (await handleInboxState(req, res, pathSegments, queryParams)) return;
71
+ if (await handleDbQuery(req, res, pathSegments, queryParams)) return;
64
72
  if (await handleMcpOAuth(req, res, pathSegments, queryParams)) return;
65
73
  if (await handleRealtimeTicket(req, res, pathSegments, queryParams)) return;
66
74
 
@@ -426,14 +434,20 @@ describe("handleCore admission wiring", () => {
426
434
  expect(await admissionAuditRows()).toEqual([]);
427
435
  });
428
436
 
429
- test("flag on requester grant denies verb-less writes, allows reads and declared verbs, and audits each decision", async () => {
437
+ test("flag on requester grant admits declared verbs and reads, denies verb-less writes, and audits each decision", async () => {
430
438
  process.env.RBAC_ENABLED = "true";
431
439
  const { userId, plaintext } = await createTokenForUser("Requester User");
432
440
  await narrowUserToRequester(userId);
433
441
 
434
- const denied = await api(port, "POST", "/api/tasks", {
442
+ const created = await api(port, "POST", "/api/tasks", {
443
+ bearer: plaintext,
444
+ body: { task: "requester creates its own task" },
445
+ });
446
+ expect(created.status).toBe(201);
447
+
448
+ const denied = await api(port, "POST", "/api/db-query", {
435
449
  bearer: plaintext,
436
- body: { task: "requester should not create generic tasks" },
450
+ body: { sql: "select 1" },
437
451
  });
438
452
  expect(denied.status).toBe(403);
439
453
  expect(denied.body).toEqual({
@@ -454,9 +468,17 @@ describe("handleCore admission wiring", () => {
454
468
 
455
469
  await flushAuditBuffer();
456
470
  const rows = await admissionAuditRows();
457
- expect(rows).toHaveLength(3);
471
+ expect(rows).toHaveLength(4);
458
472
 
459
473
  expect(rows.find((row) => row.resourceId === "POST /api/tasks")).toMatchObject({
474
+ principalType: "user",
475
+ principalId: userId,
476
+ verb: "task.create.own",
477
+ decision: "allow",
478
+ reason: null,
479
+ source: "http",
480
+ });
481
+ expect(rows.find((row) => row.resourceId === "POST /api/db-query")).toMatchObject({
460
482
  principalType: "user",
461
483
  principalId: userId,
462
484
  verb: "(admission:no-verb)",
@@ -485,6 +507,322 @@ describe("handleCore admission wiring", () => {
485
507
  });
486
508
  });
487
509
 
510
+ describe("requester and viewer task lifecycle", () => {
511
+ async function createTask(bearer: string, task: string): Promise<string> {
512
+ const res = await api(port, "POST", "/api/tasks", { bearer, body: { task } });
513
+ expect(res.status).toBe(201);
514
+ return (res.body as { id: string }).id;
515
+ }
516
+
517
+ async function setStatus(taskId: string, status: string): Promise<void> {
518
+ await getDbClient().run("UPDATE agent_tasks SET status = ? WHERE id = ?", [status, taskId]);
519
+ }
520
+
521
+ async function lifecycle(bearer: string, taskId: string): Promise<Record<string, number>> {
522
+ await setStatus(taskId, "in_progress");
523
+ const pause = await api(port, "POST", `/api/tasks/${taskId}/pause`, { bearer });
524
+ await setStatus(taskId, "paused");
525
+ const resume = await api(port, "POST", `/api/tasks/${taskId}/resume`, { bearer });
526
+ const steer = await api(port, "POST", `/api/tasks/${taskId}/steer`, {
527
+ bearer,
528
+ body: { message: "requester steer" },
529
+ });
530
+ const title = await api(port, "PATCH", `/api/tasks/${taskId}/title`, {
531
+ bearer,
532
+ body: { title: "requester title" },
533
+ });
534
+ const cancel = await api(port, "POST", `/api/tasks/${taskId}/cancel`, { bearer });
535
+ return {
536
+ pause: pause.status,
537
+ resume: resume.status,
538
+ steer: steer.status,
539
+ title: title.status,
540
+ cancel: cancel.status,
541
+ };
542
+ }
543
+
544
+ test("a requester creates, pauses, resumes, steers, renames and cancels its own task", async () => {
545
+ process.env.RBAC_ENABLED = "true";
546
+ const { userId, plaintext } = await createTokenForUser("Lifecycle Requester");
547
+ await narrowUserToRequester(userId);
548
+
549
+ const taskId = await createTask(plaintext, "requester lifecycle");
550
+ const owner = await getDbClient().get<{ requestedByUserId: string | null }>(
551
+ "SELECT requestedByUserId FROM agent_tasks WHERE id = ?",
552
+ [taskId],
553
+ );
554
+ expect(owner?.requestedByUserId).toBe(userId);
555
+
556
+ expect(await lifecycle(plaintext, taskId)).toEqual({
557
+ pause: 200,
558
+ resume: 200,
559
+ steer: 200,
560
+ title: 200,
561
+ cancel: 200,
562
+ });
563
+ });
564
+
565
+ test("the same calls on another user's task hit the session wall, not admission", async () => {
566
+ process.env.RBAC_ENABLED = "true";
567
+ const other = await createTokenForUser("Task Owner");
568
+ const foreignTaskId = await createTask(other.plaintext, "someone else's task");
569
+ const { userId, plaintext } = await createTokenForUser("Foreign Requester");
570
+ await narrowUserToRequester(userId);
571
+
572
+ const calls: [string, string, unknown][] = [
573
+ ["POST", `/api/tasks/${foreignTaskId}/pause`, undefined],
574
+ ["POST", `/api/tasks/${foreignTaskId}/resume`, undefined],
575
+ ["POST", `/api/tasks/${foreignTaskId}/steer`, { message: "not mine" }],
576
+ ["PATCH", `/api/tasks/${foreignTaskId}/title`, { title: "not mine" }],
577
+ ["POST", `/api/tasks/${foreignTaskId}/cancel`, undefined],
578
+ ];
579
+ for (const [method, path, body] of calls) {
580
+ const res = await api(port, method, path, { bearer: plaintext, body });
581
+ const error = (res.body as { error?: string }).error ?? "";
582
+ expect({ path, status: res.status, admission: error.includes("admission:") }).toEqual({
583
+ path,
584
+ status: 403,
585
+ admission: false,
586
+ });
587
+ }
588
+ const task = await getDbClient().get<{ status: string; title: string | null }>(
589
+ "SELECT status, title FROM agent_tasks WHERE id = ?",
590
+ [foreignTaskId],
591
+ );
592
+ expect(task?.status).not.toBe("cancelled");
593
+ expect(task?.title).not.toBe("not mine");
594
+ });
595
+
596
+ // Regression: the session wall reads an ownerless root (Slack, schedules,
597
+ // operator key) as everyone's, so without the handler ownership check a
598
+ // requester could cancel, pause, resume or rename any such task.
599
+ test("a requester cannot change an ownerless task, an admin user still can", async () => {
600
+ process.env.RBAC_ENABLED = "true";
601
+ const { userId, plaintext } = await createTokenForUser("Ownerless Requester");
602
+ await narrowUserToRequester(userId);
603
+ const admin = await createTokenForUser("Ownerless Admin");
604
+
605
+ const taskId = await createTask(API_KEY, "operator task with no owner");
606
+ const owner = await getDbClient().get<{ requestedByUserId: string | null }>(
607
+ "SELECT requestedByUserId FROM agent_tasks WHERE id = ?",
608
+ [taskId],
609
+ );
610
+ expect(owner?.requestedByUserId).toBeNull();
611
+
612
+ await setStatus(taskId, "in_progress");
613
+ const pause = await api(port, "POST", `/api/tasks/${taskId}/pause`, { bearer: plaintext });
614
+ await setStatus(taskId, "paused");
615
+ const resume = await api(port, "POST", `/api/tasks/${taskId}/resume`, { bearer: plaintext });
616
+ const title = await api(port, "PATCH", `/api/tasks/${taskId}/title`, {
617
+ bearer: plaintext,
618
+ body: { title: "not mine" },
619
+ });
620
+ const cancel = await api(port, "POST", `/api/tasks/${taskId}/cancel`, { bearer: plaintext });
621
+ expect({
622
+ pause: pause.status,
623
+ resume: resume.status,
624
+ title: title.status,
625
+ cancel: cancel.status,
626
+ }).toEqual({ pause: 403, resume: 403, title: 403, cancel: 403 });
627
+ const task = await getDbClient().get<{ status: string; title: string | null }>(
628
+ "SELECT status, title FROM agent_tasks WHERE id = ?",
629
+ [taskId],
630
+ );
631
+ expect(task?.status).toBe("paused");
632
+ expect(task?.title).not.toBe("not mine");
633
+
634
+ const adminTitle = await api(port, "PATCH", `/api/tasks/${taskId}/title`, {
635
+ bearer: admin.plaintext,
636
+ body: { title: "admin title" },
637
+ });
638
+ const adminCancel = await api(port, "POST", `/api/tasks/${taskId}/cancel`, {
639
+ bearer: admin.plaintext,
640
+ });
641
+ expect({ title: adminTitle.status, cancel: adminCancel.status }).toEqual({
642
+ title: 200,
643
+ cancel: 200,
644
+ });
645
+ });
646
+
647
+ test("a viewer cannot create a task", async () => {
648
+ process.env.RBAC_ENABLED = "true";
649
+ const { userId, plaintext } = await createTokenForUser("Viewer User");
650
+ await detachRole(userId, "admin");
651
+ await attachRole(userId, "viewer");
652
+
653
+ const res = await api(port, "POST", "/api/tasks", {
654
+ bearer: plaintext,
655
+ body: { task: "viewer should not create" },
656
+ });
657
+ expect(res.status).toBe(403);
658
+ expect(res.body).toEqual({
659
+ error: "Forbidden: admission: missing permission 'task.create.own'",
660
+ });
661
+ });
662
+ });
663
+
664
+ describe("per-user inbox state", () => {
665
+ const item = { itemType: "approval", itemId: "inbox-guard", status: "done" };
666
+
667
+ test("a requester writes its own inbox state but not another user's", async () => {
668
+ process.env.RBAC_ENABLED = "true";
669
+ const other = await createUser({ name: "Inbox Other" });
670
+ const { userId, plaintext } = await createTokenForUser("Inbox Requester");
671
+ await narrowUserToRequester(userId);
672
+
673
+ const own = await api(port, "PATCH", "/api/inbox-state", {
674
+ bearer: plaintext,
675
+ body: { ...item, userId },
676
+ });
677
+ expect(own.status).toBe(200);
678
+
679
+ const foreign = await api(port, "PATCH", "/api/inbox-state", {
680
+ bearer: plaintext,
681
+ body: { ...item, userId: other.id },
682
+ });
683
+ expect(foreign.status).toBe(403);
684
+
685
+ const foreignBulk = await api(port, "PATCH", "/api/inbox-state/bulk", {
686
+ bearer: plaintext,
687
+ body: { userId: other.id, items: [item] },
688
+ });
689
+ expect(foreignBulk.status).toBe(403);
690
+
691
+ const rows = await getDbClient().query<{ userId: string }>(
692
+ "SELECT userId FROM inbox_item_state WHERE itemId = 'inbox-guard'",
693
+ );
694
+ expect(rows.map((row) => row.userId)).toEqual([userId]);
695
+ });
696
+
697
+ test("the operator key and an admin user still write any user's inbox state", async () => {
698
+ process.env.RBAC_ENABLED = "true";
699
+ const other = await createUser({ name: "Inbox Target" });
700
+ const admin = await createTokenForUser("Inbox Admin");
701
+
702
+ const operator = await api(port, "PATCH", "/api/inbox-state", {
703
+ body: { ...item, userId: other.id },
704
+ });
705
+ expect(operator.status).toBe(200);
706
+
707
+ const adminCall = await api(port, "PATCH", "/api/inbox-state/bulk", {
708
+ bearer: admin.plaintext,
709
+ body: { userId: other.id, items: [{ ...item, itemId: "inbox-guard-2" }] },
710
+ });
711
+ expect(adminCall.status).toBe(200);
712
+ });
713
+ });
714
+
715
+ describe("join the shared agent-fs drive", () => {
716
+ const AGENT_FS_ENV_KEYS = [
717
+ "AGENT_FS_API_URL",
718
+ "API_AGENT_FS_API_KEY",
719
+ "AGENT_FS_API_KEY",
720
+ "AGENT_FS_DEFAULT_ORG_ID",
721
+ "AGENT_FS_SHARED_ORG_ID",
722
+ "AGENT_FS_DEFAULT_DRIVE_ID",
723
+ "AGENT_FS_REGISTER_EMAIL",
724
+ ];
725
+ let invites: { email: string; role: string }[] = [];
726
+
727
+ beforeEach(() => {
728
+ invites = [];
729
+ for (const key of AGENT_FS_ENV_KEYS) delete process.env[key];
730
+ process.env.AGENT_FS_API_URL = "https://agent-fs.example.test";
731
+ process.env.API_AGENT_FS_API_KEY = "example-afs-admin-key";
732
+ process.env.AGENT_FS_REGISTER_EMAIL = "admin@example.test";
733
+ setAgentFsProvisionFetchForTests((async (input, init) => {
734
+ const url = new URL(String(input));
735
+ const method = init?.method ?? "GET";
736
+ if (url.pathname === "/auth/me") return Response.json({ userId: "admin-user" });
737
+ if (url.pathname === "/orgs" && method === "GET") {
738
+ return Response.json({ orgs: [{ id: "shared-org", name: "swarm" }] });
739
+ }
740
+ if (url.pathname === "/orgs/shared-org/drives" && method === "GET") {
741
+ return Response.json({ drives: [{ id: "shared-drive", name: "shared" }] });
742
+ }
743
+ if (url.pathname === "/orgs/shared-org/members" && method === "GET") {
744
+ return Response.json({ members: [] });
745
+ }
746
+ if (url.pathname === "/orgs/shared-org/members/invite" && method === "POST") {
747
+ invites.push(JSON.parse(String(init?.body)));
748
+ return Response.json({ ok: true });
749
+ }
750
+ return Response.json({ error: "unexpected route" }, { status: 500 });
751
+ }) as typeof fetch);
752
+ });
753
+
754
+ afterEach(() => {
755
+ resetAgentFsProvisionFetchForTests();
756
+ for (const key of AGENT_FS_ENV_KEYS) delete process.env[key];
757
+ });
758
+
759
+ async function tokenWithRole(name: string, email: string, role: string) {
760
+ const user = await createUser({ name, email });
761
+ const { plaintext } = await mintToken(user.id, "admission", ACTOR);
762
+ if (role !== "admin") {
763
+ await detachRole(user.id, "admin");
764
+ await attachRole(user.id, role);
765
+ }
766
+ return plaintext;
767
+ }
768
+
769
+ test("a requester cannot invite another email", async () => {
770
+ process.env.RBAC_ENABLED = "true";
771
+ const bearer = await tokenWithRole("Join Requester", "Req.User@example.test", "requester");
772
+
773
+ const res = await api(port, "POST", "/api/fs/members/invite", {
774
+ bearer,
775
+ body: { email: "someone-else@example.test", role: "admin" },
776
+ });
777
+ expect(res.status).toBe(403);
778
+ expect(invites).toEqual([]);
779
+ });
780
+
781
+ test("a requester joins with its own email as editor, whatever role it asks for", async () => {
782
+ process.env.RBAC_ENABLED = "true";
783
+ const bearer = await tokenWithRole("Join Requester", "Req.User@example.test", "requester");
784
+
785
+ const res = await api(port, "POST", "/api/fs/members/invite", {
786
+ bearer,
787
+ body: { email: "req.user@EXAMPLE.test", role: "admin" },
788
+ });
789
+ expect(res.status).toBe(200);
790
+ expect(res.body).toEqual({ orgId: "shared-org", invited: true, status: "added" });
791
+ expect(invites).toEqual([{ email: "Req.User@example.test", role: "editor" }]);
792
+ });
793
+
794
+ test("a viewer joins with its own email as viewer", async () => {
795
+ process.env.RBAC_ENABLED = "true";
796
+ const bearer = await tokenWithRole("Join Viewer", "viewer@example.test", "viewer");
797
+
798
+ const res = await api(port, "POST", "/api/fs/members/invite", {
799
+ bearer,
800
+ body: { email: "viewer@example.test" },
801
+ });
802
+ expect(res.status).toBe(200);
803
+ expect(invites).toEqual([{ email: "viewer@example.test", role: "viewer" }]);
804
+ });
805
+
806
+ test("the operator key and an admin user invite any email at the requested role", async () => {
807
+ process.env.RBAC_ENABLED = "true";
808
+ const bearer = await tokenWithRole("Join Admin", "admin-user@example.test", "admin");
809
+
810
+ const operator = await api(port, "POST", "/api/fs/members/invite", {
811
+ body: { email: "teammate@example.test", role: "admin" },
812
+ });
813
+ expect(operator.status).toBe(200);
814
+ const admin = await api(port, "POST", "/api/fs/members/invite", {
815
+ bearer,
816
+ body: { email: "another@example.test" },
817
+ });
818
+ expect(admin.status).toBe(200);
819
+ expect(invites).toEqual([
820
+ { email: "teammate@example.test", role: "admin" },
821
+ { email: "another@example.test", role: "editor" },
822
+ ]);
823
+ });
824
+ });
825
+
488
826
  describe("realtime ticket admission", () => {
489
827
  async function grantOnly(userId: string, verbs: PermissionVerb[]): Promise<void> {
490
828
  const roleId = `rbac-role-test-${crypto.randomUUID()}`;
@@ -139,6 +139,7 @@ const ANY_AUTHENTICATED_VERBS: PermissionVerb[] = [
139
139
  "mcp-oauth.authorize.any",
140
140
  "task.create.own",
141
141
  "favorite.write.own",
142
+ "fs.join.own",
142
143
  "script.search",
143
144
  ];
144
145
 
@@ -16,7 +16,9 @@ import type { PermissionVerb } from "../rbac";
16
16
  const TEST_DB_PATH = "./test-rbac-roles.sqlite";
17
17
  const REQUESTER_ROLE_ID = "rbac-role-requester";
18
18
  const REQUESTER_VERBS = [
19
+ "comb.presence",
19
20
  "favorite.write.own",
21
+ "fs.join.own",
20
22
  "session.presence",
21
23
  "session.share",
22
24
  "task.action.own",
@@ -24,6 +26,7 @@ const REQUESTER_VERBS = [
24
26
  "task.create.own",
25
27
  "task.fs.mutate",
26
28
  "task.read.own",
29
+ "task.steer.own",
27
30
  ] satisfies PermissionVerb[];
28
31
 
29
32
  async function removeDbFiles() {
@@ -194,7 +197,12 @@ describe("ensureRbacSeedsSynced", () => {
194
197
  ensureRbacSeedsSynced({ quiet: true });
195
198
 
196
199
  const viewerVerbs = await roleVerbs("rbac-role-viewer");
197
- expect(viewerVerbs).toEqual(["favorite.write.own", "session.presence"]);
200
+ expect(viewerVerbs).toEqual([
201
+ "comb.presence",
202
+ "favorite.write.own",
203
+ "fs.join.own",
204
+ "session.presence",
205
+ ]);
198
206
  for (const verb of viewerVerbs) expect(REQUESTER_VERBS).toContain(verb);
199
207
  });
200
208
 
@@ -2,7 +2,8 @@
2
2
  * A task's requester drives attribution, cost reporting and admission. A non-lead agent may omit
3
3
  * `requestedByUserId` or pass the requester of its own current task. Only a lead, the operator or
4
4
  * a user may name any other registered user. Covers the send-task tool and POST /api/tasks.
5
- * POST /api/tasks also refuses an agent a foreign parent or a chat thread it does not own.
5
+ * POST /api/tasks also refuses an agent a foreign parent or a chat thread it does not own, and
6
+ * refuses a non-admin user a chat thread.
6
7
  */
7
8
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8
9
  import { unlink } from "node:fs/promises";
@@ -16,8 +17,10 @@ import {
16
17
  createUser,
17
18
  getDbClient,
18
19
  getTaskById,
20
+ getUserById,
19
21
  initDb,
20
22
  } from "../be/db";
23
+ import { attachRole, detachRole } from "../be/rbac-roles";
21
24
  import { handleTasks } from "../http/tasks";
22
25
  import { chatSdkContextKey } from "../tasks/context-key";
23
26
  import { registerSendTaskTool } from "../tools/send-task";
@@ -70,7 +73,7 @@ const taskCount = async () =>
70
73
  const KEYED_REQUEST: HttpRequestAuth = { kind: "operator", fingerprint: "test-key" };
71
74
 
72
75
  async function postTask(
73
- caller: { agentId?: string; sourceTaskId?: string },
76
+ caller: { agentId?: string; sourceTaskId?: string; userId?: string },
74
77
  body: Record<string, unknown>,
75
78
  ): Promise<{ status: number; body: { id?: string; requestedByUserId?: string } }> {
76
79
  const response = await fetch(`${routeBaseUrl}/api/tasks`, {
@@ -79,6 +82,7 @@ async function postTask(
79
82
  "Content-Type": "application/json",
80
83
  ...(caller.agentId ? { "X-Agent-ID": caller.agentId } : {}),
81
84
  ...(caller.sourceTaskId ? { "X-Source-Task-Id": caller.sourceTaskId } : {}),
85
+ ...(caller.userId ? { "X-Test-User-Id": caller.userId } : {}),
82
86
  },
83
87
  body: JSON.stringify({ task: "created over HTTP", ...body }),
84
88
  });
@@ -114,7 +118,10 @@ beforeAll(async () => {
114
118
  registerSendTaskTool(mcp);
115
119
 
116
120
  routeServer = createServer(async (req, res) => {
117
- setRequestAuth(req, KEYED_REQUEST);
121
+ // A test user id header stands in for an aswt_ user token.
122
+ const testUserId = req.headers["x-test-user-id"] as string | undefined;
123
+ const user = testUserId ? await getUserById(testUserId) : null;
124
+ setRequestAuth(req, user ? { kind: "user", userId: user.id, user } : KEYED_REQUEST);
118
125
  const url = req.url ?? "/";
119
126
  const handled = await handleTasks(
120
127
  req,
@@ -316,3 +323,57 @@ describe("POST /api/tasks parent and chat-thread binding for an agent caller", (
316
323
  ).toBe(201);
317
324
  });
318
325
  });
326
+
327
+ describe("POST /api/tasks chat-thread and context binding for a user caller", () => {
328
+ test("a Requester cannot bind a new task to a chat thread by contextKey", async () => {
329
+ const requester = await createUser({ name: "Chat Requester", email: "chat-req@example.com" });
330
+ // A new user starts as admin. Make this one a Requester only.
331
+ await attachRole(requester.id, "requester");
332
+ await detachRole(requester.id, "admin");
333
+ const before = await taskCount();
334
+ const result = await postTask(
335
+ { userId: requester.id },
336
+ { source: "chat", contextKey: chatSdkContextKey("telegram:556666") },
337
+ );
338
+ expect(result.status).toBe(403);
339
+ expect(await taskCount()).toBe(before);
340
+
341
+ // A page session key is not a chat thread: the Requester keeps it.
342
+ const page = await postTask({ userId: requester.id }, { contextKey: "home:abc123" });
343
+ expect(page.status).toBe(201);
344
+ });
345
+
346
+ test("a Requester cannot name the context of another user's open task", async () => {
347
+ const requester = await createUser({ name: "Context Requester", email: "ctx-req@example.com" });
348
+ await attachRole(requester.id, "requester");
349
+ await detachRole(requester.id, "admin");
350
+ await createTaskExtended("private case notes", {
351
+ agentId: LEAD_ID,
352
+ contextKey: "case:private-1",
353
+ requestedByUserId: userAId,
354
+ });
355
+ const before = await taskCount();
356
+ const foreign = await postTask({ userId: requester.id }, { contextKey: "case:private-1" });
357
+ expect(foreign.status).toBe(403);
358
+ expect(await taskCount()).toBe(before);
359
+
360
+ // The context of the Requester's own open task stays usable.
361
+ await createTaskExtended("my case notes", {
362
+ agentId: LEAD_ID,
363
+ contextKey: "case:mine-1",
364
+ requestedByUserId: requester.id,
365
+ });
366
+ const own = await postTask({ userId: requester.id }, { contextKey: "case:mine-1" });
367
+ expect(own.status).toBe(201);
368
+ });
369
+
370
+ test("an admin user may bind a task to a chat thread", async () => {
371
+ const admin = await createUser({ name: "Chat Admin", email: "chat-admin@example.com" });
372
+ await attachRole(admin.id, "admin");
373
+ const result = await postTask(
374
+ { userId: admin.id },
375
+ { source: "chat", contextKey: chatSdkContextKey("telegram:557777") },
376
+ );
377
+ expect(result.status).toBe(201);
378
+ });
379
+ });
@@ -16,10 +16,11 @@ import {
16
16
  upsertInboxState,
17
17
  } from "../be/db";
18
18
  import * as sessionMoves from "../be/db/session-moves";
19
+ import { attachRole, detachRole, ensureRbacSeedsSynced } from "../be/rbac-roles";
19
20
  import { setSessionShares } from "../be/session-shares";
20
21
  import * as assets from "../http/assets";
21
22
  import { handleAssets } from "../http/assets";
22
- import { SESSION_READ_DENIED } from "../http/session-access";
23
+ import { NOT_YOUR_TASK_DENIED, SESSION_READ_DENIED } from "../http/session-access";
23
24
  import { handleSessions } from "../http/sessions";
24
25
  import { getPathSegments, parseQueryParams } from "../http/utils";
25
26
  import { chatSdkContextKey } from "../tasks/context-key";
@@ -705,3 +706,40 @@ test("old route denies read-only sharees with and without cascade and admits wri
705
706
  200,
706
707
  );
707
708
  });
709
+
710
+ // Regression: the session wall reads an ownerless root as everyone's, so with
711
+ // RBAC on a requester could re-key Slack, schedule and operator sessions.
712
+ test("with RBAC on, a requester cannot move an ownerless session; an admin user still can", async () => {
713
+ const previous = process.env.RBAC_ENABLED;
714
+ process.env.RBAC_ENABLED = "true";
715
+ try {
716
+ await ensureRbacSeedsSynced();
717
+ const requester = (await createUser({ name: "Move Requester" })).id;
718
+ await detachRole(requester, "admin");
719
+ await attachRole(requester, "requester");
720
+ const ownerless = await createTaskExtended("ownerless root", { key: "shared/slack/", agentId });
721
+ const own = await session("shared/requester-own/", requester);
722
+
723
+ const denied = await call(
724
+ "session-moves",
725
+ { rootTaskIds: [ownerless.id, own.root.id], groupKey: "shared/requester-moved/" },
726
+ requester,
727
+ );
728
+ expect(denied.status).toBe(200);
729
+ expect(denied.body.moved).toEqual([own.root.id]);
730
+ expect(denied.body.skipped).toEqual([
731
+ { rootTaskId: ownerless.id, reason: NOT_YOUR_TASK_DENIED },
732
+ ]);
733
+ expect((await getTaskById(ownerless.id))?.key).toBe("shared/slack/");
734
+
735
+ const admin = await call(
736
+ "session-moves",
737
+ { rootTaskIds: [ownerless.id], groupKey: "shared/admin-moved/" },
738
+ owner,
739
+ );
740
+ expect(admin.body.moved).toEqual([ownerless.id]);
741
+ } finally {
742
+ if (previous === undefined) delete process.env.RBAC_ENABLED;
743
+ else process.env.RBAC_ENABLED = previous;
744
+ }
745
+ });