@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,423 @@
|
|
|
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
|
+
## Built-in primitives
|
|
214
|
+
|
|
215
|
+
Every HTML page automatically gets a small set of zero-dep web components
|
|
216
|
+
auto-injected alongside the Browser SDK. Drop them into your page body —
|
|
217
|
+
no `<script>` import, no bundling, no Tailwind required (though Tailwind
|
|
218
|
+
Play CDN is loaded, so utility classes work too).
|
|
219
|
+
|
|
220
|
+
### `<swarm-diff>` — unified diff renderer
|
|
221
|
+
|
|
222
|
+
Render a unified diff with a two-column gutter, severity annotations, and a
|
|
223
|
+
deterministic anchor id per hunk (so deep-linking + jump lists work). The
|
|
224
|
+
element reads its payload from its `textContent` as JSON of shape
|
|
225
|
+
`{ hunks: [{ old_start, old_lines, new_start, new_lines, lines, annotations? }] }`.
|
|
226
|
+
|
|
227
|
+
```html
|
|
228
|
+
<swarm-diff
|
|
229
|
+
file="src/foo.ts"
|
|
230
|
+
base-sha="abc123"
|
|
231
|
+
head-sha="def456">
|
|
232
|
+
{ "hunks": [
|
|
233
|
+
{ "old_start": 10, "old_lines": 3, "new_start": 10, "new_lines": 4,
|
|
234
|
+
"lines": [
|
|
235
|
+
{ "type": "context", "text": " const x = 1;" },
|
|
236
|
+
{ "type": "del", "text": "- console.log(x);" },
|
|
237
|
+
{ "type": "add", "text": "+ logger.info({ x });" },
|
|
238
|
+
{ "type": "add", "text": "+ return x;" }
|
|
239
|
+
],
|
|
240
|
+
"annotations": [
|
|
241
|
+
{ "line": 12, "severity": "warn", "text": "Avoid raw console.log" }
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
] }
|
|
245
|
+
</swarm-diff>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Inputs**
|
|
249
|
+
|
|
250
|
+
| Attribute | Required | Notes |
|
|
251
|
+
|---|---|---|
|
|
252
|
+
| `file` | yes | Path label rendered in the hunk header. Used for the anchor id slug. |
|
|
253
|
+
| `base-sha` | no | Pre-change SHA. Rendered in the header next to `head-sha`. |
|
|
254
|
+
| `head-sha` | no | Post-change SHA. |
|
|
255
|
+
|
|
256
|
+
**Line shape**
|
|
257
|
+
|
|
258
|
+
| Field | Values | Notes |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| `type` | `context` \| `add` \| `del` | Drives row tint (green / red / neutral) and gutter line numbering. |
|
|
261
|
+
| `text` | string | Rendered verbatim (HTML-escaped). |
|
|
262
|
+
|
|
263
|
+
**Annotation shape** — attaches to a NEW-side line by line number; rendered
|
|
264
|
+
as a margin badge on that row.
|
|
265
|
+
|
|
266
|
+
| Field | Values | Notes |
|
|
267
|
+
|---|---|---|
|
|
268
|
+
| `line` | integer | New-side line number (falls back to old-side if no add for that line). |
|
|
269
|
+
| `severity` | `error` \| `warn` \| `info` | Drives badge color. |
|
|
270
|
+
| `text` | string | Badge body. |
|
|
271
|
+
|
|
272
|
+
**Anchor id**
|
|
273
|
+
|
|
274
|
+
Each hunk gets `id="swarm-diff-<file-slug>-<old_start>"` for deep-linking.
|
|
275
|
+
Use `<swarm-diff-jumps></swarm-diff-jumps>` anywhere in the page body to
|
|
276
|
+
render a tiny "Jump to" navigation of every diff hunk on the page —
|
|
277
|
+
handy when an agent ships a multi-file annotated PR.
|
|
278
|
+
|
|
279
|
+
**Programmatic form**
|
|
280
|
+
|
|
281
|
+
If you need to render a diff from a fetch response (rather than inline JSON),
|
|
282
|
+
use `window.swarmUi.renderDiff(rootEl, diffData)`:
|
|
283
|
+
|
|
284
|
+
```html
|
|
285
|
+
<div id="diff-target"></div>
|
|
286
|
+
<script>
|
|
287
|
+
const data = await fetch('/some/diff.json').then(r => r.json());
|
|
288
|
+
window.swarmUi.renderDiff(document.getElementById('diff-target'), data);
|
|
289
|
+
</script>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Annotated-PR example**
|
|
293
|
+
|
|
294
|
+
```html
|
|
295
|
+
<!doctype html>
|
|
296
|
+
<html><head><title>PR #1234 — `console.log` cleanup</title></head>
|
|
297
|
+
<body>
|
|
298
|
+
<h1>PR #1234 — cleanup raw <code>console.log</code> calls</h1>
|
|
299
|
+
<p>Replaces ad-hoc logging with the project logger.</p>
|
|
300
|
+
|
|
301
|
+
<swarm-diff-jumps></swarm-diff-jumps>
|
|
302
|
+
|
|
303
|
+
<swarm-diff file="src/foo.ts" base-sha="abc123" head-sha="def456">
|
|
304
|
+
{ "hunks": [
|
|
305
|
+
{ "old_start": 10, "old_lines": 3, "new_start": 10, "new_lines": 4,
|
|
306
|
+
"lines": [
|
|
307
|
+
{ "type": "context", "text": " const x = 1;" },
|
|
308
|
+
{ "type": "del", "text": "- console.log(x);" },
|
|
309
|
+
{ "type": "add", "text": "+ logger.info({ x });" },
|
|
310
|
+
{ "type": "add", "text": "+ return x;" }
|
|
311
|
+
],
|
|
312
|
+
"annotations": [
|
|
313
|
+
{ "line": 12, "severity": "warn", "text": "Avoid raw console.log" }
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
] }
|
|
317
|
+
</swarm-diff>
|
|
318
|
+
|
|
319
|
+
<swarm-diff file="src/bar.ts" base-sha="abc123" head-sha="def456">
|
|
320
|
+
{ "hunks": [
|
|
321
|
+
{ "old_start": 5, "old_lines": 1, "new_start": 5, "new_lines": 1,
|
|
322
|
+
"lines": [
|
|
323
|
+
{ "type": "del", "text": "- console.error('boom');" },
|
|
324
|
+
{ "type": "add", "text": "+ logger.error('boom');" }
|
|
325
|
+
]
|
|
326
|
+
}
|
|
327
|
+
] }
|
|
328
|
+
</swarm-diff>
|
|
329
|
+
</body></html>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Print / PDF export
|
|
333
|
+
|
|
334
|
+
Every HTML page also gets a `@media print` rule baked into the head defaults:
|
|
335
|
+
- Light theme on print (white background, black text, underlined black links).
|
|
336
|
+
- Anything with the `.no-print` class is hidden (annotation badges and the
|
|
337
|
+
jump list already carry this class — use it on agent-emitted chrome you
|
|
338
|
+
want suppressed in PDF exports).
|
|
339
|
+
- `.swarm-card` and `<swarm-diff>` get `break-inside: avoid` so they don't
|
|
340
|
+
split mid-element across pages.
|
|
341
|
+
|
|
342
|
+
Trigger the export from the SPA's "Export PDF" button on `/pages/:id` — it
|
|
343
|
+
opens the iframe's native print dialog (HTML pages) or the SPA's print
|
|
344
|
+
dialog (JSON pages). The browser's "Print → Save as PDF" handles the actual
|
|
345
|
+
file. No headless Chromium, no server-side rendering — zero infra weight.
|
|
346
|
+
|
|
347
|
+
> Want a custom print layout? Override the print styles in your page's own
|
|
348
|
+
> `<style>` block — agent CSS always wins over the head defaults.
|
|
349
|
+
|
|
350
|
+
## View counter
|
|
351
|
+
|
|
352
|
+
Every successful `200` from `GET /p/:id` (HTML inline) and `GET /p/:id.json`
|
|
353
|
+
(JSON metadata) bumps a `view_count` field on the page. `302` (JSON pages
|
|
354
|
+
redirecting to the SPA), `401`/`403` (auth gate), and `404` do NOT bump.
|
|
355
|
+
The count is exposed on `GET /api/pages` listing (`viewCount` field) and the
|
|
356
|
+
SPA `/pages` index renders it as a small eye-count badge per row.
|
|
357
|
+
|
|
358
|
+
No per-viewer dedup — this is a coarse popularity signal, not analytics.
|
|
359
|
+
Bumps are best-effort (wrapped in try/catch so a counter write never fails
|
|
360
|
+
the response).
|
|
361
|
+
|
|
362
|
+
## JSON Renderer
|
|
363
|
+
|
|
364
|
+
JSON pages are rendered via [`@json-render/react`](https://json-render.dev)
|
|
365
|
+
with a custom `swarm.call` action handler. Action shape:
|
|
366
|
+
|
|
367
|
+
```jsonc
|
|
368
|
+
{
|
|
369
|
+
"type": "button",
|
|
370
|
+
"label": "Reassign",
|
|
371
|
+
"action": {
|
|
372
|
+
"swarm.call": {
|
|
373
|
+
"method": "POST",
|
|
374
|
+
"endpoint": "/api/tasks/abc/reassign",
|
|
375
|
+
"body": { "agentId": "xyz" }
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
`swarm.call` dispatches through the SPA's bearer (for `app_url` loads) or
|
|
382
|
+
the page-session cookie (for direct `api_url` loads). The endpoint must be
|
|
383
|
+
a valid swarm API path — there is no allowlist, but the viewer's identity
|
|
384
|
+
bounds what the call can do.
|
|
385
|
+
|
|
386
|
+
See the `@json-render/core` docs for the supported node types (`text`,
|
|
387
|
+
`button`, `input`, `card`, etc.).
|
|
388
|
+
|
|
389
|
+
## Security & Blast Radius
|
|
390
|
+
|
|
391
|
+
- Declared actions on `authed` / `password` pages run with the **viewer's**
|
|
392
|
+
identity, not the page author's. A button that says "Delete all tasks"
|
|
393
|
+
will delete the viewer's tasks if the viewer clicks it.
|
|
394
|
+
- Treat agent-generated HTML / JSON like trusted code — the agent already
|
|
395
|
+
has equivalent MCP access, so a malicious page is no worse than a
|
|
396
|
+
malicious tool call. But: don't ship pages to **external** users (via
|
|
397
|
+
`password`) without reviewing the body first.
|
|
398
|
+
- HTML pages render inside a sandboxed iframe with
|
|
399
|
+
`sandbox="allow-scripts allow-forms allow-same-origin"`. This limits
|
|
400
|
+
some attack surface (no top-level navigation, no pointer-lock) but the
|
|
401
|
+
page still has full access to the SwarmSDK if cookies are present.
|
|
402
|
+
- All page bodies pass through `scrubSecrets` at the egress boundary
|
|
403
|
+
(`/p/:id`, `/p/:id.json`, listing endpoint) — accidental secrets in
|
|
404
|
+
the body get masked at serve time, not at write time. Don't rely on
|
|
405
|
+
scrubbing as a security boundary — keep secrets out of bodies.
|
|
406
|
+
|
|
407
|
+
## Limits
|
|
408
|
+
|
|
409
|
+
- **Body size**: 5 MiB per version (HTML or JSON). Bumping requires careful
|
|
410
|
+
thought about SQLite write-amplification — full bodies are snapshotted on
|
|
411
|
+
every update.
|
|
412
|
+
- **TTL**: none. Pages persist until explicitly deleted via `DELETE
|
|
413
|
+
/api/pages/:id` (or the SPA listing UI when it gains a delete affordance).
|
|
414
|
+
- **Per-agent quota**: none in v1. Be considerate.
|
|
415
|
+
- **Slug uniqueness**: scoped to `(agentId, slug)`. Two agents can both
|
|
416
|
+
have a `status-report` page without colliding.
|
|
417
|
+
|
|
418
|
+
## See Also
|
|
419
|
+
|
|
420
|
+
- `plugin/skills/artifacts/skill.md` — full custom Hono apps with PM2 +
|
|
421
|
+
tunneled subdomain. Use for interactive servers, not static content.
|
|
422
|
+
- `runbooks/secret-scrubbing.md` — egress scrubbing details.
|
|
423
|
+
- SPA listing: `${UI_URL}/pages`.
|