@desplega.ai/agent-swarm 1.78.1 → 1.79.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 (75) hide show
  1. package/README.md +1 -0
  2. package/openapi.json +1335 -236
  3. package/package.json +4 -4
  4. package/plugin/skills/artifacts/SKILL.md +151 -0
  5. package/plugin/skills/artifacts/examples/static-report.sh +1 -1
  6. package/plugin/skills/kv-storage/SKILL.md +168 -0
  7. package/plugin/skills/pages/SKILL.md +423 -0
  8. package/src/artifact-sdk/browser-sdk.ts +396 -19
  9. package/src/be/db.ts +548 -0
  10. package/src/be/migrations/059_pages.sql +34 -0
  11. package/src/be/migrations/060_page_versions.sql +19 -0
  12. package/src/be/migrations/061_kv_store.sql +34 -0
  13. package/src/be/migrations/062_pages_view_count.sql +9 -0
  14. package/src/commands/artifact.ts +17 -11
  15. package/src/commands/provider-credentials.ts +1 -1
  16. package/src/http/index.ts +9 -1
  17. package/src/http/kv.ts +658 -0
  18. package/src/http/page-proxy.ts +213 -0
  19. package/src/http/pages-public.ts +507 -0
  20. package/src/http/pages.ts +608 -0
  21. package/src/http/status.ts +1 -1
  22. package/src/http/utils.ts +68 -5
  23. package/src/pages/version.ts +44 -0
  24. package/src/prompts/session-templates.ts +51 -0
  25. package/src/providers/pi-mono-adapter.ts +3 -3
  26. package/src/providers/pi-mono-extension.ts +1 -1
  27. package/src/server.ts +29 -1
  28. package/src/tasks/context-key.ts +28 -0
  29. package/src/telemetry.ts +65 -1
  30. package/src/tests/artifact-commands.test.ts +92 -0
  31. package/src/tests/artifact-sdk.test.ts +80 -74
  32. package/src/tests/context-key.test.ts +17 -0
  33. package/src/tests/create-page-tool.test.ts +197 -0
  34. package/src/tests/fixtures/sample-json-page.json +52 -0
  35. package/src/tests/kv-http.test.ts +331 -0
  36. package/src/tests/kv-namespace-resolution.test.ts +172 -0
  37. package/src/tests/kv-page-proxy.test.ts +212 -0
  38. package/src/tests/kv-storage.test.ts +227 -0
  39. package/src/tests/kv-tool.test.ts +217 -0
  40. package/src/tests/launch-password-rejection.test.ts +139 -0
  41. package/src/tests/page-proxy-authed.test.ts +146 -0
  42. package/src/tests/page-proxy.test.ts +270 -0
  43. package/src/tests/page-session.test.ts +169 -0
  44. package/src/tests/pages-actions-endpoint.test.ts +102 -0
  45. package/src/tests/pages-authed-mode.test.ts +211 -0
  46. package/src/tests/pages-http.test.ts +193 -0
  47. package/src/tests/pages-list-endpoint.test.ts +149 -0
  48. package/src/tests/pages-password-hash.test.ts +57 -0
  49. package/src/tests/pages-password-mode.test.ts +265 -0
  50. package/src/tests/pages-public-authed-401.test.ts +102 -0
  51. package/src/tests/pages-public-html.test.ts +151 -0
  52. package/src/tests/pages-public-json-redirect.test.ts +86 -0
  53. package/src/tests/pages-storage.test.ts +196 -0
  54. package/src/tests/pages-versioning.test.ts +231 -0
  55. package/src/tests/pages-view-count.test.ts +220 -0
  56. package/src/tests/prompt-template-session.test.ts +3 -2
  57. package/src/tests/skill-update-scope.test.ts +165 -0
  58. package/src/tests/swarm-diff.test.ts +303 -0
  59. package/src/tests/telemetry-init.test.ts +149 -0
  60. package/src/tests/workflow-wait-event.test.ts +4 -7
  61. package/src/tools/create-page.ts +263 -0
  62. package/src/tools/kv/index.ts +5 -0
  63. package/src/tools/kv/kv-delete.ts +89 -0
  64. package/src/tools/kv/kv-get.ts +64 -0
  65. package/src/tools/kv/kv-incr.ts +116 -0
  66. package/src/tools/kv/kv-list.ts +81 -0
  67. package/src/tools/kv/kv-set.ts +194 -0
  68. package/src/tools/kv/resolve-namespace.ts +58 -0
  69. package/src/tools/skills/skill-update.ts +26 -0
  70. package/src/tools/tool-config.ts +10 -0
  71. package/src/types.ts +107 -0
  72. package/src/utils/internal-ai/complete-structured.ts +2 -2
  73. package/src/utils/internal-ai/credentials.ts +3 -3
  74. package/src/utils/page-session.ts +254 -0
  75. package/plugin/skills/artifacts/skill.md +0 -70
