@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
@@ -0,0 +1,213 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { getPage } from "../be/db";
3
+ import { extractAndVerifyCookie } from "../utils/page-session";
4
+ import { route } from "./route-def";
5
+ import { jsonError } from "./utils";
6
+
7
+ /**
8
+ * `/@swarm/api/*` proxy. Cookie-gated equivalent of the artifact-sdk proxy
9
+ * (src/artifact-sdk/server.ts:42-69), but lives on the MAIN API server and
10
+ * authenticates via the page-session cookie instead of basic-auth + a per-
11
+ * artifact tunnel.
12
+ *
13
+ * Flow:
14
+ * 1. Browser hits `/@swarm/api/<rest>` from inside an iframe of `/p/:id`.
15
+ * 2. We parse the `page_session` cookie, verify HMAC + expiry.
16
+ * 3. Look up the page; map `agentId` → `X-Agent-ID`.
17
+ * 4. Re-issue the request to the same API server's `/api/<rest>` with
18
+ * `Authorization: Bearer ${API_KEY}` and `X-Agent-ID: ${page.agentId}`
19
+ * injected server-side. The bearer NEVER touches the browser.
20
+ *
21
+ * Cookie IS the auth — this route opts out of the global bearer gate via
22
+ * `route({ auth: { apiKey: false } })`. Unknown paths fail closed (the
23
+ * `isPublicRoute` check defaults to bearer-required), so we must declare the
24
+ * route here even though we don't use route().match() for the actual
25
+ * dispatch (we use a startsWith check below — segment-based matching gets
26
+ * unwieldy for an arbitrary suffix).
27
+ */
28
+
29
+ // Registered purely so the global bearer-gate (`isPublicRoute`) skips the
30
+ // API-key check for /@swarm/api/* requests. The handler below does its own
31
+ // cookie validation. The path pattern uses a single dynamic segment as a
32
+ // stand-in; the actual route accepts ANY suffix and is matched manually via
33
+ // `req.url.startsWith("/@swarm/api/")` for clarity.
34
+ route({
35
+ method: "get",
36
+ path: "/@swarm/api/{path}",
37
+ pattern: ["@swarm", "api", null],
38
+ exact: false,
39
+ summary: "Cookie-gated proxy to the swarm API (used by db-backed page iframes)",
40
+ tags: ["Pages"],
41
+ responses: {
42
+ 200: { description: "Proxied response from the underlying /api/* endpoint" },
43
+ 401: { description: "No or invalid page-session cookie" },
44
+ 404: { description: "Page referenced by the cookie no longer exists" },
45
+ },
46
+ auth: { apiKey: false },
47
+ });
48
+ route({
49
+ method: "post",
50
+ path: "/@swarm/api/{path}",
51
+ pattern: ["@swarm", "api", null],
52
+ exact: false,
53
+ summary: "Cookie-gated proxy to the swarm API (POST)",
54
+ tags: ["Pages"],
55
+ responses: {
56
+ 200: { description: "Proxied response" },
57
+ 401: { description: "No or invalid page-session cookie" },
58
+ },
59
+ auth: { apiKey: false },
60
+ });
61
+ route({
62
+ method: "put",
63
+ path: "/@swarm/api/{path}",
64
+ pattern: ["@swarm", "api", null],
65
+ exact: false,
66
+ summary: "Cookie-gated proxy to the swarm API (PUT)",
67
+ tags: ["Pages"],
68
+ responses: {
69
+ 200: { description: "Proxied response" },
70
+ 401: { description: "No or invalid page-session cookie" },
71
+ },
72
+ auth: { apiKey: false },
73
+ });
74
+ route({
75
+ method: "delete",
76
+ path: "/@swarm/api/{path}",
77
+ pattern: ["@swarm", "api", null],
78
+ exact: false,
79
+ summary: "Cookie-gated proxy to the swarm API (DELETE)",
80
+ tags: ["Pages"],
81
+ responses: {
82
+ 200: { description: "Proxied response" },
83
+ 401: { description: "No or invalid page-session cookie" },
84
+ },
85
+ auth: { apiKey: false },
86
+ });
87
+ route({
88
+ method: "patch",
89
+ path: "/@swarm/api/{path}",
90
+ pattern: ["@swarm", "api", null],
91
+ exact: false,
92
+ summary: "Cookie-gated proxy to the swarm API (PATCH)",
93
+ tags: ["Pages"],
94
+ responses: {
95
+ 200: { description: "Proxied response" },
96
+ 401: { description: "No or invalid page-session cookie" },
97
+ },
98
+ auth: { apiKey: false },
99
+ });
100
+
101
+ const PROXY_PREFIX = "/@swarm/api/";
102
+
103
+ /**
104
+ * Handle `/@swarm/api/*` requests. Returns `true` if the request was handled
105
+ * (response sent), `false` if not — caller continues to the next handler.
106
+ *
107
+ * Place BEFORE `handlePages` in the central `handlers` array — `/@swarm/api/*`
108
+ * does not overlap `/api/pages/*` segment-wise (different first segment), but
109
+ * we keep the ordering explicit.
110
+ */
111
+ export async function handlePageProxy(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
112
+ const url = req.url ?? "";
113
+ if (!url.startsWith(PROXY_PREFIX)) return false;
114
+
115
+ // Strip query string for cookie/path checks, but preserve it for the
116
+ // forwarded URL so callers like `/me?include=inbox` still work.
117
+ const queryIdx = url.indexOf("?");
118
+ const pathPart = queryIdx === -1 ? url : url.slice(0, queryIdx);
119
+ const queryPart = queryIdx === -1 ? "" : url.slice(queryIdx);
120
+
121
+ // ─── Cookie validation ────────────────────────────────────────────────────
122
+ const payload = await extractAndVerifyCookie(req);
123
+ if (!payload) {
124
+ jsonError(res, "no page session", 401);
125
+ return true;
126
+ }
127
+
128
+ const page = getPage(payload.pageId);
129
+ if (!page) {
130
+ // Cookie was issued before the page was deleted. Treat as a stale session
131
+ // rather than 404 so the client knows to refresh / re-launch.
132
+ jsonError(res, "page session no longer valid", 401);
133
+ return true;
134
+ }
135
+
136
+ // ─── Rewrite + forward ─────────────────────────────────────────────────────
137
+ // `/@swarm/api/me` → `/api/me`. Preserve query string.
138
+ //
139
+ // This is an IN-PROCESS proxy — we always dispatch to the same server we're
140
+ // running in. We deliberately do NOT use `deriveApiBaseUrl(req)`: that would
141
+ // honour `MCP_BASE_URL` (which may be an ngrok tunnel or other external host
142
+ // pointing back at us), forcing a network round-trip through the public
143
+ // surface and breaking when `localhost:<PORT>` is reachable but the public
144
+ // URL isn't (test envs, offline dev, restrictive networks).
145
+ const rewrittenPath = pathPart.replace(PROXY_PREFIX, "/api/");
146
+ const port = process.env.PORT || "3013";
147
+ const baseUrl = `http://127.0.0.1:${port}`;
148
+ const targetUrl = `${baseUrl}${rewrittenPath}${queryPart}`;
149
+
150
+ const apiKey = process.env.API_KEY ?? "";
151
+ // `X-Page-Id` is the trust anchor for page-scoped KV: only the page-proxy
152
+ // ever sets it (any external `X-Page-Id` header is dropped because we don't
153
+ // forward the original headers). The KV handler treats this as the highest-
154
+ // priority namespace source so a page can't escape `task:page:<own>`.
155
+ const headers: Record<string, string> = {
156
+ Authorization: `Bearer ${apiKey}`,
157
+ "X-Agent-ID": page.agentId,
158
+ "X-Page-Id": page.id,
159
+ };
160
+
161
+ // Forward content-type / accept verbatim for non-GET so JSON bodies work.
162
+ const reqContentType = req.headers["content-type"];
163
+ if (reqContentType) {
164
+ headers["Content-Type"] = Array.isArray(reqContentType) ? reqContentType[0]! : reqContentType;
165
+ }
166
+ const reqAccept = req.headers.accept;
167
+ if (reqAccept) {
168
+ headers.Accept = Array.isArray(reqAccept) ? reqAccept[0]! : reqAccept;
169
+ }
170
+
171
+ // Pull the body if there is one. We DO buffer here — page traffic is
172
+ // expected to be small JSON payloads, and streaming would complicate cookie
173
+ // failure-mode handling.
174
+ const method = (req.method ?? "GET").toUpperCase();
175
+ let body: Buffer | undefined;
176
+ if (method !== "GET" && method !== "HEAD") {
177
+ const chunks: Buffer[] = [];
178
+ for await (const chunk of req) chunks.push(chunk as Buffer);
179
+ if (chunks.length > 0) body = Buffer.concat(chunks);
180
+ }
181
+
182
+ let upstream: Response;
183
+ try {
184
+ upstream = await fetch(targetUrl, {
185
+ method,
186
+ headers,
187
+ body,
188
+ // Prevent the runtime from following redirects — surface them to the
189
+ // caller as-is so the SDK sees the same response it would direct-fetching.
190
+ redirect: "manual",
191
+ });
192
+ } catch (err) {
193
+ // Don't echo the underlying error to the client (could leak target URL
194
+ // shape under some env-var typos). Log to server, send generic 502.
195
+ console.error("[page-proxy] upstream fetch failed:", err);
196
+ jsonError(res, "upstream error", 502);
197
+ return true;
198
+ }
199
+
200
+ // Pipe upstream response back. Preserve status + content-type.
201
+ const upstreamCt = upstream.headers.get("content-type");
202
+ if (upstreamCt) res.setHeader("Content-Type", upstreamCt);
203
+
204
+ // Copy a small allowlist of useful headers. Don't blanket-forward — upstream
205
+ // may include `Set-Cookie` we don't want to re-emit, etc.
206
+ const cacheControl = upstream.headers.get("cache-control");
207
+ if (cacheControl) res.setHeader("Cache-Control", cacheControl);
208
+
209
+ res.writeHead(upstream.status);
210
+ const buf = Buffer.from(await upstream.arrayBuffer());
211
+ res.end(buf);
212
+ return true;
213
+ }