@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.
- package/README.md +1 -0
- package/dist/{acp-adapter-v85a2bqy.js → acp-adapter-kedstxx6.js} +6 -6
- package/dist/{actions-zgcxfspj.js → actions-vget6r1k.js} +18 -16
- package/dist/{amp-adapter-jw9xfz42.js → amp-adapter-xeb5mktv.js} +2 -2
- package/dist/{app-4e1s4675.js → app-mqap8t8m.js} +3 -3
- package/dist/{assistant-v238n9dk.js → assistant-h0gv97mv.js} +20 -18
- package/dist/{boot-reembed-p666cw5n.js → boot-reembed-qr8a894z.js} +1 -1
- package/dist/{boot-reembed-w6pm64j4.js → boot-reembed-rj85jjhw.js} +1 -1
- package/dist/{boot-scrub-sweep-jcyjagn3.js → boot-scrub-sweep-eyfvn6v9.js} +3 -3
- package/dist/{boot-seeds-gbedmamp.js → boot-seeds-xs9a8dx1.js} +4 -4
- package/dist/{claude-adapter-q7wspzt8.js → claude-adapter-bm23cye7.js} +5 -5
- package/dist/{claude-managed-adapter-n7ahjyx6.js → claude-managed-adapter-1mwrsm9d.js} +2 -2
- package/dist/{claude-managed-setup-wza784fb.js → claude-managed-setup-gkfgks4j.js} +1 -1
- package/dist/{claude-sdk-session-yf400ptr.js → claude-sdk-session-apb4ypz3.js} +5 -5
- package/dist/cli-02vhhssk.js +848 -0
- package/dist/{cli-4ync7m60.js → cli-0ebey9hg.js} +1 -1
- package/dist/{cli-665j3feb.js → cli-0xk505e8.js} +1 -1
- package/dist/{cli-30m5q8mc.js → cli-1cevz46c.js} +5 -5
- package/dist/{cli-gzvb3d4a.js → cli-1jx0srjt.js} +3 -3
- package/dist/{cli-zj4d6afs.js → cli-1pa3rz69.js} +21 -6
- package/dist/{cli-fhvqt2fp.js → cli-2rwet3r7.js} +1 -1
- package/dist/{cli-azs9bhc5.js → cli-3wjs22x3.js} +2 -2
- package/dist/{cli-dxwh4j32.js → cli-4fhyq711.js} +45 -36
- package/dist/{cli-yv3n83x8.js → cli-5ff2sjyt.js} +1 -1
- package/dist/{cli-6vcyw6v7.js → cli-5s5hx57y.js} +5 -3
- package/dist/{cli-hz64bcnj.js → cli-6ftrrwm1.js} +9 -371
- package/dist/{cli-zes4tsbb.js → cli-8b9wvjps.js} +1 -1
- package/dist/cli-8cxkw1f5.js +382 -0
- package/dist/{cli-4s1kqrqg.js → cli-8rwvksbc.js} +2 -2
- package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
- package/dist/{cli-yv0vgscc.js → cli-a1esmmee.js} +13 -2
- package/dist/{cli-4hmaheb6.js → cli-avmqhatn.js} +7 -7
- package/dist/{cli-g6p7c2my.js → cli-b80a1100.js} +1 -1
- package/dist/{cli-tr24eeny.js → cli-bfc3k1x5.js} +3 -3
- package/dist/{cli-ka01m5rb.js → cli-bhwa8rzc.js} +13 -11
- package/dist/{cli-8cvvytad.js → cli-bn26xbcs.js} +2 -2
- package/dist/{cli-k28p6xh7.js → cli-c8fwpgqb.js} +1 -1
- package/dist/{cli-rc3r0bnh.js → cli-ccn0eybf.js} +1 -1
- package/dist/{cli-9td5ss65.js → cli-dsgj3e7a.js} +2 -2
- package/dist/{cli-a17tzmn0.js → cli-ecshf504.js} +2 -2
- package/dist/{cli-d43xqzbe.js → cli-eqgfs202.js} +1 -1
- package/dist/{cli-ppapy34w.js → cli-f3nx1h8z.js} +2 -2
- package/dist/{cli-ea7chd26.js → cli-ffbrkh1r.js} +74 -35
- package/dist/{cli-xgn4rwyv.js → cli-kk2r80n5.js} +4 -4
- package/dist/{cli-hn3pftj2.js → cli-kz116kvw.js} +3 -3
- package/dist/{cli-1v8w782f.js → cli-mvg2hf8p.js} +4 -4
- package/dist/{cli-5d1m1qca.js → cli-mz1ezzzh.js} +1 -1
- package/dist/{cli-jhmjwqcg.js → cli-nme7wery.js} +6 -6
- package/dist/{cli-65q70v2v.js → cli-nrrg0fvy.js} +18 -4
- package/dist/{cli-8jtrekpy.js → cli-p1m2b6gc.js} +7 -7
- package/dist/{cli-re4wkbzx.js → cli-rg7ntypm.js} +328 -47
- package/dist/{cli-fp4xbw07.js → cli-rsd7ga7v.js} +10 -8
- package/dist/{cli-etvn5ej2.js → cli-sppc0cmw.js} +1 -1
- package/dist/{cli-qrwp40st.js → cli-tg9ksbn8.js} +1 -1
- package/dist/{cli-6p7ddg72.js → cli-tgvpmnwz.js} +2 -2
- package/dist/{cli-q1812jbw.js → cli-tj4j2ts4.js} +6 -6
- package/dist/{cli-kqypty34.js → cli-ttaz6c7m.js} +2 -2
- package/dist/{cli-w7z94e6c.js → cli-tx00c0h6.js} +4 -2
- package/dist/{cli-4g629sgr.js → cli-wrv09v7q.js} +20 -1
- package/dist/{cli-cby329ny.js → cli-wyrr38xm.js} +2 -2
- package/dist/{cli-y3dvz6ge.js → cli-x1p5dhq2.js} +3 -3
- package/dist/{cli-8qsx817b.js → cli-x719j3vs.js} +219 -629
- package/dist/{cli-9e9kjqsj.js → cli-xfe63zx3.js} +1 -1
- package/dist/{cli-wtg6edvd.js → cli-ywyrffff.js} +1 -1
- package/dist/{cli-0rh20bp3.js → cli-zvze9rbm.js} +1 -1
- package/dist/cli.js +14 -14
- package/dist/{codex-adapter-nhjj2veb.js → codex-adapter-pp2gq8gr.js} +3 -3
- package/dist/{codex-hook-x6tm6y5m.js → codex-hook-qwzccd64.js} +1 -1
- package/dist/{codex-session-runner-6hggrjbx.js → codex-session-runner-0vzn09t8.js} +3 -3
- package/dist/{commands-mrw1ph2w.js → commands-psqhkttb.js} +2 -2
- package/dist/{controller-pt1zam5w.js → controller-t20vz2k8.js} +2 -2
- package/dist/{controllers-connect-ymw5541d.js → controllers-connect-gkxmqw9d.js} +24 -20
- package/dist/{cursor-adapter-cf4zh5x0.js → cursor-adapter-3v542qe5.js} +1 -1
- package/dist/{db-hy80219q.js → db-2xbyenjr.js} +2 -2
- package/dist/{db-hhpy6m4x.js → db-nc1rd869.js} +3 -2
- package/dist/{dispatcher-6pdjtkr1.js → dispatcher-ewceb8ts.js} +17 -15
- package/dist/{dsh-adapter-fbx06re2.js → dsh-adapter-75xwh78t.js} +1 -1
- package/dist/{easter-egg-3d-6hg2r942.js → easter-egg-3d-xsc47jwf.js} +1 -1
- package/dist/{execute-bcqc69a5.js → execute-gzmw1mnw.js} +1 -1
- package/dist/{external-channel-j6vb6ya1.js → external-channel-a5dsmxn4.js} +2 -2
- package/dist/{google-generative-ai-phpmwxvg.js → google-generative-ai-bmq8gkf3.js} +1 -1
- package/dist/{google-vertex-es9ybgdy.js → google-vertex-b1rvp52s.js} +1 -1
- package/dist/{grok-adapter-1193sprp.js → grok-adapter-vj88xr23.js} +7 -7
- package/dist/handlers-zbs90bny.js +39 -0
- package/dist/{heartbeat-h1w1ktce.js → heartbeat-t2m9r3wa.js} +20 -18
- package/dist/{hook-skrbja18.js → hook-8femx1bg.js} +4 -4
- package/dist/{hook-ejja4bpe.js → hook-xa677f8r.js} +5 -5
- package/dist/{http-mzh41mfj.js → http-xpmmq028.js} +868 -253
- package/dist/{index-vjn3wbzf.js → index-8pwc1jc8.js} +17 -15
- package/dist/{index-epr2gnbw.js → index-cdzyfrsp.js} +1 -1
- package/dist/{index-xmmkyx82.js → index-dg2tmj99.js} +17 -15
- package/dist/index-nxa6me0q.js +41 -0
- package/dist/{index-6rmnn89d.js → index-s7ckdajq.js} +9 -9
- package/dist/{index-wgsw9xcz.js → index-syztqdt6.js} +1 -1
- package/dist/{index-x0q2a4f9.js → index-ysd45kgc.js} +1 -1
- package/dist/{index-g909y089.js → index-zgvqzjsw.js} +8 -8
- package/dist/{keepalive-4djvbm2j.js → keepalive-3nr4st3r.js} +3 -3
- package/dist/{lead-61kqp604.js → lead-td8h2sfn.js} +33 -31
- package/dist/{maintenance-f3xhnjsm.js → maintenance-verwpe2c.js} +1 -1
- package/dist/{oauth-refresh-sweep-fpd6x6rw.js → oauth-refresh-sweep-9q2t0y3q.js} +2 -2
- package/dist/{onboard-196tjgyn.js → onboard-aytqedmq.js} +2 -2
- package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-f4hryjny.js} +4 -4
- package/dist/{otel-impl-g8fh4c21.js → otel-impl-hckw4wt1.js} +1 -1
- package/dist/{pi-mono-adapter-zasp3ze3.js → pi-mono-adapter-hxby0ce1.js} +6 -6
- package/dist/{pricing-refresh-sdcf5yaz.js → pricing-refresh-5nvme3sm.js} +3 -3
- package/dist/{rbac-roles-bsp5657v.js → rbac-roles-pd8c26m1.js} +5 -3
- package/dist/{rbac-roles-2x4g1wb5.js → rbac-roles-vbpy21mg.js} +2 -2
- package/dist/{render-v2-0pyb39bb.js → render-v2-7nqgvk2q.js} +3 -3
- package/dist/{resume-a86tkj6r.js → resume-b6djcj3c.js} +6 -6
- package/dist/{runtime-ar8310ba.js → runtime-txrxc9za.js} +1 -1
- package/dist/session-access-fc3f9y12.js +25 -0
- package/dist/{setup-ypbzcapd.js → setup-n39szm6b.js} +1 -1
- package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-wyp2k6z3.js} +2 -2
- package/dist/{work-objects-yjp8de12.js → work-objects-jem2fre0.js} +20 -18
- package/dist/{worker-4ptnn1ge.js → worker-wr6xgfr2.js} +33 -31
- package/openapi.json +844 -110
- package/package.json +1 -1
- package/src/apps/action-caller.ts +6 -0
- package/src/apps/script-types.ts +16 -3
- package/src/be/db/active-sessions.ts +5 -0
- package/src/be/db/session-visibility.ts +29 -0
- package/src/be/db/tasks/read.ts +61 -3
- package/src/be/db/tasks/write.ts +15 -6
- package/src/be/db.ts +92 -33
- package/src/be/events.ts +11 -0
- package/src/be/migrations/230_session_shares.sql +23 -0
- package/src/be/rbac-roles.ts +2 -0
- package/src/be/session-shares.ts +244 -0
- package/src/be/swarm-config-guard.ts +23 -2
- package/src/commands/runner.ts +11 -0
- package/src/http/active-sessions.ts +13 -2
- package/src/http/agents.ts +10 -3
- package/src/http/all-routes.ts +1 -0
- package/src/http/api-keys.ts +3 -1
- package/src/http/approval-requests.ts +2 -1
- package/src/http/apps.ts +36 -11
- package/src/http/config.ts +3 -0
- package/src/http/context.ts +2 -0
- package/src/http/core.ts +39 -2
- package/src/http/events.ts +6 -2
- package/src/http/favorite-owner.ts +1 -1
- package/src/http/fs.ts +38 -3
- package/src/http/index.ts +3 -1
- package/src/http/memory.ts +3 -1
- package/src/http/ranking.ts +367 -0
- package/src/http/realtime.ts +26 -21
- package/src/http/route-def.ts +77 -11
- package/src/http/script-runs.ts +2 -1
- package/src/http/scripts.ts +8 -0
- package/src/http/session-access.ts +179 -0
- package/src/http/session-data.ts +10 -2
- package/src/http/sessions.ts +212 -12
- package/src/http/stats.ts +7 -3
- package/src/http/tasks.ts +83 -7
- package/src/http/used-by.ts +11 -3
- package/src/prompts/session-templates.ts +3 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/ranking/telemetry-ranking.ts +147 -0
- package/src/ranking/usage.ts +57 -0
- package/src/rbac/admission.ts +22 -7
- package/src/rbac/legacy-policy.ts +2 -0
- package/src/rbac/permissions.ts +9 -0
- package/src/realtime/auth.ts +69 -0
- package/src/realtime/tickets.ts +4 -1
- package/src/realtime/transport.ts +77 -9
- package/src/slack/blocks.ts +18 -0
- package/src/slack/dm.ts +29 -0
- package/src/telemetry-contract/EVENTS.md +3 -0
- package/src/telemetry-contract/SOURCE.json +37 -4
- package/src/telemetry-contract/catalog.json +69 -0
- package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
- package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
- package/src/telemetry-contract/ranking/taxonomy.json +8 -0
- package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
- package/src/telemetry-contract/types.gen.ts +708 -1
- package/src/telemetry.ts +26 -2
- package/src/tests/apps-rbac.test.ts +189 -2
- package/src/tests/apps-script-types.test.ts +16 -3
- package/src/tests/check-pr-body.test.ts +23 -0
- package/src/tests/check-ranking-entry.test.ts +146 -0
- package/src/tests/db-extraction-contract.test.ts +1 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/org-identity.test.ts +44 -0
- package/src/tests/ranking-listing.test.ts +112 -0
- package/src/tests/ranking-route.test.ts +319 -0
- package/src/tests/ranking-usage.test.ts +50 -0
- package/src/tests/rbac-admission-e2e.test.ts +8 -0
- package/src/tests/rbac-admission.test.ts +38 -0
- package/src/tests/rbac-charact-http.test.ts +4 -3
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +2 -0
- package/src/tests/realtime-room-surfaces.test.ts +3 -1
- package/src/tests/realtime-session-join-race.test.ts +142 -0
- package/src/tests/realtime-tickets.test.ts +24 -17
- package/src/tests/realtime-transport.test.ts +91 -0
- package/src/tests/session-route-posture.test.ts +639 -0
- package/src/tests/session-sharing-routes.test.ts +282 -0
- package/src/tests/session-sharing.test.ts +526 -0
- package/src/tests/sessions-detail-batch.test.ts +7 -1
- package/src/tests/slack-tool-blocks.test.ts +101 -0
- package/src/tests/slack-update.test.ts +55 -0
- package/src/tests/slack-user-dm.test.ts +177 -0
- package/src/tests/used-by-route.test.ts +10 -0
- package/src/tools/slack-post.ts +18 -12
- package/src/tools/slack-read.ts +13 -1
- package/src/tools/slack-reply.ts +7 -10
- package/src/tools/slack-start-thread.ts +18 -11
- package/src/tools/slack-update.ts +2 -9
- package/src/types.ts +35 -0
- package/src/utils/org-identity.ts +33 -0
- package/templates/skills/apps/SKILL.md +17 -0
- package/templates/skills/apps/content.md +17 -0
- package/dist/handlers-tb3xgjnm.js +0 -37
- package/dist/index-nyfcdpky.js +0 -39
- package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
- /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", () => {
|
package/src/tools/slack-post.ts
CHANGED
|
@@ -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
|
|
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) {
|
package/src/tools/slack-read.ts
CHANGED
|
@@ -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(
|
|
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), {
|
package/src/tools/slack-reply.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
};
|