@desplega.ai/agent-swarm 1.78.1 → 1.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/openapi.json +542 -1
  2. package/package.json +1 -1
  3. package/plugin/skills/artifacts/SKILL.md +151 -0
  4. package/plugin/skills/artifacts/examples/static-report.sh +1 -1
  5. package/plugin/skills/pages/SKILL.md +274 -0
  6. package/src/artifact-sdk/browser-sdk.ts +105 -20
  7. package/src/be/db.ts +239 -0
  8. package/src/be/migrations/059_pages.sql +34 -0
  9. package/src/be/migrations/060_page_versions.sql +19 -0
  10. package/src/commands/artifact.ts +17 -11
  11. package/src/http/index.ts +7 -1
  12. package/src/http/page-proxy.ts +208 -0
  13. package/src/http/pages-public.ts +466 -0
  14. package/src/http/pages.ts +608 -0
  15. package/src/http/utils.ts +68 -5
  16. package/src/pages/version.ts +44 -0
  17. package/src/prompts/session-templates.ts +51 -0
  18. package/src/server.ts +10 -1
  19. package/src/tests/artifact-commands.test.ts +92 -0
  20. package/src/tests/artifact-sdk.test.ts +80 -74
  21. package/src/tests/create-page-tool.test.ts +197 -0
  22. package/src/tests/fixtures/sample-json-page.json +52 -0
  23. package/src/tests/launch-password-rejection.test.ts +139 -0
  24. package/src/tests/page-proxy-authed.test.ts +146 -0
  25. package/src/tests/page-proxy.test.ts +266 -0
  26. package/src/tests/page-session.test.ts +164 -0
  27. package/src/tests/pages-actions-endpoint.test.ts +102 -0
  28. package/src/tests/pages-authed-mode.test.ts +207 -0
  29. package/src/tests/pages-http.test.ts +193 -0
  30. package/src/tests/pages-list-endpoint.test.ts +149 -0
  31. package/src/tests/pages-password-hash.test.ts +57 -0
  32. package/src/tests/pages-password-mode.test.ts +265 -0
  33. package/src/tests/pages-public-authed-401.test.ts +102 -0
  34. package/src/tests/pages-public-html.test.ts +151 -0
  35. package/src/tests/pages-public-json-redirect.test.ts +86 -0
  36. package/src/tests/pages-storage.test.ts +196 -0
  37. package/src/tests/pages-versioning.test.ts +231 -0
  38. package/src/tests/prompt-template-session.test.ts +3 -2
  39. package/src/tests/skill-update-scope.test.ts +165 -0
  40. package/src/tests/workflow-wait-event.test.ts +4 -7
  41. package/src/tools/create-page.ts +263 -0
  42. package/src/tools/skills/skill-update.ts +26 -0
  43. package/src/tools/tool-config.ts +3 -0
  44. package/src/types.ts +54 -0
  45. package/src/utils/page-session.ts +254 -0
  46. package/plugin/skills/artifacts/skill.md +0 -70
