@desplega.ai/agent-swarm 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/README.md +1 -0
  2. package/dist/{acp-adapter-v85a2bqy.js → acp-adapter-kedstxx6.js} +6 -6
  3. package/dist/{actions-zgcxfspj.js → actions-vget6r1k.js} +18 -16
  4. package/dist/{amp-adapter-jw9xfz42.js → amp-adapter-xeb5mktv.js} +2 -2
  5. package/dist/{app-4e1s4675.js → app-mqap8t8m.js} +3 -3
  6. package/dist/{assistant-v238n9dk.js → assistant-h0gv97mv.js} +20 -18
  7. package/dist/{boot-reembed-p666cw5n.js → boot-reembed-qr8a894z.js} +1 -1
  8. package/dist/{boot-reembed-w6pm64j4.js → boot-reembed-rj85jjhw.js} +1 -1
  9. package/dist/{boot-scrub-sweep-jcyjagn3.js → boot-scrub-sweep-eyfvn6v9.js} +3 -3
  10. package/dist/{boot-seeds-gbedmamp.js → boot-seeds-xs9a8dx1.js} +4 -4
  11. package/dist/{claude-adapter-q7wspzt8.js → claude-adapter-bm23cye7.js} +5 -5
  12. package/dist/{claude-managed-adapter-n7ahjyx6.js → claude-managed-adapter-1mwrsm9d.js} +2 -2
  13. package/dist/{claude-managed-setup-wza784fb.js → claude-managed-setup-gkfgks4j.js} +1 -1
  14. package/dist/{claude-sdk-session-yf400ptr.js → claude-sdk-session-apb4ypz3.js} +5 -5
  15. package/dist/cli-02vhhssk.js +848 -0
  16. package/dist/{cli-4ync7m60.js → cli-0ebey9hg.js} +1 -1
  17. package/dist/{cli-665j3feb.js → cli-0xk505e8.js} +1 -1
  18. package/dist/{cli-30m5q8mc.js → cli-1cevz46c.js} +5 -5
  19. package/dist/{cli-gzvb3d4a.js → cli-1jx0srjt.js} +3 -3
  20. package/dist/{cli-zj4d6afs.js → cli-1pa3rz69.js} +21 -6
  21. package/dist/{cli-fhvqt2fp.js → cli-2rwet3r7.js} +1 -1
  22. package/dist/{cli-azs9bhc5.js → cli-3wjs22x3.js} +2 -2
  23. package/dist/{cli-dxwh4j32.js → cli-4fhyq711.js} +45 -36
  24. package/dist/{cli-yv3n83x8.js → cli-5ff2sjyt.js} +1 -1
  25. package/dist/{cli-6vcyw6v7.js → cli-5s5hx57y.js} +5 -3
  26. package/dist/{cli-hz64bcnj.js → cli-6ftrrwm1.js} +9 -371
  27. package/dist/{cli-zes4tsbb.js → cli-8b9wvjps.js} +1 -1
  28. package/dist/cli-8cxkw1f5.js +382 -0
  29. package/dist/{cli-4s1kqrqg.js → cli-8rwvksbc.js} +2 -2
  30. package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
  31. package/dist/{cli-yv0vgscc.js → cli-a1esmmee.js} +13 -2
  32. package/dist/{cli-4hmaheb6.js → cli-avmqhatn.js} +7 -7
  33. package/dist/{cli-g6p7c2my.js → cli-b80a1100.js} +1 -1
  34. package/dist/{cli-tr24eeny.js → cli-bfc3k1x5.js} +3 -3
  35. package/dist/{cli-ka01m5rb.js → cli-bhwa8rzc.js} +13 -11
  36. package/dist/{cli-8cvvytad.js → cli-bn26xbcs.js} +2 -2
  37. package/dist/{cli-k28p6xh7.js → cli-c8fwpgqb.js} +1 -1
  38. package/dist/{cli-rc3r0bnh.js → cli-ccn0eybf.js} +1 -1
  39. package/dist/{cli-9td5ss65.js → cli-dsgj3e7a.js} +2 -2
  40. package/dist/{cli-a17tzmn0.js → cli-ecshf504.js} +2 -2
  41. package/dist/{cli-d43xqzbe.js → cli-eqgfs202.js} +1 -1
  42. package/dist/{cli-ppapy34w.js → cli-f3nx1h8z.js} +2 -2
  43. package/dist/{cli-ea7chd26.js → cli-ffbrkh1r.js} +74 -35
  44. package/dist/{cli-xgn4rwyv.js → cli-kk2r80n5.js} +4 -4
  45. package/dist/{cli-hn3pftj2.js → cli-kz116kvw.js} +3 -3
  46. package/dist/{cli-1v8w782f.js → cli-mvg2hf8p.js} +4 -4
  47. package/dist/{cli-5d1m1qca.js → cli-mz1ezzzh.js} +1 -1
  48. package/dist/{cli-jhmjwqcg.js → cli-nme7wery.js} +6 -6
  49. package/dist/{cli-65q70v2v.js → cli-nrrg0fvy.js} +18 -4
  50. package/dist/{cli-8jtrekpy.js → cli-p1m2b6gc.js} +7 -7
  51. package/dist/{cli-re4wkbzx.js → cli-rg7ntypm.js} +328 -47
  52. package/dist/{cli-fp4xbw07.js → cli-rsd7ga7v.js} +10 -8
  53. package/dist/{cli-etvn5ej2.js → cli-sppc0cmw.js} +1 -1
  54. package/dist/{cli-qrwp40st.js → cli-tg9ksbn8.js} +1 -1
  55. package/dist/{cli-6p7ddg72.js → cli-tgvpmnwz.js} +2 -2
  56. package/dist/{cli-q1812jbw.js → cli-tj4j2ts4.js} +6 -6
  57. package/dist/{cli-kqypty34.js → cli-ttaz6c7m.js} +2 -2
  58. package/dist/{cli-w7z94e6c.js → cli-tx00c0h6.js} +4 -2
  59. package/dist/{cli-4g629sgr.js → cli-wrv09v7q.js} +20 -1
  60. package/dist/{cli-cby329ny.js → cli-wyrr38xm.js} +2 -2
  61. package/dist/{cli-y3dvz6ge.js → cli-x1p5dhq2.js} +3 -3
  62. package/dist/{cli-8qsx817b.js → cli-x719j3vs.js} +219 -629
  63. package/dist/{cli-9e9kjqsj.js → cli-xfe63zx3.js} +1 -1
  64. package/dist/{cli-wtg6edvd.js → cli-ywyrffff.js} +1 -1
  65. package/dist/{cli-0rh20bp3.js → cli-zvze9rbm.js} +1 -1
  66. package/dist/cli.js +14 -14
  67. package/dist/{codex-adapter-nhjj2veb.js → codex-adapter-pp2gq8gr.js} +3 -3
  68. package/dist/{codex-hook-x6tm6y5m.js → codex-hook-qwzccd64.js} +1 -1
  69. package/dist/{codex-session-runner-6hggrjbx.js → codex-session-runner-0vzn09t8.js} +3 -3
  70. package/dist/{commands-mrw1ph2w.js → commands-psqhkttb.js} +2 -2
  71. package/dist/{controller-pt1zam5w.js → controller-t20vz2k8.js} +2 -2
  72. package/dist/{controllers-connect-ymw5541d.js → controllers-connect-gkxmqw9d.js} +24 -20
  73. package/dist/{cursor-adapter-cf4zh5x0.js → cursor-adapter-3v542qe5.js} +1 -1
  74. package/dist/{db-hy80219q.js → db-2xbyenjr.js} +2 -2
  75. package/dist/{db-hhpy6m4x.js → db-nc1rd869.js} +3 -2
  76. package/dist/{dispatcher-6pdjtkr1.js → dispatcher-ewceb8ts.js} +17 -15
  77. package/dist/{dsh-adapter-fbx06re2.js → dsh-adapter-75xwh78t.js} +1 -1
  78. package/dist/{easter-egg-3d-6hg2r942.js → easter-egg-3d-xsc47jwf.js} +1 -1
  79. package/dist/{execute-bcqc69a5.js → execute-gzmw1mnw.js} +1 -1
  80. package/dist/{external-channel-j6vb6ya1.js → external-channel-a5dsmxn4.js} +2 -2
  81. package/dist/{google-generative-ai-phpmwxvg.js → google-generative-ai-bmq8gkf3.js} +1 -1
  82. package/dist/{google-vertex-es9ybgdy.js → google-vertex-b1rvp52s.js} +1 -1
  83. package/dist/{grok-adapter-1193sprp.js → grok-adapter-vj88xr23.js} +7 -7
  84. package/dist/handlers-zbs90bny.js +39 -0
  85. package/dist/{heartbeat-h1w1ktce.js → heartbeat-t2m9r3wa.js} +20 -18
  86. package/dist/{hook-skrbja18.js → hook-8femx1bg.js} +4 -4
  87. package/dist/{hook-ejja4bpe.js → hook-xa677f8r.js} +5 -5
  88. package/dist/{http-mzh41mfj.js → http-xpmmq028.js} +868 -253
  89. package/dist/{index-vjn3wbzf.js → index-8pwc1jc8.js} +17 -15
  90. package/dist/{index-epr2gnbw.js → index-cdzyfrsp.js} +1 -1
  91. package/dist/{index-xmmkyx82.js → index-dg2tmj99.js} +17 -15
  92. package/dist/index-nxa6me0q.js +41 -0
  93. package/dist/{index-6rmnn89d.js → index-s7ckdajq.js} +9 -9
  94. package/dist/{index-wgsw9xcz.js → index-syztqdt6.js} +1 -1
  95. package/dist/{index-x0q2a4f9.js → index-ysd45kgc.js} +1 -1
  96. package/dist/{index-g909y089.js → index-zgvqzjsw.js} +8 -8
  97. package/dist/{keepalive-4djvbm2j.js → keepalive-3nr4st3r.js} +3 -3
  98. package/dist/{lead-61kqp604.js → lead-td8h2sfn.js} +33 -31
  99. package/dist/{maintenance-f3xhnjsm.js → maintenance-verwpe2c.js} +1 -1
  100. package/dist/{oauth-refresh-sweep-fpd6x6rw.js → oauth-refresh-sweep-9q2t0y3q.js} +2 -2
  101. package/dist/{onboard-196tjgyn.js → onboard-aytqedmq.js} +2 -2
  102. package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-f4hryjny.js} +4 -4
  103. package/dist/{otel-impl-g8fh4c21.js → otel-impl-hckw4wt1.js} +1 -1
  104. package/dist/{pi-mono-adapter-zasp3ze3.js → pi-mono-adapter-hxby0ce1.js} +6 -6
  105. package/dist/{pricing-refresh-sdcf5yaz.js → pricing-refresh-5nvme3sm.js} +3 -3
  106. package/dist/{rbac-roles-bsp5657v.js → rbac-roles-pd8c26m1.js} +5 -3
  107. package/dist/{rbac-roles-2x4g1wb5.js → rbac-roles-vbpy21mg.js} +2 -2
  108. package/dist/{render-v2-0pyb39bb.js → render-v2-7nqgvk2q.js} +3 -3
  109. package/dist/{resume-a86tkj6r.js → resume-b6djcj3c.js} +6 -6
  110. package/dist/{runtime-ar8310ba.js → runtime-txrxc9za.js} +1 -1
  111. package/dist/session-access-fc3f9y12.js +25 -0
  112. package/dist/{setup-ypbzcapd.js → setup-n39szm6b.js} +1 -1
  113. package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-wyp2k6z3.js} +2 -2
  114. package/dist/{work-objects-yjp8de12.js → work-objects-jem2fre0.js} +20 -18
  115. package/dist/{worker-4ptnn1ge.js → worker-wr6xgfr2.js} +33 -31
  116. package/openapi.json +844 -110
  117. package/package.json +1 -1
  118. package/src/apps/action-caller.ts +6 -0
  119. package/src/apps/script-types.ts +16 -3
  120. package/src/be/db/active-sessions.ts +5 -0
  121. package/src/be/db/session-visibility.ts +29 -0
  122. package/src/be/db/tasks/read.ts +61 -3
  123. package/src/be/db/tasks/write.ts +15 -6
  124. package/src/be/db.ts +92 -33
  125. package/src/be/events.ts +11 -0
  126. package/src/be/migrations/230_session_shares.sql +23 -0
  127. package/src/be/rbac-roles.ts +2 -0
  128. package/src/be/session-shares.ts +244 -0
  129. package/src/be/swarm-config-guard.ts +23 -2
  130. package/src/commands/runner.ts +11 -0
  131. package/src/http/active-sessions.ts +13 -2
  132. package/src/http/agents.ts +10 -3
  133. package/src/http/all-routes.ts +1 -0
  134. package/src/http/api-keys.ts +3 -1
  135. package/src/http/approval-requests.ts +2 -1
  136. package/src/http/apps.ts +36 -11
  137. package/src/http/config.ts +3 -0
  138. package/src/http/context.ts +2 -0
  139. package/src/http/core.ts +39 -2
  140. package/src/http/events.ts +6 -2
  141. package/src/http/favorite-owner.ts +1 -1
  142. package/src/http/fs.ts +38 -3
  143. package/src/http/index.ts +3 -1
  144. package/src/http/memory.ts +3 -1
  145. package/src/http/ranking.ts +367 -0
  146. package/src/http/realtime.ts +26 -21
  147. package/src/http/route-def.ts +77 -11
  148. package/src/http/script-runs.ts +2 -1
  149. package/src/http/scripts.ts +8 -0
  150. package/src/http/session-access.ts +179 -0
  151. package/src/http/session-data.ts +10 -2
  152. package/src/http/sessions.ts +212 -12
  153. package/src/http/stats.ts +7 -3
  154. package/src/http/tasks.ts +83 -7
  155. package/src/http/used-by.ts +11 -3
  156. package/src/prompts/session-templates.ts +3 -1
  157. package/src/providers/amp-adapter.ts +1 -1
  158. package/src/ranking/telemetry-ranking.ts +147 -0
  159. package/src/ranking/usage.ts +57 -0
  160. package/src/rbac/admission.ts +22 -7
  161. package/src/rbac/legacy-policy.ts +2 -0
  162. package/src/rbac/permissions.ts +9 -0
  163. package/src/realtime/auth.ts +69 -0
  164. package/src/realtime/tickets.ts +4 -1
  165. package/src/realtime/transport.ts +77 -9
  166. package/src/slack/blocks.ts +18 -0
  167. package/src/slack/dm.ts +29 -0
  168. package/src/telemetry-contract/EVENTS.md +3 -0
  169. package/src/telemetry-contract/SOURCE.json +37 -4
  170. package/src/telemetry-contract/catalog.json +69 -0
  171. package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
  172. package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
  173. package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
  174. package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
  175. package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
  176. package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
  177. package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
  178. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
  179. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
  180. package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
  181. package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
  182. package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
  183. package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
  184. package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
  185. package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
  186. package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
  187. package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
  188. package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
  189. package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
  190. package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
  191. package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
  192. package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
  193. package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
  194. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
  195. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
  196. package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
  197. package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
  198. package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
  199. package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
  200. package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
  201. package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
  202. package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
  203. package/src/telemetry-contract/ranking/taxonomy.json +8 -0
  204. package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
  205. package/src/telemetry-contract/types.gen.ts +708 -1
  206. package/src/telemetry.ts +26 -2
  207. package/src/tests/apps-rbac.test.ts +189 -2
  208. package/src/tests/apps-script-types.test.ts +16 -3
  209. package/src/tests/check-pr-body.test.ts +23 -0
  210. package/src/tests/check-ranking-entry.test.ts +146 -0
  211. package/src/tests/db-extraction-contract.test.ts +1 -0
  212. package/src/tests/db-task-read-contract.test.ts +1 -0
  213. package/src/tests/docs-index-footprint.test.js +2 -2
  214. package/src/tests/org-identity.test.ts +44 -0
  215. package/src/tests/ranking-listing.test.ts +112 -0
  216. package/src/tests/ranking-route.test.ts +319 -0
  217. package/src/tests/ranking-usage.test.ts +50 -0
  218. package/src/tests/rbac-admission-e2e.test.ts +8 -0
  219. package/src/tests/rbac-admission.test.ts +38 -0
  220. package/src/tests/rbac-charact-http.test.ts +4 -3
  221. package/src/tests/rbac-engine.test.ts +2 -0
  222. package/src/tests/rbac-roles.test.ts +2 -0
  223. package/src/tests/realtime-room-surfaces.test.ts +3 -1
  224. package/src/tests/realtime-session-join-race.test.ts +142 -0
  225. package/src/tests/realtime-tickets.test.ts +24 -17
  226. package/src/tests/realtime-transport.test.ts +91 -0
  227. package/src/tests/session-route-posture.test.ts +639 -0
  228. package/src/tests/session-sharing-routes.test.ts +282 -0
  229. package/src/tests/session-sharing.test.ts +526 -0
  230. package/src/tests/sessions-detail-batch.test.ts +7 -1
  231. package/src/tests/slack-tool-blocks.test.ts +101 -0
  232. package/src/tests/slack-update.test.ts +55 -0
  233. package/src/tests/slack-user-dm.test.ts +177 -0
  234. package/src/tests/used-by-route.test.ts +10 -0
  235. package/src/tools/slack-post.ts +18 -12
  236. package/src/tools/slack-read.ts +13 -1
  237. package/src/tools/slack-reply.ts +7 -10
  238. package/src/tools/slack-start-thread.ts +18 -11
  239. package/src/tools/slack-update.ts +2 -9
  240. package/src/types.ts +35 -0
  241. package/src/utils/org-identity.ts +33 -0
  242. package/templates/skills/apps/SKILL.md +17 -0
  243. package/templates/skills/apps/content.md +17 -0
  244. package/dist/handlers-tb3xgjnm.js +0 -37
  245. package/dist/index-nyfcdpky.js +0 -39
  246. package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
  247. /package/dist/{cli-j0v75y13.js → cli-y83zk5sb.js} +0 -0