@@ -64,48 +64,48 @@ describe("BROWSER_SDK_JS", () => {
64
64
  expect(BROWSER_SDK_JS).toContain("class SwarmSDK");
65
65
  });
66
66
 
67
- test("contains all expected API methods", () => {
68
- const expectedMethods = [
69
- "createTask",
70
- "getTasks",
71
- "getTaskDetails",
72
- "storeProgress",
73
- "postMessage",
74
- "readMessages",
75
- "getSwarm",
76
- "listServices",
77
- "slackReply",
67
+ test("exposes the seven canonical domains", () => {
68
+ const expectedDomains = [
69
+ "this.tasks",
70
+ "this.agents",
71
+ "this.events",
72
+ "this.memory",
73
+ "this.repos",
74
+ "this.schedules",
75
+ "this.approvalRequests",
78
76
  ];
79
- for (const method of expectedMethods) {
80
- expect(BROWSER_SDK_JS).toContain(method);
77
+ for (const domain of expectedDomains) {
78
+ expect(BROWSER_SDK_JS).toContain(domain);
81
79
  }
82
80
  });
83
81
 
84
- test("assigns SwarmSDK to window", () => {
82
+ test("removed domains are NOT exposed (messages, services, slack)", () => {
83
+ const removed = ["this.messages", "this.services", "this.slack", "postMessage", "readMessages"];
84
+ for (const r of removed) {
85
+ expect(BROWSER_SDK_JS).not.toContain(r);
86
+ }
87
+ });
88
+
89
+ test("assigns SwarmSDK class + swarmSdk singleton to window", () => {
85
90
  expect(BROWSER_SDK_JS).toContain("window.SwarmSDK = SwarmSDK");
91
+ expect(BROWSER_SDK_JS).toContain("window.swarmSdk = new SwarmSDK()");
86
92
  });
87
93
 
88
- test("uses correct proxy API paths", () => {
89
- expect(BROWSER_SDK_JS).toContain("/@swarm/api/tasks");
90
- expect(BROWSER_SDK_JS).toContain("/@swarm/api/agents");
91
- expect(BROWSER_SDK_JS).toContain("/@swarm/api/messages");
92
- expect(BROWSER_SDK_JS).toContain("/@swarm/api/services");
93
- expect(BROWSER_SDK_JS).toContain("/@swarm/api/slack/reply");
94
+ test("routes calls through the /@swarm/api/* proxy", () => {
95
+ expect(BROWSER_SDK_JS).toContain("const base = '/@swarm/api'");
96
+ // Sentinel endpoints — one per domain.
97
+ expect(BROWSER_SDK_JS).toContain("'/tasks'");
98
+ expect(BROWSER_SDK_JS).toContain("'/agents'");
99
+ expect(BROWSER_SDK_JS).toContain("'/events'");
100
+ expect(BROWSER_SDK_JS).toContain("'/memory/search'");
101
+ expect(BROWSER_SDK_JS).toContain("'/repos'");
102
+ expect(BROWSER_SDK_JS).toContain("'/schedules'");
103
+ expect(BROWSER_SDK_JS).toContain("'/approval-requests'");
94
104
  });
95
105
 
96
106
  test("fetches config on construction", () => {
97
107
  expect(BROWSER_SDK_JS).toContain("fetch('/@swarm/config')");
98
108
  });
99
-
100
- test("has _post helper with JSON content-type", () => {
101
- expect(BROWSER_SDK_JS).toContain("_post(url, body)");
102
- expect(BROWSER_SDK_JS).toContain("'Content-Type': 'application/json'");
103
- expect(BROWSER_SDK_JS).toContain("JSON.stringify(body)");
104
- });
105
-
106
- test("has _get helper", () => {
107
- expect(BROWSER_SDK_JS).toContain("_get(url)");
108
- });
109
109
  });
110
110
 
111
111
  // ─── createArtifactServer factory tests ──────────────────────────────────
@@ -536,27 +536,29 @@ describe("artifact CLI command", () => {
536
536
  const url = new URL(req.url);
537
537
  if (url.pathname === "/api/services") {
538
538
  return new Response(
539
- JSON.stringify([
540
- {
541
- id: "svc-1",
542
- name: "artifact-dashboard",
543
- agentId: "agent-123",
544
- status: "healthy",
545
- metadata: {
546
- type: "artifact",
547
- artifactName: "dashboard",
548
- port: 3001,
549
- publicUrl: "https://test.lt.example.com",
539
+ JSON.stringify({
540
+ services: [
541
+ {
542
+ id: "svc-1",
543
+ name: "artifact-dashboard",
544
+ agentId: "agent-123",
545
+ status: "healthy",
546
+ metadata: {
547
+ type: "artifact",
548
+ artifactName: "dashboard",
549
+ port: 3001,
550
+ publicUrl: "https://test.lt.example.com",
551
+ },
550
552
  },
551
- },
552
- {
553
- id: "svc-2",
554
- name: "some-other-service",
555
- agentId: "agent-456",
556
- status: "healthy",
557
- metadata: { type: "web" },
558
- },
559
- ]),
553
+ {
554
+ id: "svc-2",
555
+ name: "some-other-service",
556
+ agentId: "agent-456",
557
+ status: "healthy",
558
+ metadata: { type: "web" },
559
+ },
560
+ ],
561
+ }),
560
562
  );
561
563
  }
562
564
  return new Response("Not found", { status: 404 });
@@ -593,7 +595,7 @@ describe("artifact CLI command", () => {
593
595
  const mockPort = await getAvailablePort();
594
596
  const mockServer = Bun.serve({
595
597
  port: mockPort,
596
- fetch: () => new Response(JSON.stringify([])),
598
+ fetch: () => new Response(JSON.stringify({ services: [] })),
597
599
  });
598
600
 
599
601
  const origEnv = { ...process.env };
@@ -626,22 +628,24 @@ describe("artifact CLI command", () => {
626
628
  port: mockPort,
627
629
  fetch: () =>
628
630
  new Response(
629
- JSON.stringify([
630
- {
631
- id: "s1",
632
- name: "web-server",
633
- agentId: "a1",
634
- status: "healthy",
635
- metadata: { type: "web" },
636
- },
637
- {
638
- id: "s2",
639
- name: "api-server",
640
- agentId: "a2",
641
- status: "healthy",
642
- metadata: {},
643
- },
644
- ]),
631
+ JSON.stringify({
632
+ services: [
633
+ {
634
+ id: "s1",
635
+ name: "web-server",
636
+ agentId: "a1",
637
+ status: "healthy",
638
+ metadata: { type: "web" },
639
+ },
640
+ {
641
+ id: "s2",
642
+ name: "api-server",
643
+ agentId: "a2",
644
+ status: "healthy",
645
+ metadata: {},
646
+ },
647
+ ],
648
+ }),
645
649
  ),
646
650
  });
647
651
 
@@ -682,13 +686,15 @@ describe("artifact CLI command", () => {
682
686
  const url = new URL(req.url);
683
687
  if (req.method === "GET" && url.pathname === "/api/services") {
684
688
  return new Response(
685
- JSON.stringify([
686
- {
687
- id: "svc-to-delete",
688
- name: "artifact-my-report",
689
- metadata: { type: "artifact", artifactName: "my-report" },
690
- },
691
- ]),
689
+ JSON.stringify({
690
+ services: [
691
+ {
692
+ id: "svc-to-delete",
693
+ name: "artifact-my-report",
694
+ metadata: { type: "artifact", artifactName: "my-report" },
695
+ },
696
+ ],
697
+ }),
692
698
  );
693
699
  }
694
700
  if (req.method === "DELETE" && url.pathname.startsWith("/api/services/")) {
@@ -1,10 +1,12 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
+ agentContextKey,
3
4
  agentmailContextKey,
4
5
  buildJiraContextKey,
5
6
  githubContextKey,
6
7
  gitlabContextKey,
7
8
  linearContextKey,
9
+ pageContextKey,
8
10
  parseContextKey,
9
11
  scheduleContextKey,
10
12
  slackContextKey,
@@ -49,6 +51,21 @@ describe("context-key builders", () => {
49
51
  test("workflowContextKey builds expected format", () => {
50
52
  expect(workflowContextKey({ workflowRunId: "run-uuid" })).toBe("task:workflow:run-uuid");
51
53
  });
54
+
55
+ test("agentContextKey builds expected format", () => {
56
+ expect(agentContextKey({ agentId: "agent-uuid" })).toBe("task:agent:agent-uuid");
57
+ });
58
+
59
+ test("pageContextKey builds expected format", () => {
60
+ expect(pageContextKey({ pageId: "page-uuid" })).toBe("task:page:page-uuid");
61
+ });
62
+
63
+ test("agentContextKey and pageContextKey reject empty / colon-bearing inputs", () => {
64
+ expect(() => agentContextKey({ agentId: "" })).toThrow(/non-empty/);
65
+ expect(() => agentContextKey({ agentId: "a:b" })).toThrow(/must not contain/);
66
+ expect(() => pageContextKey({ pageId: "" })).toThrow(/non-empty/);
67
+ expect(() => pageContextKey({ pageId: "p:q" })).toThrow(/must not contain/);
68
+ });
52
69
  });
53
70
 
54
71
  describe("context-key separator safety", () => {
@@ -0,0 +1,197 @@
1
+ /**
2
+ * `create_page` MCP tool — unit-level coverage. Registers the tool against
3
+ * a fresh `McpServer`, pulls the handler out of the SDK's registry, and
4
+ * invokes it directly with a stubbed agent-id `requestInfo`.
5
+ *
6
+ * Verifies:
7
+ * - first-call path: creates a row in `pages`, returns `{id, version=1, app_url, api_url}`
8
+ * - upsert path: second call with the same slug bumps the edit-counter
9
+ * and writes a version row
10
+ * - capability gate: tool is registered when `CAPABILITIES` contains
11
+ * `pages`, NOT registered when missing (verified via `createServer`)
12
+ */
13
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
14
+ import crypto from "node:crypto";
15
+ import { unlink } from "node:fs/promises";
16
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
+ import { closeDb, getPageBySlug, getPageVersions, initDb } from "../be/db";
18
+ import { registerCreatePageTool } from "../tools/create-page";
19
+
20
+ const TEST_DB_PATH = "./test-create-page-tool.sqlite";
21
+
22
+ type RegisteredTool = {
23
+ handler: (args: unknown, extra: unknown) => Promise<unknown>;
24
+ };
25
+
26
+ function buildServer() {
27
+ const server = new McpServer({ name: "create-page-test", version: "1.0.0" });
28
+ registerCreatePageTool(server);
29
+ const registered = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
30
+ ._registeredTools;
31
+ const tool = registered.create_page;
32
+ if (!tool) throw new Error("create_page tool not registered");
33
+ return tool;
34
+ }
35
+
36
+ describe("create_page MCP tool", () => {
37
+ const agentId = crypto.randomUUID();
38
+ const fakeMeta = {
39
+ sessionId: "session-1",
40
+ requestInfo: { headers: { "x-agent-id": agentId } },
41
+ };
42
+
43
+ beforeAll(async () => {
44
+ for (const suffix of ["", "-wal", "-shm"]) {
45
+ try {
46
+ await unlink(`${TEST_DB_PATH}${suffix}`);
47
+ } catch {}
48
+ }
49
+ initDb(TEST_DB_PATH);
50
+ // The tool reads MCP_BASE_URL / APP_URL when building share URLs.
51
+ process.env.MCP_BASE_URL = "http://test-api:9999";
52
+ process.env.APP_URL = "http://test-app:5274";
53
+ });
54
+
55
+ afterAll(async () => {
56
+ closeDb();
57
+ delete process.env.MCP_BASE_URL;
58
+ delete process.env.APP_URL;
59
+ for (const suffix of ["", "-wal", "-shm"]) {
60
+ try {
61
+ await unlink(`${TEST_DB_PATH}${suffix}`);
62
+ } catch {}
63
+ }
64
+ });
65
+
66
+ test("first call creates a row + returns shareable URLs", async () => {
67
+ const tool = buildServer();
68
+ const result = (await tool.handler(
69
+ {
70
+ title: "Hello Page",
71
+ body: "<h1>hello</h1>",
72
+ contentType: "text/html",
73
+ authMode: "public",
74
+ },
75
+ fakeMeta,
76
+ )) as {
77
+ structuredContent: {
78
+ id: string;
79
+ version: number;
80
+ app_url: string;
81
+ api_url: string;
82
+ yourAgentId: string;
83
+ };
84
+ };
85
+
86
+ expect(result.structuredContent.id).toMatch(/^[0-9a-f]{32}$/);
87
+ expect(result.structuredContent.version).toBe(1);
88
+ expect(result.structuredContent.api_url).toBe(
89
+ `http://test-api:9999/p/${result.structuredContent.id}`,
90
+ );
91
+ expect(result.structuredContent.app_url).toBe(
92
+ `http://test-app:5274/pages/${result.structuredContent.id}`,
93
+ );
94
+ expect(result.structuredContent.yourAgentId).toBe(agentId);
95
+
96
+ // DB row exists with the auto-slug from the title.
97
+ const row = getPageBySlug(agentId, "hello-page");
98
+ expect(row).not.toBeNull();
99
+ expect(row!.body).toBe("<h1>hello</h1>");
100
+ });
101
+
102
+ test("re-running with the same slug upserts + bumps edit-counter", async () => {
103
+ const tool = buildServer();
104
+
105
+ const first = (await tool.handler(
106
+ {
107
+ title: "Upsert Page",
108
+ slug: "upsert",
109
+ body: "v0",
110
+ contentType: "text/html",
111
+ authMode: "public",
112
+ },
113
+ fakeMeta,
114
+ )) as { structuredContent: { id: string; version: number } };
115
+ expect(first.structuredContent.version).toBe(1);
116
+
117
+ const second = (await tool.handler(
118
+ {
119
+ title: "Upsert Page",
120
+ slug: "upsert",
121
+ body: "v1",
122
+ contentType: "text/html",
123
+ authMode: "public",
124
+ },
125
+ fakeMeta,
126
+ )) as { structuredContent: { id: string; version: number } };
127
+ expect(second.structuredContent.id).toBe(first.structuredContent.id);
128
+ expect(second.structuredContent.version).toBe(2);
129
+
130
+ // Version row holds the PRE-update body.
131
+ const versions = getPageVersions(first.structuredContent.id);
132
+ expect(versions).toHaveLength(1);
133
+ expect(versions[0]!.snapshot.body).toBe("v0");
134
+
135
+ // Parent now holds the new body.
136
+ const row = getPageBySlug(agentId, "upsert");
137
+ expect(row?.body).toBe("v1");
138
+ });
139
+
140
+ test("missing X-Agent-ID returns an error result", async () => {
141
+ const tool = buildServer();
142
+ const result = (await tool.handler(
143
+ {
144
+ title: "Anon",
145
+ body: "x",
146
+ contentType: "text/html",
147
+ authMode: "public",
148
+ },
149
+ { sessionId: "s", requestInfo: { headers: {} } },
150
+ )) as { isError?: boolean };
151
+ expect(result.isError).toBe(true);
152
+ });
153
+
154
+ test("password is hashed (not stored verbatim)", async () => {
155
+ const tool = buildServer();
156
+ await tool.handler(
157
+ {
158
+ title: "Pw",
159
+ slug: "pw-tool",
160
+ body: "secret",
161
+ contentType: "text/html",
162
+ authMode: "password",
163
+ password: "open-sesame",
164
+ },
165
+ fakeMeta,
166
+ );
167
+ const row = getPageBySlug(agentId, "pw-tool");
168
+ expect(row?.passwordHash).toBeDefined();
169
+ expect(row?.passwordHash).not.toBe("open-sesame");
170
+ expect(await Bun.password.verify("open-sesame", row!.passwordHash!)).toBe(true);
171
+ });
172
+ });
173
+
174
+ describe("create_page MCP tool capability gating", () => {
175
+ test("not registered without 'pages' capability; registered with it", async () => {
176
+ // Save + clear env then load the server module fresh.
177
+ const orig = process.env.CAPABILITIES;
178
+ try {
179
+ // Default capabilities don't include 'pages' (step-3 enforced).
180
+ process.env.CAPABILITIES = "core,task-pool,profiles,services,scheduling,memory,workflows";
181
+ // Force a fresh module evaluation so the capability check re-runs.
182
+ delete require.cache[require.resolve("../server")];
183
+ const without = await import("../server");
184
+ expect(without.hasCapability("pages")).toBe(false);
185
+
186
+ process.env.CAPABILITIES =
187
+ "core,task-pool,profiles,services,scheduling,memory,workflows,pages";
188
+ delete require.cache[require.resolve("../server")];
189
+ const withPages = await import("../server");
190
+ expect(withPages.hasCapability("pages")).toBe(true);
191
+ } finally {
192
+ if (orig === undefined) delete process.env.CAPABILITIES;
193
+ else process.env.CAPABILITIES = orig;
194
+ delete require.cache[require.resolve("../server")];
195
+ }
196
+ });
197
+ });
@@ -0,0 +1,52 @@
1
+ {
2
+ "root": "root",
3
+ "elements": {
4
+ "root": {
5
+ "type": "Container",
6
+ "props": { "direction": "column", "gap": "md" },
7
+ "children": ["heading", "intro", "sdkBtn", "callBtn"]
8
+ },
9
+ "heading": {
10
+ "type": "Heading",
11
+ "props": { "text": "Sample JSON page", "level": "h1" },
12
+ "children": []
13
+ },
14
+ "intro": {
15
+ "type": "Text",
16
+ "props": {
17
+ "content": "Two buttons below dispatch swarm.sdk and swarm.call actions. Click each to exercise the renderer's action wiring.",
18
+ "tone": "muted"
19
+ },
20
+ "children": []
21
+ },
22
+ "sdkBtn": {
23
+ "type": "Button",
24
+ "props": { "label": "Create task via SDK" },
25
+ "children": [],
26
+ "on": {
27
+ "press": {
28
+ "action": "swarm.sdk",
29
+ "params": {
30
+ "sdk": "createTask",
31
+ "args": { "description": "from-json-page" }
32
+ }
33
+ }
34
+ }
35
+ },
36
+ "callBtn": {
37
+ "type": "Button",
38
+ "props": { "label": "Create channel via raw call", "variant": "secondary" },
39
+ "children": [],
40
+ "on": {
41
+ "press": {
42
+ "action": "swarm.call",
43
+ "params": {
44
+ "method": "POST",
45
+ "endpoint": "/api/channels",
46
+ "body": { "name": "from-json-page" }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }