@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.
- package/openapi.json +542 -1
- package/package.json +1 -1
- package/plugin/skills/artifacts/SKILL.md +151 -0
- package/plugin/skills/artifacts/examples/static-report.sh +1 -1
- package/plugin/skills/pages/SKILL.md +274 -0
- package/src/artifact-sdk/browser-sdk.ts +105 -20
- package/src/be/db.ts +239 -0
- package/src/be/migrations/059_pages.sql +34 -0
- package/src/be/migrations/060_page_versions.sql +19 -0
- package/src/commands/artifact.ts +17 -11
- package/src/http/index.ts +7 -1
- package/src/http/page-proxy.ts +208 -0
- package/src/http/pages-public.ts +466 -0
- package/src/http/pages.ts +608 -0
- package/src/http/utils.ts +68 -5
- package/src/pages/version.ts +44 -0
- package/src/prompts/session-templates.ts +51 -0
- package/src/server.ts +10 -1
- package/src/tests/artifact-commands.test.ts +92 -0
- package/src/tests/artifact-sdk.test.ts +80 -74
- package/src/tests/create-page-tool.test.ts +197 -0
- package/src/tests/fixtures/sample-json-page.json +52 -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 +266 -0
- package/src/tests/page-session.test.ts +164 -0
- package/src/tests/pages-actions-endpoint.test.ts +102 -0
- package/src/tests/pages-authed-mode.test.ts +207 -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/prompt-template-session.test.ts +3 -2
- package/src/tests/skill-update-scope.test.ts +165 -0
- package/src/tests/workflow-wait-event.test.ts +4 -7
- package/src/tools/create-page.ts +263 -0
- package/src/tools/skills/skill-update.ts +26 -0
- package/src/tools/tool-config.ts +3 -0
- package/src/types.ts +54 -0
- package/src/utils/page-session.ts +254 -0
- 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.
|
|
@@ -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
|
-
//
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
`;
|