@@ -128,6 +128,61 @@ describe("slack-update", () => {
128
128
  });
129
129
  });
130
130
 
131
+ test("splits a long message into sections under Slack's cap", async () => {
132
+ const message = Array.from(
133
+ { length: 120 },
134
+ (_, index) => `line ${String(index).padStart(3, "0")} radar item with some words`,
135
+ )
136
+ .join("\n")
137
+ .slice(0, 3946);
138
+ const tool = buildTool();
139
+ const result = await tool.handler(
140
+ { channelId: "C1", messageTs: "1783411554.596189", message },
141
+ meta(leadAgentId),
142
+ );
143
+
144
+ expect(result.structuredContent.success).toBe(true);
145
+ const payload = mockChatUpdate.mock.calls[0][0] as {
146
+ text: string;
147
+ blocks: { type: string; text: { type: string; text: string } }[];
148
+ };
149
+ expect(payload.text).toBe(message);
150
+ expect(payload.blocks.length).toBeGreaterThanOrEqual(2);
151
+ for (const block of payload.blocks) {
152
+ expect(block.type).toBe("section");
153
+ expect(block.text.text.length).toBeLessThanOrEqual(2900);
154
+ }
155
+ const joined = payload.blocks.map((block) => block.text.text).join("");
156
+ expect(joined.replace(/\s/g, "")).toBe(message.replace(/\s/g, ""));
157
+ });
158
+
159
+ test("keeps a 2,931-char message with an emoji at 2,900 in one intact section", async () => {
160
+ const message = `${"x".repeat(2899)}😀${"y".repeat(30)}`;
161
+ expect(message.length).toBe(2931);
162
+ const tool = buildTool();
163
+ const result = await tool.handler(
164
+ { channelId: "C1", messageTs: "1783411554.596189", message },
165
+ meta(leadAgentId),
166
+ );
167
+
168
+ expect(result.structuredContent.success).toBe(true);
169
+ expect(mockChatUpdate.mock.calls[0][0].blocks).toEqual([
170
+ { type: "section", text: { type: "mrkdwn", text: message } },
171
+ ]);
172
+ });
173
+
174
+ test("keeps a short message in exactly one section", async () => {
175
+ const tool = buildTool();
176
+ await tool.handler(
177
+ { channelId: "C1", messageTs: "1783411554.596189", message: "short" },
178
+ meta(leadAgentId),
179
+ );
180
+
181
+ expect(mockChatUpdate.mock.calls[0][0]).toMatchObject({
182
+ blocks: [{ type: "section", text: { type: "mrkdwn", text: "short" } }],
183
+ });
184
+ });
185
+
131
186
  test.each([
132
187
  ["message_not_found", "No message found at that timestamp in this channel."],
133
188
  [
@@ -0,0 +1,177 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { closeDb, createAgent, getSlackMessageByChannelTs, initDb } from "../be/db";
5
+ import { isSlackUserId } from "../slack/dm";
6
+ import { registerSlackPostTool } from "../tools/slack-post";
7
+ import { registerSlackReadTool } from "../tools/slack-read";
8
+ import { registerSlackStartThreadTool } from "../tools/slack-start-thread";
9
+
10
+ const TEST_DB_PATH = "./test-slack-user-dm.sqlite";
11
+ const USER_ID = "U08USERDM";
12
+ // The bot's own IM with USER_ID: readable, writable, delivers message.im events.
13
+ const BOT_DM = "D0BOTDM01";
14
+ // Where Slack puts a bot post addressed to the bare user id: the bot is not a member.
15
+ const FOREIGN_DM = "D0FOREIGN";
16
+
17
+ function slackError(code: string): Error {
18
+ return Object.assign(new Error(`An API error occurred: ${code}`), {
19
+ data: { ok: false, error: code },
20
+ });
21
+ }
22
+
23
+ let tsCounter = 0;
24
+ const postMessage = mock(async (args: { channel: string; thread_ts?: string }) => {
25
+ if (args.channel === FOREIGN_DM) throw slackError("channel_not_found");
26
+ const ts = `500.${++tsCounter}`;
27
+ return { ok: true, ts, channel: args.channel === USER_ID ? FOREIGN_DM : args.channel };
28
+ });
29
+ const conversationsOpen = mock(async (args: { users: string }) => {
30
+ if (args.users !== USER_ID) throw slackError("user_not_found");
31
+ return { ok: true, channel: { id: BOT_DM } };
32
+ });
33
+ const conversationsReplies = mock(async (args: { channel: string; ts: string }) => {
34
+ if (args.channel !== BOT_DM) throw slackError("channel_not_found");
35
+ return { ok: true, messages: [{ user: USER_ID, text: "reply from human", ts: args.ts }] };
36
+ });
37
+
38
+ mock.module("../slack/app", () => ({
39
+ getSlackApp: () => ({
40
+ client: {
41
+ chat: { postMessage },
42
+ conversations: { open: conversationsOpen, replies: conversationsReplies },
43
+ auth: { test: async () => ({ ok: true, user_id: "UBOT" }) },
44
+ users: { info: async () => ({ ok: true, user: { real_name: "Human" } }) },
45
+ },
46
+ }),
47
+ }));
48
+
49
+ type ToolResult = {
50
+ structuredContent: {
51
+ success: boolean;
52
+ message: string;
53
+ channelId?: string;
54
+ ts?: string;
55
+ messages?: Array<{ text: string }>;
56
+ };
57
+ };
58
+ type RegisteredTool = { handler: (args: unknown, extra: unknown) => Promise<ToolResult> };
59
+
60
+ function buildTools() {
61
+ const server = new McpServer({ name: "slack-user-dm-test", version: "1.0.0" });
62
+ registerSlackPostTool(server);
63
+ registerSlackReadTool(server);
64
+ registerSlackStartThreadTool(server);
65
+ return (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
66
+ ._registeredTools;
67
+ }
68
+
69
+ function meta(agentId: string) {
70
+ return { sessionId: "s1", requestInfo: { headers: { "x-agent-id": agentId } } };
71
+ }
72
+
73
+ async function removeDbFiles() {
74
+ for (const suffix of ["", "-wal", "-shm"]) {
75
+ try {
76
+ await unlink(`${TEST_DB_PATH}${suffix}`);
77
+ } catch {}
78
+ }
79
+ }
80
+
81
+ let leadId: string;
82
+
83
+ beforeAll(async () => {
84
+ await removeDbFiles();
85
+ initDb(TEST_DB_PATH);
86
+ leadId = (await createAgent({ name: "Lead DM", isLead: true, status: "idle" })).id;
87
+ });
88
+
89
+ beforeEach(() => {
90
+ postMessage.mockClear();
91
+ conversationsOpen.mockClear();
92
+ conversationsReplies.mockClear();
93
+ });
94
+
95
+ afterAll(async () => {
96
+ closeDb();
97
+ await removeDbFiles();
98
+ });
99
+
100
+ describe("Slack tools addressed with a user id", () => {
101
+ test("isSlackUserId matches user ids only", () => {
102
+ expect(isSlackUserId("U08NR6QD6CS")).toBe(true);
103
+ expect(isSlackUserId("W012ABCDEF")).toBe(true);
104
+ expect(isSlackUserId("D0A4T383FMF")).toBe(false);
105
+ expect(isSlackUserId("C0AR967K0KZ")).toBe(false);
106
+ expect(isSlackUserId("G01PRIVATE")).toBe(false);
107
+ });
108
+
109
+ test("slack-start-thread opens the bot DM and returns an id that reads and writes work on", async () => {
110
+ const tools = buildTools();
111
+
112
+ const started = await tools["slack-start-thread"]!.handler(
113
+ { channelId: USER_ID, message: "hello" },
114
+ meta(leadId),
115
+ );
116
+ expect(started.structuredContent.success).toBe(true);
117
+ expect(conversationsOpen).toHaveBeenCalledWith({ users: USER_ID });
118
+ expect(postMessage.mock.calls[0]![0].channel).toBe(BOT_DM);
119
+ expect(started.structuredContent.channelId).toBe(BOT_DM);
120
+ const threadTs = started.structuredContent.ts!;
121
+ // The recorded message points at the bot DM, so inbound replies match it.
122
+ expect((await getSlackMessageByChannelTs(BOT_DM, threadTs))?.threadTs).toBe(threadTs);
123
+
124
+ const posted = await tools["slack-post"]!.handler(
125
+ { channelId: BOT_DM, threadTs, message: "follow-up" },
126
+ meta(leadId),
127
+ );
128
+ expect(posted.structuredContent.success).toBe(true);
129
+ expect(postMessage.mock.calls[1]![0]).toMatchObject({ channel: BOT_DM, thread_ts: threadTs });
130
+
131
+ const read = await tools["slack-read"]!.handler({ channelId: BOT_DM, threadTs }, meta(leadId));
132
+ expect(read.structuredContent.success).toBe(true);
133
+ expect(read.structuredContent.messages?.[0]?.text).toContain("reply from human");
134
+ });
135
+
136
+ test("slack-post and slack-read with the user id resolve to the same bot DM", async () => {
137
+ const tools = buildTools();
138
+
139
+ const posted = await tools["slack-post"]!.handler(
140
+ { channelId: USER_ID, threadTs: "500.1", message: "by user id" },
141
+ meta(leadId),
142
+ );
143
+ expect(posted.structuredContent.success).toBe(true);
144
+ expect(postMessage.mock.calls[0]![0]).toMatchObject({ channel: BOT_DM, thread_ts: "500.1" });
145
+
146
+ const read = await tools["slack-read"]!.handler(
147
+ { channelId: USER_ID, threadTs: "500.1" },
148
+ meta(leadId),
149
+ );
150
+ expect(read.structuredContent.success).toBe(true);
151
+ expect(conversationsReplies.mock.calls[0]![0].channel).toBe(BOT_DM);
152
+ });
153
+
154
+ test("conversation ids pass through without opening a DM", async () => {
155
+ const tools = buildTools();
156
+
157
+ const started = await tools["slack-start-thread"]!.handler(
158
+ { channelId: "C0CHANNEL1", message: "channel post" },
159
+ meta(leadId),
160
+ );
161
+ expect(started.structuredContent.channelId).toBe("C0CHANNEL1");
162
+ expect(postMessage.mock.calls[0]![0].channel).toBe("C0CHANNEL1");
163
+ expect(conversationsOpen).not.toHaveBeenCalled();
164
+ });
165
+
166
+ test("a user id Slack cannot open a DM with fails without posting", async () => {
167
+ const tools = buildTools();
168
+
169
+ const started = await tools["slack-start-thread"]!.handler(
170
+ { channelId: "U0NOBODY1", message: "hello" },
171
+ meta(leadId),
172
+ );
173
+ expect(started.structuredContent.success).toBe(false);
174
+ expect(started.structuredContent.message).toContain("user_not_found");
175
+ expect(postMessage).not.toHaveBeenCalled();
176
+ });
177
+ });
@@ -184,8 +184,18 @@ describe("POST /api/used-by", () => {
184
184
  await upsertSwarmConfig({ scope: "global", key: "SWARM_ORG_WEBSITE", value: "" });
185
185
  const result = await post({ granted: true }, OPERATOR);
186
186
  expect(result.statusCode).toBe(400);
187
+ expect(JSON.parse(result.body)).toEqual({
188
+ error: "Set the organization name and website first",
189
+ });
187
190
  expect(proxied).toEqual([]);
188
191
  });
192
+
193
+ test("a placeholder name still grants: the ranking's placeholder rule does not apply", async () => {
194
+ await upsertSwarmConfig({ scope: "global", key: "SWARM_ORG_NAME", value: "Swarm" });
195
+ const result = await post({ granted: true }, OPERATOR);
196
+ expect(result.statusCode).toBe(200);
197
+ expect(proxied[0]?.body).toMatchObject({ org_name: "Swarm", org_website: "acme.com" });
198
+ });
189
199
  });
190
200
 
191
201
  describe("fillOrgLogoFromWebsite", () => {
@@ -8,8 +8,9 @@ import {
8
8
  } from "@/be/db";
9
9
  import { can } from "@/rbac";
10
10
  import { getSlackApp } from "@/slack/app";
11
- import { getTaskLink } from "@/slack/blocks";
11
+ import { buildMrkdwnSectionBlocks, getTaskLink } from "@/slack/blocks";
12
12
  import { withAutoJoin } from "@/slack/channel-join";
13
+ import { resolveSlackConversationId } from "@/slack/dm";
13
14
  import { externalChannelToolError, getChannelBlockReason } from "@/slack/external-channel";
14
15
  import { getAgentDisplayName, getAgentEmoji, markdownToSlack } from "@/slack/responses";
15
16
  import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
@@ -24,7 +25,12 @@ export const registerSlackPostTool = (server: McpServer) => {
24
25
  annotations: { openWorldHint: true },
25
26
 
26
27
  inputSchema: z.object({
27
- channelId: z.string().min(1).describe("The Slack channel ID to post to."),
28
+ channelId: z
29
+ .string()
30
+ .min(1)
31
+ .describe(
32
+ "The Slack channel ID to post to. A user ID (U…/W…) posts in the bot's DM with that user.",
33
+ ),
28
34
  message: z.string().min(1).max(4000).describe("The message content to post."),
29
35
  blocks: z
30
36
  .array(z.record(z.string(), z.unknown()))
@@ -42,7 +48,7 @@ export const registerSlackPostTool = (server: McpServer) => {
42
48
  messageTs: z.string().optional(),
43
49
  }),
44
50
  },
45
- async ({ channelId, message, threadTs, blocks }, requestInfo, _meta) => {
51
+ async ({ channelId: requestedChannelId, message, threadTs, blocks }, requestInfo, _meta) => {
46
52
  if (!requestInfo.agentId) {
47
53
  return toolErr("Agent ID not found.");
48
54
  }
@@ -68,6 +74,14 @@ export const registerSlackPostTool = (server: McpServer) => {
68
74
  return toolErr("Slack not configured.");
69
75
  }
70
76
 
77
+ let channelId: string;
78
+ try {
79
+ channelId = await resolveSlackConversationId(app.client, requestedChannelId);
80
+ } catch (error) {
81
+ const errorMsg = error instanceof Error ? error.message : String(error);
82
+ return toolErr(`Failed to open a DM with ${requestedChannelId}: ${errorMsg}`);
83
+ }
84
+
71
85
  const blockReason = await getChannelBlockReason(app.client, channelId);
72
86
  if (blockReason) {
73
87
  return toolErr(externalChannelToolError(channelId, blockReason));
@@ -82,15 +96,7 @@ export const registerSlackPostTool = (server: McpServer) => {
82
96
  const contextKey = sourceTask?.contextKey;
83
97
  const tree = threadTs ? await getSlackTreeMessageByThread(channelId, threadTs) : null;
84
98
  const messageBlocks: Record<string, unknown>[] = [
85
- ...(blocks ?? [
86
- {
87
- type: "section",
88
- text: {
89
- type: "mrkdwn",
90
- text: slackMessage,
91
- },
92
- },
93
- ]),
99
+ ...(blocks ?? buildMrkdwnSectionBlocks(slackMessage)),
94
100
  ];
95
101
  if (sourceTask && tree) {
96
102
  if (messageBlocks.length >= 50) {
@@ -4,6 +4,7 @@ import { getAgentById, getInboxMessageById, getTaskById } from "@/be/db";
4
4
  import { can } from "@/rbac";
5
5
  import { getSlackApp } from "@/slack/app";
6
6
  import { withAutoJoin } from "@/slack/channel-join";
7
+ import { resolveSlackConversationId } from "@/slack/dm";
7
8
  import { externalChannelToolError, getChannelBlockReason } from "@/slack/external-channel";
8
9
  import { downloadFile, type SlackFile } from "@/slack/files";
9
10
  import { attachableTask, attachSlackFilesForAgent } from "@/slack/inbound-files";
@@ -59,7 +60,9 @@ export const registerSlackReadTool = (server: McpServer) => {
59
60
  channelId: z
60
61
  .string()
61
62
  .optional()
62
- .describe("Slack channel ID to read from (requires lead privileges)."),
63
+ .describe(
64
+ "Slack channel ID to read from (requires lead privileges). A user ID (U…/W…) reads the bot's DM with that user.",
65
+ ),
63
66
  threadTs: z
64
67
  .string()
65
68
  .optional()
@@ -155,6 +158,15 @@ export const registerSlackReadTool = (server: McpServer) => {
155
158
  return toolErr("Slack not configured.", { data: { messages: [] } });
156
159
  }
157
160
 
161
+ try {
162
+ slackChannelId = await resolveSlackConversationId(app.client, slackChannelId);
163
+ } catch (error) {
164
+ const errorMsg = error instanceof Error ? error.message : String(error);
165
+ return toolErr(`Failed to open a DM with ${slackChannelId}: ${errorMsg}`, {
166
+ data: { messages: [] },
167
+ });
168
+ }
169
+
158
170
  const blockReason = await getChannelBlockReason(app.client, slackChannelId);
159
171
  if (blockReason) {
160
172
  return toolErr(externalChannelToolError(slackChannelId, blockReason), {
@@ -11,7 +11,12 @@ import {
11
11
  } from "@/be/db";
12
12
  import { getTaskCitations } from "@/be/task-citations";
13
13
  import { getSlackApp } from "@/slack/app";
14
- import { buildCaptionBlock, getTaskLink, getTaskUrl } from "@/slack/blocks";
14
+ import {
15
+ buildCaptionBlock,
16
+ buildMrkdwnSectionBlocks,
17
+ getTaskLink,
18
+ getTaskUrl,
19
+ } from "@/slack/blocks";
15
20
  import { withAutoJoin } from "@/slack/channel-join";
16
21
  import { externalChannelToolError, getChannelBlockReason } from "@/slack/external-channel";
17
22
  import { getAgentDisplayName, getAgentEmoji, markdownToSlack } from "@/slack/responses";
@@ -140,15 +145,7 @@ export const registerSlackReplyTool = (server: McpServer) => {
140
145
 
141
146
  const tree = await getSlackTreeMessageByThread(slackChannelId, slackThreadTs);
142
147
  const messageBlocks: Record<string, unknown>[] = [
143
- ...(renderedBlocks ?? [
144
- {
145
- type: "section",
146
- text: {
147
- type: "mrkdwn",
148
- text: slackMessage,
149
- },
150
- },
151
- ]),
148
+ ...(renderedBlocks ?? buildMrkdwnSectionBlocks(slackMessage)),
152
149
  ];
153
150
  for (const group of taskCitationSourceGroups(
154
151
  blocks ? JSON.stringify(blocks) : message,
@@ -3,7 +3,9 @@ import * as z from "zod";
3
3
  import { getAgentById, getTaskById, recordSlackMessage } from "@/be/db";
4
4
  import { can } from "@/rbac";
5
5
  import { getSlackApp } from "@/slack/app";
6
+ import { buildMrkdwnSectionBlocks } from "@/slack/blocks";
6
7
  import { withAutoJoin } from "@/slack/channel-join";
8
+ import { resolveSlackConversationId } from "@/slack/dm";
7
9
  import { externalChannelToolError, getChannelBlockReason } from "@/slack/external-channel";
8
10
  import { getAgentDisplayName, getAgentEmoji, markdownToSlack } from "@/slack/responses";
9
11
  import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
@@ -18,7 +20,12 @@ export const registerSlackStartThreadTool = (server: McpServer) => {
18
20
  annotations: { openWorldHint: true },
19
21
 
20
22
  inputSchema: z.object({
21
- channelId: z.string().min(1).describe("The Slack channel ID to post to."),
23
+ channelId: z
24
+ .string()
25
+ .min(1)
26
+ .describe(
27
+ "The Slack channel ID to post to. A user ID (U…/W…) opens the bot's DM with that user; use the returned channelId afterwards.",
28
+ ),
22
29
  message: z.string().min(1).max(4000).describe("The message content to post."),
23
30
  blocks: z
24
31
  .array(z.record(z.string(), z.unknown()))
@@ -32,7 +39,7 @@ export const registerSlackStartThreadTool = (server: McpServer) => {
32
39
  messageTs: z.string().optional(),
33
40
  }),
34
41
  },
35
- async ({ channelId, message, blocks }, requestInfo, _meta) => {
42
+ async ({ channelId: requestedChannelId, message, blocks }, requestInfo, _meta) => {
36
43
  if (!requestInfo.agentId) {
37
44
  return toolErr("Agent ID not found.");
38
45
  }
@@ -57,6 +64,14 @@ export const registerSlackStartThreadTool = (server: McpServer) => {
57
64
  return toolErr("Slack not configured.");
58
65
  }
59
66
 
67
+ let channelId: string;
68
+ try {
69
+ channelId = await resolveSlackConversationId(app.client, requestedChannelId);
70
+ } catch (error) {
71
+ const errorMsg = error instanceof Error ? error.message : String(error);
72
+ return toolErr(`Failed to open a DM with ${requestedChannelId}: ${errorMsg}`);
73
+ }
74
+
60
75
  const blockReason = await getChannelBlockReason(app.client, channelId);
61
76
  if (blockReason) {
62
77
  return toolErr(externalChannelToolError(channelId, blockReason));
@@ -64,15 +79,7 @@ export const registerSlackStartThreadTool = (server: McpServer) => {
64
79
 
65
80
  try {
66
81
  const slackMessage = markdownToSlack(message);
67
- const messageBlocks = blocks ?? [
68
- {
69
- type: "section",
70
- text: {
71
- type: "mrkdwn",
72
- text: slackMessage,
73
- },
74
- },
75
- ];
82
+ const messageBlocks = blocks ?? buildMrkdwnSectionBlocks(slackMessage);
76
83
 
77
84
  const result = await withAutoJoin(app.client, channelId, () =>
78
85
  app.client.chat.postMessage({
@@ -4,6 +4,7 @@ import * as z from "zod";
4
4
  import { getAgentById } from "@/be/db";
5
5
  import { can } from "@/rbac";
6
6
  import { getSlackApp } from "@/slack/app";
7
+ import { buildMrkdwnSectionBlocks } from "@/slack/blocks";
7
8
  import { externalChannelToolError, getChannelBlockReason } from "@/slack/external-channel";
8
9
  import { parseSlackTs } from "@/slack/message-text";
9
10
  import { markdownToSlack } from "@/slack/responses";
@@ -77,15 +78,7 @@ export const registerSlackUpdateTool = (server: McpServer) => {
77
78
  text: slackMessage,
78
79
  unfurl_links: false,
79
80
  unfurl_media: false,
80
- blocks: [
81
- {
82
- type: "section",
83
- text: {
84
- type: "mrkdwn",
85
- text: slackMessage,
86
- },
87
- },
88
- ],
81
+ blocks: buildMrkdwnSectionBlocks(slackMessage),
89
82
  } satisfies ChatUpdatePayload;
90
83
  const result = await app.client.chat.update(updatePayload);
91
84
 
package/src/types.ts CHANGED
@@ -453,6 +453,41 @@ export const ModelProfileSchema = z.object({
453
453
  });
454
454
  export type ModelProfile = z.infer<typeof ModelProfileSchema>;
455
455
 
456
+ // ============================================================================
457
+ // Session sharing
458
+ // ============================================================================
459
+ //
460
+ // `SessionShareAccessSchema` MUST stay in sync with the SQL CHECK constraint
461
+ // on session_shares.access (migration 230).
462
+
463
+ export const SessionShareAccessSchema = z.enum(["read", "write"]);
464
+ export type SessionShareAccess = z.infer<typeof SessionShareAccessSchema>;
465
+
466
+ /** The caller's access to a session. `none` is never returned; it is a 403. */
467
+ export const SessionAccessSchema = z.enum(["owner", "write", "read"]);
468
+
469
+ export const SessionOwnerSchema = z
470
+ .object({ id: z.string(), name: z.string() })
471
+ .openapi("SessionOwner");
472
+
473
+ export const SessionSharesSchema = z
474
+ .object({
475
+ /** What every user may do. `none` = private (owner and named users only). */
476
+ everyone: z.enum(["none", "read", "write"]),
477
+ users: z.array(
478
+ z.object({ userId: z.string(), access: SessionShareAccessSchema, createdAt: z.string() }),
479
+ ),
480
+ })
481
+ .openapi("SessionShares");
482
+ export type SessionShares = z.infer<typeof SessionSharesSchema>;
483
+
484
+ export const SessionSharesUpdateSchema = z.object({
485
+ everyone: z.enum(["none", "read", "write"]),
486
+ users: z
487
+ .array(z.object({ userId: z.string().min(1), access: SessionShareAccessSchema }))
488
+ .max(500),
489
+ });
490
+
456
491
  // ============================================================================
457
492
  // Task Steering Messages
458
493
  // ============================================================================
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The "is the org identity set" rule, shared by every feature that needs a real
3
+ * organization name and website (the public ranking, the "used by" grant).
4
+ * Pure: the API passes global config values, workers pass `process.env`.
5
+ */
6
+ import { normalizeOrgWebsite } from "./org-website";
7
+
8
+ export const ORG_IDENTITY_KEYS = ["SWARM_ORG_NAME", "SWARM_ORG_WEBSITE"] as const;
9
+ export type OrgIdentityKey = (typeof ORG_IDENTITY_KEYS)[number];
10
+
11
+ /** Names the product falls back to when none is set; they are not an org name. */
12
+ const PLACEHOLDER_NAMES = new Set(["swarm", "your swarm"]);
13
+
14
+ export interface OrgIdentityStatus {
15
+ /** The trimmed name, or null when blank. A placeholder is returned but listed as missing. */
16
+ name: string | null;
17
+ /** The website as a bare hostname, or null when blank or not a public hostname. */
18
+ website: string | null;
19
+ missing: OrgIdentityKey[];
20
+ ok: boolean;
21
+ }
22
+
23
+ export function orgIdentityStatus(src: {
24
+ SWARM_ORG_NAME?: string | null;
25
+ SWARM_ORG_WEBSITE?: string | null;
26
+ }): OrgIdentityStatus {
27
+ const name = src.SWARM_ORG_NAME?.trim() || null;
28
+ const website = normalizeOrgWebsite(src.SWARM_ORG_WEBSITE) || null;
29
+ const missing: OrgIdentityKey[] = [];
30
+ if (!name || PLACEHOLDER_NAMES.has(name.toLowerCase())) missing.push("SWARM_ORG_NAME");
31
+ if (!website) missing.push("SWARM_ORG_WEBSITE");
32
+ return { name, website, missing, ok: missing.length === 0 };
33
+ }
@@ -30,6 +30,23 @@ For every edit: `app-get` -> modify the smallest coherent subtree -> `app-patch`
30
30
 
31
31
  One `app-get` returns everything needed to drive an app you did not write: `definition.queries` are runnable with `app-query` (supply every `$param` the query references), and `definition.actions` are invocable with `POST /api/apps/<id>/actions/<name>` (a `script` action runs its script, a `task` action spawns a task, a `sync` action refreshes sources). Read a query's `$param` placeholders and an action's kind/args from the definition before calling — do not guess parameter names.
32
32
 
33
+ ### Verified action caller
34
+
35
+ Script actions receive `caller` as a reserved, server-stamped argument. The server overwrites `caller` from definition defaults and request input.
36
+
37
+ ```typescript
38
+ type AppActionCaller = {
39
+ kind: "user" | "agent" | "operator" | "guest";
40
+ id: string | null;
41
+ name?: string;
42
+ pageId?: string;
43
+ };
44
+ ```
45
+
46
+ The caller identifies the authenticated user or agent. Operator and guest callers have `id: null`. A signed page session adds `pageId`.
47
+ With the swarm key, a registered `X-Agent-ID` produces an agent caller and `agent:<id>` row provenance. Missing or unknown agent IDs remain operator callers.
48
+ Task actions include the same caller as server-verified JSON in the task prompt. Script execution still uses the saved script owner's bindings.
49
+
33
50
  ## Safe schema evolution and rollback
34
51
 
35
52
  Prefer **hiding** a column over deleting it. A hidden column's values stay on every existing row, so an unhide or rollback can make it visible again. Hiding is metadata-only: hidden columns cannot be used in queries, page bindings, `app.mutate` values, or new row writes, and `required` is ignored while hidden. Row GETs still expose the historical value for backward compatibility.
@@ -25,6 +25,23 @@ For every edit: `app-get` -> modify the smallest coherent subtree -> `app-patch`
25
25
 
26
26
  One `app-get` returns everything needed to drive an app you did not write: `definition.queries` are runnable with `app-query` (supply every `$param` the query references), and `definition.actions` are invocable with `POST /api/apps/<id>/actions/<name>` (a `script` action runs its script, a `task` action spawns a task, a `sync` action refreshes sources). Read a query's `$param` placeholders and an action's kind/args from the definition before calling — do not guess parameter names.
27
27
 
28
+ ### Verified action caller
29
+
30
+ Script actions receive `caller` as a reserved, server-stamped argument. The server overwrites `caller` from definition defaults and request input.
31
+
32
+ ```typescript
33
+ type AppActionCaller = {
34
+ kind: "user" | "agent" | "operator" | "guest";
35
+ id: string | null;
36
+ name?: string;
37
+ pageId?: string;
38
+ };
39
+ ```
40
+
41
+ The caller identifies the authenticated user or agent. Operator and guest callers have `id: null`. A signed page session adds `pageId`.
42
+ With the swarm key, a registered `X-Agent-ID` produces an agent caller and `agent:<id>` row provenance. Missing or unknown agent IDs remain operator callers.
43
+ Task actions include the same caller as server-verified JSON in the task prompt. Script execution still uses the saved script owner's bindings.
44
+
28
45
  ## Safe schema evolution and rollback
29
46
 
30
47
  Prefer **hiding** a column over deleting it. A hidden column's values stay on every existing row, so an unhide or rollback can make it visible again. Hiding is metadata-only: hidden columns cannot be used in queries, page bindings, `app.mutate` values, or new row writes, and `required` is ignored while hidden. Row GETs still expose the historical value for backward compatibility.
@@ -1,37 +0,0 @@
1
- import {
2
- isUserAllowed2,
3
- registerMessageHandler2
4
- } from "./cli-4hmaheb6.js";
5
- import"./cli-30m5q8mc.js";
6
- import"./cli-y3dvz6ge.js";
7
- import"./cli-8qsx817b.js";
8
- import"./cli-rdr166ht.js";
9
- import"./cli-fhvqt2fp.js";
10
- import"./cli-qz93y3x5.js";
11
- import"./cli-65q70v2v.js";
12
- import"./cli-3h26v7yj.js";
13
- import"./cli-7a91w22y.js";
14
- import"./cli-3ggwdtb0.js";
15
- import"./cli-g6p7c2my.js";
16
- import"./cli-xgn4rwyv.js";
17
- import"./cli-kqypty34.js";
18
- import"./cli-qrwp40st.js";
19
- import"./cli-qje9k2nh.js";
20
- import"./cli-jhmjwqcg.js";
21
- import"./cli-q1812jbw.js";
22
- import"./cli-8cvvytad.js";
23
- import"./cli-vjfpshn3.js";
24
- import"./cli-13p25grh.js";
25
- import"./cli-0adyhvq2.js";
26
- import"./cli-dxwh4j32.js";
27
- import"./cli-9e9kjqsj.js";
28
- import"./cli-hz64bcnj.js";
29
- import"./cli-445c1vp7.js";
30
- import"./cli-kvzgak7c.js";
31
- import"./cli-6vcyw6v7.js";
32
- import"./cli-re4wkbzx.js";
33
- import"./cli-ptkpaqfy.js";
34
- export {
35
- isUserAllowed2 as isUserAllowed,
36
- registerMessageHandler2 as registerMessageHandler
37
- };