@@ -0,0 +1,151 @@
1
+ ---
2
+ name: artifacts
3
+ description: Serve interactive web content (HTML pages, dashboards, approval flows, static reports, custom Hono apps) to a public URL via localtunnel. Use when the user asks to "create an artifact for X", "host this for me", "make me a tunneled URL", "spin up a web server for X", "publish this report so I can see it", "share this file/page publicly", "expose this dashboard", "give me a live link", or anything that needs a browser-reachable URL pointing at agent-generated content. Wraps the `agent-swarm artifact` CLI plus the `createArtifactServer` SDK; covers static directories, custom Hono apps, daemonization (nohup / PM2), HTTP Basic auth, and the in-page swarm Browser SDK.
4
+ ---
5
+
6
+ # Artifacts — Serving Interactive Web Content
7
+
8
+ Serve a directory or a Hono app to a public, auth-protected URL via localtunnel. Useful for sharing reports, dashboards, approval flows, or anything else a human needs to look at in a browser.
9
+
10
+ The CLI is a subcommand of `agent-swarm`. Always invoke as **`agent-swarm artifact <subcommand>`** — there is no top-level `artifact` binary.
11
+
12
+ ## Quick Start
13
+
14
+ ### Static content
15
+ ```bash
16
+ # Create your content in a persisted directory
17
+ mkdir -p /workspace/personal/artifacts/my-report
18
+ echo '<h1>My Report</h1>' > /workspace/personal/artifacts/my-report/index.html
19
+
20
+ # Serve it (auto-assigns a free port, creates tunnel, registers in service registry)
21
+ agent-swarm artifact serve /workspace/personal/artifacts/my-report --name my-report
22
+ # -> Artifact "my-report" live at https://<agentId>-my-report.lt.desplega.ai (port <auto>)
23
+ ```
24
+
25
+ ### Programmatic (custom Hono server)
26
+ ```typescript
27
+ import { createArtifactServer } from '../artifact-sdk';
28
+ import { Hono } from 'hono';
29
+
30
+ const app = new Hono();
31
+ app.get('/', (c) => c.html('<h1>Dashboard</h1>'));
32
+
33
+ const server = createArtifactServer({ name: 'dashboard', app });
34
+ await server.start();
35
+ console.log(`Live at: ${server.url}`);
36
+ ```
37
+
38
+ You can also `agent-swarm artifact serve ./server.ts --name dashboard` if `server.ts` exports a Hono instance as its default export.
39
+
40
+ ## CLI Commands
41
+
42
+ | Command | Description |
43
+ |---|---|
44
+ | `agent-swarm artifact serve <path> --name <name> [--port <port>] [--no-auth] [--subdomain <sub>]` | Start serving content. `<path>` is a directory (static) or a `.ts`/`.js` file exporting a default Hono app. |
45
+ | `agent-swarm artifact list` | List active artifacts (name, agent, port, URL, status) from the service registry. |
46
+ | `agent-swarm artifact stop <name>` | Stop an artifact: deletes the matching PM2 process and unregisters it from the service registry. See "Known limitation" below for non-PM2 processes. |
47
+
48
+ Flags accepted by `serve`:
49
+ - `--name <name>` — defaults to the basename of `<path>`. Used for the subdomain and PM2 process name.
50
+ - `--port <port>` — pin to a specific port. Default: auto-assigned ephemeral port.
51
+ - `--no-auth` — disable HTTP Basic auth on the tunnel (DANGEROUS — anyone with the URL can access).
52
+ - `--subdomain <sub>` — override the default `${agentId}-${name}` subdomain.
53
+
54
+ ## Auth & URL Pattern
55
+
56
+ Tunnels are protected by **HTTP Basic auth** by default:
57
+ - **Username:** `hi` (hardcoded MVP default in `src/artifact-sdk/tunnel.ts`)
58
+ - **Password:** the agent's `API_KEY`
59
+
60
+ Two equivalent URL forms:
61
+
62
+ ```
63
+ # Plain (browser will prompt for credentials)
64
+ https://<agentId>-<name>.lt.desplega.ai
65
+
66
+ # Auth-prefilled (works in curl, scripts, and most browsers without a prompt)
67
+ https://hi:<API_KEY>@<agentId>-<name>.lt.desplega.ai
68
+ ```
69
+
70
+ Use `--no-auth` only for genuinely public content. Anyone who learns the subdomain can read it.
71
+
72
+ ## Running it as a daemon
73
+
74
+ `agent-swarm artifact serve` blocks on a never-resolving promise to stay alive — you cannot inline it in a script that needs to do other work. Pick one of these:
75
+
76
+ ### Option A — `nohup` (quick, throwaway)
77
+
78
+ Easiest for one-off "host this for the next 10 minutes" cases:
79
+
80
+ ```bash
81
+ mkdir -p /workspace/personal/logs
82
+ nohup agent-swarm artifact serve /workspace/personal/artifacts/my-report \
83
+ --name my-report \
84
+ > /workspace/personal/logs/my-report.out 2>&1 &
85
+ echo $! > /workspace/personal/logs/my-report.pid
86
+
87
+ # Later, kill it manually:
88
+ kill "$(cat /workspace/personal/logs/my-report.pid)"
89
+ ```
90
+
91
+ ### Option B — PM2 (recommended for anything you'll come back to)
92
+
93
+ PM2 gives you auto-restart on crash, a process name, log management, and — crucially — **lets `agent-swarm artifact stop <name>` actually kill it** (see Known limitation below).
94
+
95
+ ```bash
96
+ pm2 start agent-swarm \
97
+ --name artifact-my-report \
98
+ -- artifact serve /workspace/personal/artifacts/my-report --name my-report
99
+
100
+ # Stop it cleanly later:
101
+ agent-swarm artifact stop my-report
102
+ ```
103
+
104
+ The PM2 process name **must** be `artifact-<name>` (matching `--name`) — that's exactly what `artifact stop` looks for.
105
+
106
+ ### Known limitation — `artifact stop` only kills PM2-started processes
107
+
108
+ Today, `agent-swarm artifact stop <name>` runs `pm2 delete artifact-<name>` and then unregisters the entry from the service registry. If you started the artifact with `nohup` (or `&`, or any non-PM2 launcher), `pm2 delete` silently fails and the actual server keeps running and serving — even though the command prints `Artifact '<name>' stopped.` Tracked as a follow-up bug filed alongside PR #469; until it's fixed:
109
+
110
+ - Use **PM2** if you want `artifact stop` to actually do its job.
111
+ - For `nohup`/foreground processes, kill the PID yourself (`kill <pid>` or `pkill -f 'artifact serve.*<name>'`) **and then** run `agent-swarm artifact stop <name>` to clear the registry row.
112
+
113
+ ## Multiple Artifacts
114
+
115
+ Each artifact gets its own port (auto-assigned) and subdomain (`<agentId>-<name>`). You can run several simultaneously — see `examples/multi-artifact.ts`.
116
+
117
+ ## Browser SDK
118
+
119
+ HTML artifacts can call back into the swarm API via a server-side proxy that injects auth, so browser code never sees the API key:
120
+
121
+ ```html
122
+ <script src="/@swarm/sdk.js"></script>
123
+ <script>
124
+ const swarm = new SwarmSDK();
125
+ await swarm.createTask({ task: 'Do something' });
126
+ const agents = await swarm.getSwarm();
127
+ </script>
128
+ ```
129
+
130
+ ### Available SDK Methods
131
+ - `createTask(opts)` — Create a new task
132
+ - `getTasks(filters)` — List tasks with optional filters
133
+ - `getTaskDetails(id)` — Get details for a specific task
134
+ - `storeProgress(taskId, data)` — Update task progress
135
+ - `postMessage(opts)` — Post a message to a channel
136
+ - `readMessages(opts)` — Read messages from a channel
137
+ - `getSwarm()` — Get list of agents
138
+ - `listServices()` — List registered services
139
+ - `slackReply(opts)` — Reply to a Slack thread
140
+
141
+ ## API Proxy
142
+
143
+ The `/@swarm/api/*` proxy forwards requests to the MCP server with proper authentication headers. This allows browser-side JavaScript to call swarm APIs without exposing credentials.
144
+
145
+ ## Storage
146
+
147
+ Always store artifact content in persisted directories — the working dir is wiped between sessions:
148
+ - `/workspace/personal/artifacts/` — per-agent, persists across sessions (default)
149
+ - `/workspace/shared/artifacts/` — shared across the swarm
150
+
151
+ See the `examples/` directory for complete working examples.
@@ -14,4 +14,4 @@ cat > "$ARTIFACT_DIR/index.html" << 'HTML'
14
14
  </html>
