@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.
- package/README.md +1 -0
- package/openapi.json +1335 -236
- package/package.json +4 -4
- package/plugin/skills/artifacts/SKILL.md +151 -0
- package/plugin/skills/artifacts/examples/static-report.sh +1 -1
- package/plugin/skills/kv-storage/SKILL.md +168 -0
- package/plugin/skills/pages/SKILL.md +423 -0
- package/src/artifact-sdk/browser-sdk.ts +396 -19
- package/src/be/db.ts +548 -0
- package/src/be/migrations/059_pages.sql +34 -0
- package/src/be/migrations/060_page_versions.sql +19 -0
- package/src/be/migrations/061_kv_store.sql +34 -0
- package/src/be/migrations/062_pages_view_count.sql +9 -0
- package/src/commands/artifact.ts +17 -11
- package/src/commands/provider-credentials.ts +1 -1
- package/src/http/index.ts +9 -1
- package/src/http/kv.ts +658 -0
- package/src/http/page-proxy.ts +213 -0
- package/src/http/pages-public.ts +507 -0
- package/src/http/pages.ts +608 -0
- package/src/http/status.ts +1 -1
- package/src/http/utils.ts +68 -5
- package/src/pages/version.ts +44 -0
- package/src/prompts/session-templates.ts +51 -0
- package/src/providers/pi-mono-adapter.ts +3 -3
- package/src/providers/pi-mono-extension.ts +1 -1
- package/src/server.ts +29 -1
- package/src/tasks/context-key.ts +28 -0
- package/src/telemetry.ts +65 -1
- package/src/tests/artifact-commands.test.ts +92 -0
- package/src/tests/artifact-sdk.test.ts +80 -74
- package/src/tests/context-key.test.ts +17 -0
- package/src/tests/create-page-tool.test.ts +197 -0
- package/src/tests/fixtures/sample-json-page.json +52 -0
- package/src/tests/kv-http.test.ts +331 -0
- package/src/tests/kv-namespace-resolution.test.ts +172 -0
- package/src/tests/kv-page-proxy.test.ts +212 -0
- package/src/tests/kv-storage.test.ts +227 -0
- package/src/tests/kv-tool.test.ts +217 -0
- package/src/tests/launch-password-rejection.test.ts +139 -0
- package/src/tests/page-proxy-authed.test.ts +146 -0
- package/src/tests/page-proxy.test.ts +270 -0
- package/src/tests/page-session.test.ts +169 -0
- package/src/tests/pages-actions-endpoint.test.ts +102 -0
- package/src/tests/pages-authed-mode.test.ts +211 -0
- package/src/tests/pages-http.test.ts +193 -0
- package/src/tests/pages-list-endpoint.test.ts +149 -0
- package/src/tests/pages-password-hash.test.ts +57 -0
- package/src/tests/pages-password-mode.test.ts +265 -0
- package/src/tests/pages-public-authed-401.test.ts +102 -0
- package/src/tests/pages-public-html.test.ts +151 -0
- package/src/tests/pages-public-json-redirect.test.ts +86 -0
- package/src/tests/pages-storage.test.ts +196 -0
- package/src/tests/pages-versioning.test.ts +231 -0
- package/src/tests/pages-view-count.test.ts +220 -0
- package/src/tests/prompt-template-session.test.ts +3 -2
- package/src/tests/skill-update-scope.test.ts +165 -0
- package/src/tests/swarm-diff.test.ts +303 -0
- package/src/tests/telemetry-init.test.ts +149 -0
- package/src/tests/workflow-wait-event.test.ts +4 -7
- package/src/tools/create-page.ts +263 -0
- package/src/tools/kv/index.ts +5 -0
- package/src/tools/kv/kv-delete.ts +89 -0
- package/src/tools/kv/kv-get.ts +64 -0
- package/src/tools/kv/kv-incr.ts +116 -0
- package/src/tools/kv/kv-list.ts +81 -0
- package/src/tools/kv/kv-set.ts +194 -0
- package/src/tools/kv/resolve-namespace.ts +58 -0
- package/src/tools/skills/skill-update.ts +26 -0
- package/src/tools/tool-config.ts +10 -0
- package/src/types.ts +107 -0
- package/src/utils/internal-ai/complete-structured.ts +2 -2
- package/src/utils/internal-ai/credentials.ts +3 -3
- package/src/utils/page-session.ts +254 -0
- 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
|
+
}
|