@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,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`.