15
15
  HTML
16
16
 
17
- artifact serve "$ARTIFACT_DIR" --name "my-report"
17
+ agent-swarm artifact serve "$ARTIFACT_DIR" --name "my-report"
@@ -0,0 +1,274 @@
1
+ # Pages — DB-backed Static Artifacts
2
+
3
+ DB-backed static content (HTML or JSON) served by the API directly. Cheap,
4
+ versioned, share-able by URL. The lighter-weight cousin of `artifacts` —
5
+ no PM2, no tunnels, no port allocation, no `services` registry row.
6
+
7
+ > **Capability gate**: the `create_page` MCP tool is only available when the
8
+ > agent's `CAPABILITIES` env var includes `pages`
9
+ > (e.g. `CAPABILITIES=core,task-pool,pages`). If the tool is missing from
10
+ > your MCP list, this is why.
11
+
12
+ ## When to use Pages vs Artifacts
13
+
14
+ | You need… | Use |
15
+ |---|---|
16
+ | A static HTML report / dashboard | **Pages** |
17
+ | A JSON status payload + a few buttons that call swarm APIs | **Pages** (`contentType: 'application/json'`) |
18
+ | To share an output via a URL with no server logic | **Pages** |
19
+ | Custom routes, websockets, server-side logic | **Artifacts** (`plugin/skills/artifacts/skill.md`) |
20
+ | File uploads or per-request computation | **Artifacts** |
21
+
22
+ Rule of thumb: if the content is a snapshot (you can write the full HTML/JSON
23
+ in a single call), use pages. If the content is a *running program*, use
24
+ artifacts.
25
+
26
+ ## Quick Start
27
+
28
+ ### Public HTML report
29
+ ```jsonc
30
+ // Tool call: create_page
31
+ {
32
+ "title": "Q2 Status Report",
33
+ "description": "Roll-up of in-flight tasks across the swarm",
34
+ "contentType": "text/html",
35
+ "authMode": "public",
36
+ "body": "<!doctype html><html><body><h1>Q2 Status</h1>...</body></html>"
37
+ }
38
+ // → { id, version: 1, app_url, api_url }
39
+ ```
40
+
41
+ Share `app_url` (the SPA route) for the general case; share `api_url` for a
42
+ no-SPA-required direct link.
43
+
44
+ ### Authed JSON dashboard
45
+ ```jsonc
46
+ // Tool call: create_page
47
+ {
48
+ "title": "Agent Inbox",
49
+ "description": "Live tasks for me",
50
+ "contentType": "application/json",
51
+ "authMode": "authed",
52
+ "body": "{\"$schema\":\"...\",\"type\":\"page\",\"children\":[{\"type\":\"text\",\"value\":\"Hello\"},{\"type\":\"button\",\"label\":\"Refresh\",\"action\":{\"swarm.call\":{\"method\":\"GET\",\"endpoint\":\"/api/tasks?status=in_progress\"}}}]}"
53
+ }
54
+ // → { id, version: 1, app_url, api_url }
55
+ ```
56
+
57
+ `authed` pages require a viewer to be signed in to the SPA (or to mint a
58
+ page-session cookie via the launch endpoint) before the page can call the
59
+ swarm API.
60
+
61
+ ## Auth Modes
62
+
63
+ | Mode | URL behavior | When to use |
64
+ |---|---|---|
65
+ | `public` | No gate. Anyone with the URL sees the content. Browser SDK calls **return 401** (no viewer identity → no API access). | Static reports, marketing pages, anything safe to share externally. |
66
+ | `authed` | SPA `app_url` works for any signed-in dashboard user. Direct `api_url` requires a `page_session` cookie (mint via `POST /api/pages/:id/launch`). Browser SDK calls run as the viewing user. | Per-team dashboards, JSON pages with action buttons. |
67
+ | `password` | `?key=<password>` or HTTP Basic on `/p/:id` unlocks. Once unlocked, behaves like `authed` (cookie minted, SDK calls run as viewer's identity). | Pages shared with non-swarm users (clients, contractors). |
68
+
69
+ > Password unlock has to happen on `/p/:id` directly (the API origin) because
70
+ > the password isn't sent to the SPA. Sharing an `app_url` for a `password`
71
+ > page works but the SPA will redirect the iframe through `/p/:id` for the
72
+ > Basic prompt.
73
+
74
+ ## URL Shapes
75
+
76
+ | URL | Shape | Notes |
77
+ |---|---|---|
78
+ | `app_url` | `${APP_URL}/pages/:id` | SPA route. Renders HTML in a sandboxed iframe, JSON via `@json-render/react`. Default share target. |
79
+ | `app_url` (full mode) | `${APP_URL}/pages/:id?mode=full` | Same SPA route, maximized — hides the SPA sidebar/header so the page body gets the full viewport. Slim header with title + Exit-Full button. Useful for embeds + standalone dashboards. |
80
+ | `api_url` | `${MCP_BASE_URL}/p/:id` | Direct API render. HTML inlines and serves; JSON 302-redirects to `app_url`. Useful for no-SPA-required links. |
81
+
82
+ `${APP_URL}` is the SPA origin (e.g. `https://app.agent-swarm.dev` in prod).
83
+ `${MCP_BASE_URL}` is the API origin (e.g. `https://api.desplega.agent-swarm.dev`
84
+ in prod). Both are surfaced as env vars to your agent — never hardcode hosts;
85
+ read them from `process.env`.
86
+
87
+ **Default**: share `app_url`. Append `?mode=full` when the recipient should
88
+ see ONLY the page (no surrounding swarm chrome). Use `api_url` only when you
89
+ specifically need a link that bypasses the SPA (e.g. embedding in Slack,
90
+ where the unfurl preview only follows the API origin).
91
+
92
+ ## Versioning
93
+
94
+ Every overwrite (update via `update_page` or `PUT /api/pages/:id`) snapshots
95
+ the **pre-update** state into `page_versions` and writes the new state to the
96
+ parent row. The wire `version` field is a monotonically-increasing
97
+ "edit counter" — version 1 is the initial create.
98
+
99
+ | Operation | Endpoint | Returns |
100
+ |---|---|---|
101
+ | List versions | `GET /api/pages/:id/versions` | `{ versions: PageVersion[] }` newest first |
102
+ | Read a version | `GET /api/pages/:id/versions/:version` | Single snapshot |
103
+
104
+ Snapshots are full body copies — keep this in mind for large pages (the
105
+ per-version body cap is 5 MiB).
106
+
107
+ ## Browser SDK
108
+
109
+ Every HTML page automatically gets `window.SwarmSDK` (the class) and
110
+ `window.swarmSdk` (a ready-to-use singleton) injected. The SDK routes through
111
+ the `/@swarm/api/*` proxy, which resolves the `page_session` cookie to a user
112
+ identity and forwards with proper auth headers server-side — your page never
113
+ sees or handles tokens.
114
+
115
+ The SDK is **domain-grouped**. Each domain exposes idiomatic CRUD-ish methods
116
+ that map 1:1 to the public REST API documented at
117
+ [**docs.agent-swarm.dev/docs/api-reference**](https://docs.agent-swarm.dev/docs/api-reference).
118
+
119
+ | Domain | Methods | Maps to |
120
+ |---|---|---|
121
+ | `swarmSdk.tasks` | `create(body)`, `list(filters?)`, `get(id)`, `storeProgress(id, data)` | `/api/tasks*` |
122
+ | `swarmSdk.agents` | `list()`, `get(id)` | `/api/agents*` |
123
+ | `swarmSdk.events` | `create(body)`, `list(filters?)`, `batch(body)`, `counts(filters?)` | `/api/events*` |
124
+ | `swarmSdk.memory` | `search(body)`, `list(filters?)`, `get(id)`, `rate(body)` | `/api/memory*` |
125
+ | `swarmSdk.repos` | `list()`, `get(id)`, `create(body)`, `update(id, body)`, `delete(id)` | `/api/repos*` |
126
+ | `swarmSdk.schedules` | `list()`, `get(id)`, `create(body)`, `update(id, body)`, `delete(id)`, `run(id)` | `/api/schedules*` |
127
+ | `swarmSdk.approvalRequests` | `list(filters?)`, `get(id)`, `create(body)`, `respond(id, body)` | `/api/approval-requests*` |
128
+
129
+ Inline usage:
130
+
131
+ ```html
132
+ <script>
133
+ // Singleton is ready immediately — no `new SwarmSDK()` needed.
134
+ const tasks = await window.swarmSdk.tasks.list({ status: 'in_progress' });
135
+ const agents = await window.swarmSdk.agents.list();
136
+
137
+ // Create an event from a button click
138
+ document.querySelector('#log-btn').onclick = async () => {
139
+ await window.swarmSdk.events.create({ name: 'page.button.clicked', payload: { at: Date.now() } });
140
+ };
141
+
142
+ // Approve / reject an approval request
143
+ await window.swarmSdk.approvalRequests.respond(reqId, { decision: 'approved' });
144
+ </script>
145
+ ```
146
+
147
+ Every method returns the parsed JSON response. Errors throw with `.status`
148
+ and `.response` attached to the `Error` object so callers can branch on the
149
+ HTTP status.
150
+
151
+ > **`public` pages cannot call authed endpoints.** No cookie is minted on a
152
+ > public page load → SDK calls 401. If your page needs to call swarm APIs,
153
+ > use `authed` (or `password`).
154
+
155
+ ### Full signature
156
+
157
+ This is the entire surface — copy it into your page if you want autocomplete
158
+ hints in an editor. The runtime version is auto-injected; you don't need to
159
+ include this in the page source.
160
+
161
+ ```js
162
+ class SwarmSDK {
163
+ tasks: {
164
+ create(body) // POST /api/tasks
165
+ list(filters?) // GET /api/tasks
166
+ get(id) // GET /api/tasks/:id
167
+ storeProgress(id, data) // POST /api/tasks/:id/progress
168
+ }
169
+ agents: {
170
+ list() // GET /api/agents
171
+ get(id) // GET /api/agents/:id
172
+ }
173
+ events: {
174
+ create(body) // POST /api/events
175
+ list(filters?) // GET /api/events
176
+ batch(body) // POST /api/events/batch
177
+ counts(filters?) // GET /api/events/counts
178
+ }
179
+ memory: {
180
+ search(body) // POST /api/memory/search
181
+ list(filters?) // GET /api/memory/list
182
+ get(id) // GET /api/memory/:id
183
+ rate(body) // POST /api/memory/rate
184
+ }
185
+ repos: {
186
+ list() // GET /api/repos
187
+ get(id) // GET /api/repos/:id
188
+ create(body) // POST /api/repos
189
+ update(id, body) // PUT /api/repos/:id
190
+ delete(id) // DELETE /api/repos/:id
191
+ }
192
+ schedules: {
193
+ list() // GET /api/schedules
194
+ get(id) // GET /api/schedules/:id
195
+ create(body) // POST /api/schedules
196
+ update(id, body) // PUT /api/schedules/:id
197
+ delete(id) // DELETE /api/schedules/:id
198
+ run(id) // POST /api/schedules/:id/run
199
+ }
200
+ approvalRequests: {
201
+ list(filters?) // GET /api/approval-requests
202
+ get(id) // GET /api/approval-requests/:id
203
+ create(body) // POST /api/approval-requests
204
+ respond(id, body) // POST /api/approval-requests/:id/respond
205
+ }
206
+ }
207
+ ```
208
+
209
+ For the full list of fields each endpoint accepts/returns, see
210
+ [**docs.agent-swarm.dev/docs/api-reference**](https://docs.agent-swarm.dev/docs/api-reference).
211
+ The SDK is a thin domain wrapper — anything documented there is reachable.
212
+
213
+ ## JSON Renderer
214
+
215
+ JSON pages are rendered via [`@json-render/react`](https://json-render.dev)
216
+ with a custom `swarm.call` action handler. Action shape:
217
+
218
+ ```jsonc
219
+ {
220
+ "type": "button",
221
+ "label": "Reassign",
222
+ "action": {
223
+ "swarm.call": {
224
+ "method": "POST",
225
+ "endpoint": "/api/tasks/abc/reassign",
226
+ "body": { "agentId": "xyz" }
227
+ }
228
+ }
229
+ }
230
+ ```
231
+
232
+ `swarm.call` dispatches through the SPA's bearer (for `app_url` loads) or
233
+ the page-session cookie (for direct `api_url` loads). The endpoint must be
234
+ a valid swarm API path — there is no allowlist, but the viewer's identity
235
+ bounds what the call can do.
236
+
237
+ See the `@json-render/core` docs for the supported node types (`text`,
238
+ `button`, `input`, `card`, etc.).
239
+
240
+ ## Security & Blast Radius
241
+
242
+ - Declared actions on `authed` / `password` pages run with the **viewer's**
243
+ identity, not the page author's. A button that says "Delete all tasks"
244
+ will delete the viewer's tasks if the viewer clicks it.
245
+ - Treat agent-generated HTML / JSON like trusted code — the agent already
246
+ has equivalent MCP access, so a malicious page is no worse than a
247
+ malicious tool call. But: don't ship pages to **external** users (via
248
+ `password`) without reviewing the body first.
249
+ - HTML pages render inside a sandboxed iframe with
250
+ `sandbox="allow-scripts allow-forms allow-same-origin"`. This limits
251
+ some attack surface (no top-level navigation, no pointer-lock) but the
252
+ page still has full access to the SwarmSDK if cookies are present.
253
+ - All page bodies pass through `scrubSecrets` at the egress boundary
254
+ (`/p/:id`, `/p/:id.json`, listing endpoint) — accidental secrets in
255
+ the body get masked at serve time, not at write time. Don't rely on
256
+ scrubbing as a security boundary — keep secrets out of bodies.
257
+
258
+ ## Limits
259
+
260
+ - **Body size**: 5 MiB per version (HTML or JSON). Bumping requires careful
261
+ thought about SQLite write-amplification — full bodies are snapshotted on
262
+ every update.
263
+ - **TTL**: none. Pages persist until explicitly deleted via `DELETE
264
+ /api/pages/:id` (or the SPA listing UI when it gains a delete affordance).
265
+ - **Per-agent quota**: none in v1. Be considerate.
266
+ - **Slug uniqueness**: scoped to `(agentId, slug)`. Two agents can both
267
+ have a `status-report` page without colliding.
268
+
269
+ ## See Also
270
+
271
+ - `plugin/skills/artifacts/skill.md` — full custom Hono apps with PM2 +
272
+ tunneled subdomain. Use for interactive servers, not static content.
273
+ - `runbooks/secret-scrubbing.md` — egress scrubbing details.
274
+ - SPA listing: `${UI_URL}/pages`.
@@ -1,29 +1,114 @@
1
- // This is a string template that gets served as JavaScript to the browser
1
+ // Browser-side Swarm SDK injected into agent-served HTML pages.
2
+ //
3
+ // Exposes a domain-grouped API on `window.SwarmSDK` (class) and a ready-to-use
4
+ // singleton `window.swarmSdk`. All calls route through the `/@swarm/api/*`
5
+ // proxy, which strips the page-session cookie and forwards to `/api/*` with
6
+ // a server-side bearer + agent-id. From the page's perspective, the SDK is
7
+ // authenticated automatically — no token handling on the client.
8
+ //
9
+ // Domains exposed:
10
+ // - tasks create, list, get, storeProgress
11
+ // - agents list, get
12
+ // - events create, list, batch, counts
13
+ // - memory search, list, get, rate
14
+ // - repos list, get, create, update, delete
15
+ // - schedules list, get, create, update, delete, run
16
+ // - approvalRequests list, get, create, respond
17
+ //
18
+ // Full HTTP API reference: https://docs.agent-swarm.dev/docs/api-reference
2
19
  export const BROWSER_SDK_JS = `
3
20
  class SwarmSDK {
4
21
  constructor() {
5
- this._configPromise = fetch('/@swarm/config').then(r => r.json());
6
- }
22
+ this._configPromise = fetch('/@swarm/config').then(r => r.json()).catch(() => null);
7
23
 
8
- async createTask(opts) { return this._post('/@swarm/api/tasks', opts); }
9
- async getTasks(filters) { return this._get('/@swarm/api/tasks?' + new URLSearchParams(filters)); }
10
- async getTaskDetails(id) { return this._get('/@swarm/api/tasks/' + id); }
11
- async storeProgress(taskId, data) { return this._post('/@swarm/api/tasks/' + taskId + '/progress', data); }
12
- async postMessage(opts) { return this._post('/@swarm/api/messages', opts); }
13
- async readMessages(opts) { return this._get('/@swarm/api/messages?' + new URLSearchParams(opts)); }
14
- async getSwarm() { return this._get('/@swarm/api/agents'); }
15
- async listServices() { return this._get('/@swarm/api/services'); }
16
- async slackReply(opts) { return this._post('/@swarm/api/slack/reply', opts); }
17
-
18
- async _post(url, body) {
19
- const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
20
- return res.json();
21
- }
22
- async _get(url) {
23
- const res = await fetch(url);
24
- return res.json();
24
+ const base = '/@swarm/api';
25
+ const call = async (method, path, body) => {
26
+ const init = { method };
27
+ if (body !== undefined) {
28
+ init.headers = { 'Content-Type': 'application/json' };
29
+ init.body = JSON.stringify(body);
30
+ }
31
+ const res = await fetch(base + path, init);
32
+ const text = await res.text();
33
+ let parsed = null;
34
+ if (text) {
35
+ try { parsed = JSON.parse(text); } catch { parsed = text; }
36
+ }
37
+ if (!res.ok) {
38
+ const err = new Error('SwarmSDK ' + method + ' ' + path + ': ' + res.status);
39
+ err.status = res.status;
40
+ err.response = parsed;
41
+ throw err;
42
+ }
43
+ return parsed;
44
+ };
45
+ const qs = (obj) => {
46
+ if (!obj) return '';
47
+ const p = new URLSearchParams();
48
+ for (const [k, v] of Object.entries(obj)) {
49
+ if (v === undefined || v === null) continue;
50
+ p.set(k, String(v));
51
+ }
52
+ const s = p.toString();
53
+ return s ? '?' + s : '';
54
+ };
55
+ const enc = encodeURIComponent;
56
+
57
+ this.tasks = {
58
+ create: (body) => call('POST', '/tasks', body),
59
+ list: (filters) => call('GET', '/tasks' + qs(filters)),
60
+ get: (id) => call('GET', '/tasks/' + enc(id)),
61
+ storeProgress: (id, data) => call('POST', '/tasks/' + enc(id) + '/progress', data),
62
+ };
63
+
64
+ this.agents = {
65
+ list: () => call('GET', '/agents'),
66
+ get: (id) => call('GET', '/agents/' + enc(id)),
67
+ };
68
+
69
+ this.events = {
70
+ create: (body) => call('POST', '/events', body),
71
+ list: (filters) => call('GET', '/events' + qs(filters)),
72
+ batch: (body) => call('POST', '/events/batch', body),
73
+ counts: (filters) => call('GET', '/events/counts' + qs(filters)),
74
+ };
75
+
76
+ this.memory = {
77
+ search: (body) => call('POST', '/memory/search', body),
78
+ list: (filters) => call('GET', '/memory/list' + qs(filters)),
79
+ get: (id) => call('GET', '/memory/' + enc(id)),
80
+ rate: (body) => call('POST', '/memory/rate', body),
81
+ };
82
+
83
+ this.repos = {
84
+ list: () => call('GET', '/repos'),
85
+ get: (id) => call('GET', '/repos/' + enc(id)),
86
+ create: (body) => call('POST', '/repos', body),
87
+ update: (id, body) => call('PUT', '/repos/' + enc(id), body),
88
+ delete: (id) => call('DELETE', '/repos/' + enc(id)),
89
+ };
90
+
91
+ this.schedules = {
92
+ list: () => call('GET', '/schedules'),
93
+ get: (id) => call('GET', '/schedules/' + enc(id)),
94
+ create: (body) => call('POST', '/schedules', body),
95
+ update: (id, body) => call('PUT', '/schedules/' + enc(id), body),
96
+ delete: (id) => call('DELETE', '/schedules/' + enc(id)),
97
+ run: (id) => call('POST', '/schedules/' + enc(id) + '/run'),
98
+ };
99
+
100
+ this.approvalRequests = {
101
+ list: (filters) => call('GET', '/approval-requests' + qs(filters)),
102
+ get: (id) => call('GET', '/approval-requests/' + enc(id)),
103
+ create: (body) => call('POST', '/approval-requests', body),
104
+ respond: (id, body) => call('POST', '/approval-requests/' + enc(id) + '/respond', body),
105
+ };
25
106
  }
26
107
  }
27
108
 
109
+ // Expose BOTH the class (for \`new SwarmSDK()\`) AND a ready-to-use singleton
110
+ // on \`window.swarmSdk\` so pages can call e.g. \`window.swarmSdk.agents.list()\`
111
+ // directly without instantiating.
28
112
  window.SwarmSDK = SwarmSDK;
113
+ window.swarmSdk = new SwarmSDK();
29
114
  `;