@oddessentials/agent-guild 1.14.0 → 1.15.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/docs/api.md ADDED
@@ -0,0 +1,850 @@
1
+ # Session manager API (v1)
2
+
3
+ The session manager is the only component that owns terminal processes. The
4
+ web page is one client of this API. Any other front end, such as a future
5
+ Unreal Engine interface, can use the same API to list, start, watch and drive
6
+ the same sessions at the same time.
7
+
8
+ This is the contract a client relies on. How the manager works out each value
9
+ is in the code (see [architecture.md](architecture.md)). A running manager
10
+ serves this doc at `/api.html`, where its read-only routes and sockets can be
11
+ tried.
12
+
13
+ ## Discovery and authentication
14
+
15
+ The manager listens on `127.0.0.1` only. Its default port is 47821, and
16
+ `AGENT_GUILD_PORT` overrides it.
17
+
18
+ A client finds a running manager through two files in the per-user data
19
+ directory:
20
+
21
+ | Platform | Data directory |
22
+ | --- | --- |
23
+ | Windows | `%APPDATA%\AgentGuild` |
24
+ | macOS | `~/Library/Application Support/AgentGuild` |
25
+ | Linux | `$XDG_CONFIG_HOME/agent-guild` (default `~/.config/agent-guild`) |
26
+
27
+ * `manager.json` exists while a manager runs. It holds `pid`, `host`, `port`,
28
+ `url`, `version` and `startedAt`.
29
+ * `auth-token` holds the API token. It persists across restarts. Delete it to
30
+ rotate the token.
31
+
32
+ Every endpoint except `GET /api/v1/health` requires the token:
33
+
34
+ ```
35
+ Authorization: Bearer <token>
36
+ ```
37
+
38
+ WebSocket clients that cannot set headers, such as browsers, pass
39
+ `?token=<token>` in the URL instead.
40
+
41
+ A device that cannot scan the page's sign-in QR code, such as a TV, obtains
42
+ the token by device sign-in: `POST /sign-in` answers a code to show on screen
43
+ and a `device` secret, the person approves the code in the page's
44
+ **Remote access** dialog, and `POST /sign-in/poll` with that secret then
45
+ answers the token once. Requests expire after five minutes; at most five are
46
+ live, and a new one is accepted every two seconds.
47
+
48
+ The manager rejects requests whose `Host` or browser `Origin` is outside
49
+ its loopback and saved remote-access allowlists. Native clients that send no
50
+ `Origin` still need an accepted Host and the API token. See
51
+ [remote access setup](configuration.md#reverse-proxies).
52
+
53
+ Errors use one shape:
54
+
55
+ ```json
56
+ { "error": { "code": "provider_unavailable", "message": "Codex CLI (\"codex\") was not found on PATH. ..." } }
57
+ ```
58
+
59
+ `message` is written for people. The codes below are the ones a client acts
60
+ on; any route can answer others.
61
+
62
+ ## Objects
63
+
64
+ ### Provider
65
+
66
+ ```json
67
+ {
68
+ "id": "anthropic",
69
+ "vendor": "Anthropic",
70
+ "tool": "Claude Code",
71
+ "command": "claude",
72
+ "package": "@anthropic-ai/claude-code",
73
+ "args": [],
74
+ "resumable": true,
75
+ "installable": true,
76
+ "installedVersion": "2.1.285",
77
+ "latestVersion": "2.1.290",
78
+ "updateAvailable": true,
79
+ "usageSource": "claude",
80
+ "historySource": "claude",
81
+ "memorySource": "claude",
82
+ "accounts": [{ "id": "default", "label": "Default" }, { "id": "work", "label": "Work" }],
83
+ "color": "#D97757",
84
+ "monogram": "A",
85
+ "iconUrl": null,
86
+ "install": "curl -fsSL https://claude.ai/install.sh | bash",
87
+ "docs": "https://code.claude.com/docs/en/setup",
88
+ "usageUrl": "https://claude.ai/settings/usage",
89
+ "billingUrl": "https://claude.ai/settings/billing",
90
+ "cloudUrl": "https://claude.ai/code",
91
+ "available": true,
92
+ "resolvedPath": "/usr/local/bin/claude"
93
+ }
94
+ ```
95
+
96
+ `available` is false when the command is not installed. A client should show
97
+ the provider as disabled and offer `POST /providers/:id/install` when
98
+ `installable` is true, or the `install` hint otherwise. `resumable` is true
99
+ when one of the tool's own earlier sessions can be resumed by id.
100
+
101
+ `installedVersion` and `latestVersion` are null until the first version check
102
+ finishes; a `providers.updated` event follows. `updateAvailable` is true when
103
+ the latest version is newer, and `POST /providers/:id/install` performs the
104
+ update when `updateCommand` is not null. `versionStatus` is `ok`,
105
+ `unavailable` (the tool printed no version) or `failed`, with `versionError`
106
+ saying why, and null while the tool is missing. `installChannel` is the
107
+ running copy's `channel`, or null. `lastInstall` is the last install, update
108
+ or uninstall session's result,
109
+ `{ kind, outcome, exitCode, verification, before, after, latest, at }`, with
110
+ `outcome` `installed`, `missing`, `updated`, `unchanged`, `done`, `removed`,
111
+ `remaining` or `failed`, and `at` in epoch milliseconds; or null. `warnings`
112
+ are lines the card shows about the copies, such as several being installed.
113
+ `npmNote` qualifies an npm install, or is null. `modelPattern` is the
114
+ provider's, or null. `reporting` is the tool's reporting mode: `claude`,
115
+ `codex`, `antigravity`, `grok`, `docker`, or null.
116
+
117
+ `installs` lists every copy of the tool that was found, each
118
+ `{ path, displayPath, channel, version, versionStatus, active, newer, onPath, uninstall, uninstallGuidance }`.
119
+ `channel` is `npm`, `native`, `brew`, `winget`, `legacy` or `unknown`, and for
120
+ a Docker CLI plugin `download`, `desktop`, `system` or `unknown`. `active`
121
+ marks the copy that runs, `newer` a copy newer than that one, and `onPath`
122
+ whether its folder is on PATH. `uninstall` is null when Agent Guild cannot
123
+ remove that copy, and `uninstallGuidance` then says how to remove it instead.
124
+ `POST /providers/:id/uninstall` removes one copy.
125
+
126
+ For a tool that is a Docker CLI plugin (Docker Agent), `installCommand` is the
127
+ command **Install** and **Update** run. `pluginError` says why docker could not
128
+ list the plugin's copies, or is null; `pluginPending` is true until docker has
129
+ listed them once.
130
+
131
+ `usageSource`, `historySource`, `memorySource` and `instructionsSource` are
132
+ null when `GET /usage`, `GET /providers/:id/history`,
133
+ `GET /providers/:id/memory` or `GET /providers/:id/instructions` cannot report
134
+ the tool. `historyDetails` is true when `GET /providers/:id/history/detail`
135
+ can read saved messages. `accounts` lists the sign-ins the tool can run
136
+ under: `default` is the tool's own, and each further one has its own home
137
+ folder, so it keeps its own sign-in and usage. `POST /sessions` takes an
138
+ account id. `accountsEditable` is true when accounts can be added, renamed
139
+ and removed through `/providers/:id/accounts`.
140
+
141
+ `shells` is null except for the `@shell` provider, where it lists the
142
+ installed shells, each `{ id, label, path, multiplexer }`, and `defaultShell`
143
+ is the id of the one a session runs unless `POST /sessions` names another in
144
+ `shell`. `multiplexer` is true for tmux and herdr, which are never the
145
+ default. Stopping or removing a session in one, or stopping the manager,
146
+ leaves the multiplexer's own session running, and
147
+ `POST /sessions/:id/reattach` attaches it again, also from the next manager.
148
+ `multiplexers` is `[]` for every provider but `@shell`; see
149
+ [Multiplexer installations](#multiplexer-installations).
150
+
151
+ For a tool whose agent or model reporting has to be turned on,
152
+ `reportingEnabled` says whether it is, and `reportingNote` says why that switch
153
+ cannot be used, or is null. `usageUrl`, `billingUrl` and `cloudUrl` are
154
+ `https://` links to the vendor's usage and billing pages and web app, or null.
155
+
156
+ ### Manager environment
157
+
158
+ `GET /environment?scope=` returns the cached snapshot of a scope and starts
159
+ its first check if none has finished. `POST /environment/refresh` with
160
+ `{ scope, cwd?, id? }` starts a new check and returns `202` at once; an
161
+ `environment.updated` event carries the snapshot when it changes. `scope` is:
162
+
163
+ * `manager` (the default): what the manager process finds on its PATH.
164
+ * `project`, with `cwd`: the version pin files in that folder, read without
165
+ running anything.
166
+ * `session`, with `id`: the PATH recorded when that session was spawned.
167
+ * `launch`: the manager's PATH, as a new process gets it before shell startup
168
+ files.
169
+
170
+ Every snapshot has `scope`, `host` (the computer running the manager),
171
+ `revision`, `refreshing`, `checkedAt` and `error`, which is null or says why
172
+ the check itself did not finish. A refresh keeps the previous results while
173
+ `refreshing` is true.
174
+
175
+ A project snapshot adds `cwd`, `stale` and `pins[]`, each
176
+ `{ id, label, source, version, status, detail }` with `status` `configured`,
177
+ `unreadable` or `invalid`. `configured` means the file asks for that version,
178
+ not that it is installed. A session snapshot adds `sessionId`, `spawnCwd` and
179
+ `availability`: `ok`, or `unavailable` for tmux and herdr.
180
+
181
+ The manager snapshot adds `platform`, `managerNode: { version, path }`,
182
+ `system`, `runtimes[]`, `designTools[]`, `docker` and `tools[]`. Session and
183
+ launch snapshots have `docker` but not `system`; a tmux or herdr session has
184
+ `docker: null`.
185
+
186
+ * `runtimes[]`: Node.js, Python, Go, .NET SDK, R and Rust, each with `id`,
187
+ `label`, `status`, `version` and `path`, and once checked `command` and an
188
+ optional `detail`. `status` is
189
+ `pending`, `ok`, `not_found`, `unavailable` (a launcher exists but no
190
+ runtime can be safely reported) or `failed`. Python may add
191
+ `alternatives[]`, and .NET `runtimes[]`.
192
+ * `designTools[]`: Blender, FFmpeg, GIMP, Inkscape and ImageMagick, each with
193
+ `id`, `label`, `status`, `version` and `path`, and once checked `command`
194
+ and `detail`.
195
+ `status` is `pending`, `on_path`, `not_on_path` (installed outside PATH),
196
+ `not_found` or `failed`.
197
+ * `tools[]`: `{ id, label, path, status: "detected" }` for nvm, NVM for
198
+ Windows, vfox, uv and pnpm.
199
+ * `system`: null until the first check finishes or when it cannot be read,
200
+ else `{ os, osDetail, arch, hostArch, cpu: { model, threads }, memory, wsl, wslDistributions }`.
201
+ * `docker`: `{ status, version, platform, os, arch, wsl2, context, endpoint, cli, detail }`,
202
+ with `status` `pending`, `running`, `stopped`, `not_found`, `denied`,
203
+ `remote` or `failed`. Fields other than `status` may be missing.
204
+
205
+ Probe output and the environment itself are never returned.
206
+
207
+ ### Multiplexer installations
208
+
209
+ An `@shell` provider also describes `multiplexers[]`. Each entry contains
210
+ `id` (`tmux` or `herdr`), `tool`, `docs`, `checked`, `available`,
211
+ `installable`, `installCommand`, `guidance`, `busy`, `pendingCards`,
212
+ `installs` and `lastInstall`; some are missing while `checked` is false.
213
+ Each copy in `installs` has the provider-copy fields except `newer`, and its own
214
+ `latestVersion`, `updateAvailable` and `updateCommand`. Never construct
215
+ commands from these fields: submit the returned `path` to the API.
216
+
217
+ These routes return `201 { session }`, with `task: "install"`:
218
+
219
+ | Route below `/api/v1` | Body |
220
+ | --- | --- |
221
+ | `POST /providers/:provider/multiplexers/:id/install` | `{ force?: boolean }` |
222
+ | `POST /providers/:provider/multiplexers/:id/update` | `{ path: string, force?: boolean }` |
223
+ | `POST /providers/:provider/multiplexers/:id/uninstall` | `{ path: string, force?: boolean }` |
224
+
225
+ 409 `multiplexer_in_use`, with `running` and `pending` counts, can be retried
226
+ with `force: true` after the user confirms. Herdr Uninstall refuses with 409
227
+ `herdr_running` while herdr has a session running; `force` does not bypass
228
+ that. Results arrive through `providers.updated`.
229
+
230
+ ### Usage
231
+
232
+ ```json
233
+ {
234
+ "providerId": "anthropic",
235
+ "accountId": "default",
236
+ "plan": "max",
237
+ "signedIn": true,
238
+ "windows": [
239
+ { "label": "5-hour", "usedPercent": 42.5, "resetsAt": "2026-09-30T08:00:00.000Z" },
240
+ { "label": "7-day", "usedPercent": 12, "resetsAt": "2026-10-03T05:00:00.000Z" },
241
+ { "label": "7-day Fable 5.1", "usedPercent": 48, "resetsAt": "2026-10-03T05:00:00.000Z" }
242
+ ],
243
+ "credits": null,
244
+ "fetchedAt": "2026-09-30T03:12:01.120Z",
245
+ "error": null
246
+ }
247
+ ```
248
+
249
+ One snapshot per account. Each window is one rate limit of the account's
250
+ subscription. `plan` is the subscription tier. `credits` is a prepaid credit
251
+ balance (Codex), or null. When the lookup failed, `windows` is empty and
252
+ `error` says why; `signedIn` is false when no sign-in was found for that
253
+ account, true when one was read, and null when that is unknown.
254
+ `rateLimited` is true when the vendor rate-limited the lookup.
255
+
256
+ ### History
257
+
258
+ ```json
259
+ {
260
+ "providerId": "anthropic",
261
+ "accountId": "default",
262
+ "sessions": [
263
+ { "id": "581893e5-a93d-5e49-968b-1c1c277d3255", "title": "Fix the login bug", "cwd": "/Users/me/src/app", "startedAt": "2026-10-01T13:26:53.713Z", "updatedAt": "2026-10-01T13:49:28.000Z" }
264
+ ],
265
+ "total": 42,
266
+ "fetchedAt": "2026-10-01T14:00:00.000Z",
267
+ "error": null
268
+ }
269
+ ```
270
+
271
+ The tool's own earlier sessions, newest first. `id` is what the tool resumes
272
+ by (`POST /sessions` with `resume`); `title` is the session's name or first
273
+ prompt, or null; `cwd` is the folder the session ran in, or null. Claude Code
274
+ finds a session only from its own folder, so resume with that `cwd`.
275
+ `sessions` holds at most `limit` entries of the `total` found. `error` says
276
+ why nothing could be listed; a tool that has never run lists no sessions and
277
+ no error. Antigravity CLI entries add `workspaces`, and its history may add a
278
+ `note`.
279
+
280
+ The optional `cwd` and `q` parameters filter by folder and by text (title, id
281
+ or folder) **before** applying `limit`; `total` counts matching entries.
282
+
283
+ #### Saved messages
284
+
285
+ `GET /providers/:id/history/detail?account=&id=&cursor=` returns:
286
+
287
+ ```json
288
+ {
289
+ "detail": {
290
+ "id": "581893e5-a93d-5e49-968b-1c1c277d3255",
291
+ "messages": [{ "role": "user", "text": "Fix the login bug" }],
292
+ "nextCursor": null,
293
+ "incomplete": false,
294
+ "omitted": false
295
+ }
296
+ }
297
+ ```
298
+
299
+ Only the user's requests and the visible answers are included, as plain text.
300
+ Follow `nextCursor` until it is null, even when a page has no visible
301
+ messages. `omitted` says records were left out; `incomplete` says the last one
302
+ was partial, and a later read may complete it. 409 `history_changed` means the
303
+ transcript changed: start again without a cursor.
304
+
305
+ ### Memory
306
+
307
+ ```json
308
+ {
309
+ "providerId": "xai",
310
+ "accountId": "default",
311
+ "folder": "/Users/me/src/app",
312
+ "scopes": [
313
+ { "id": "project", "label": "This project", "dir": "/Users/me/.grok/memory-v2/workspaces/app-1c1b5e9a", "note": null, "truncated": false,
314
+ "files": [{ "path": "topics/build.md", "fullPath": "/Users/me/.grok/memory-v2/workspaces/app-1c1b5e9a/topics/build.md", "folder": "/Users/me/.grok/memory-v2/workspaces/app-1c1b5e9a/topics", "title": "Build", "bytes": 2048, "modified": "2026-10-01T13:49:28.000Z" }] },
315
+ { "id": "global", "label": "Global", "dir": "/Users/me/.grok/memory-v2/global", "note": null, "truncated": false, "files": [] }
316
+ ],
317
+ "fetchedAt": "2026-10-01T14:00:00.000Z"
318
+ }
319
+ ```
320
+
321
+ The markdown files a tool keeps as memory for the working folder `folder`,
322
+ in the scopes that tool has. `dir` is null when the tool keeps no memory
323
+ there, and `note` says why, or anything else that qualifies the scope. A
324
+ file's `path` is relative to `dir`, `fullPath` is its full path and `folder`
325
+ the folder that holds it; `title` is its first `# ` heading, or null.
326
+ `truncated` says the scope has more files than were listed.
327
+
328
+ ### Instruction files
329
+
330
+ ```json
331
+ {
332
+ "providerId": "anthropic",
333
+ "accountId": "default",
334
+ "folder": "/Users/me/src/app",
335
+ "count": 2,
336
+ "note": null,
337
+ "scopes": [
338
+ { "id": "global", "label": "Your instructions", "note": null,
339
+ "files": [{ "index": 0, "path": "/Users/me/.claude/CLAUDE.md", "folder": "/Users/me/.claude", "name": "CLAUDE.md", "location": "~/.claude", "scope": "global", "bytes": 512, "modified": "2026-10-01T13:49:28.000Z", "skipped": null, "note": null }] },
340
+ { "id": "project", "label": "Project", "note": null,
341
+ "files": [
342
+ { "index": 1, "path": "/Users/me/src/app/CLAUDE.md", "folder": "/Users/me/src/app", "name": "CLAUDE.md", "location": "app", "scope": "project", "bytes": 2048, "modified": "2026-10-01T13:49:28.000Z", "skipped": null, "note": null },
343
+ { "index": 2, "path": "/Users/me/src/app/AGENTS.md", "folder": "/Users/me/src/app", "name": "AGENTS.md", "location": "app", "scope": "project", "bytes": 900, "modified": "2026-10-01T13:49:28.000Z", "skipped": "Claude Code reads CLAUDE.md instead.", "note": null }
344
+ ] }
345
+ ],
346
+ "fetchedAt": "2026-10-01T14:00:00.000Z"
347
+ }
348
+ ```
349
+
350
+ The files a tool loads when a session starts in `folder`: managed and global
351
+ files first, then project files from the outermost folder down.
352
+ `scope` is `managed`, `global` or `project`, and `location` is the file's
353
+ folder in short. `count` is the number of files that load; a file that exists
354
+ but does not load has `skipped` set to the reason, and a file's `note`
355
+ qualifies one that loads only in part. A scope's `note` says why it lists no
356
+ files. When the tool cannot be asked, `count` is null, `scopes` is empty and
357
+ `note` says why.
358
+
359
+ ### Session
360
+
361
+ ```json
362
+ {
363
+ "id": "b45b6822d158",
364
+ "name": "Claude Code",
365
+ "provider": { "id": "anthropic", "vendor": "Anthropic", "tool": "Claude Code", "color": "#D97757", "monogram": "A", "iconUrl": null },
366
+ "cwd": "/Users/me/src/app",
367
+ "resume": null,
368
+ "task": null,
369
+ "account": { "id": "default", "label": "Default" },
370
+ "clone": null,
371
+ "multiplexer": null,
372
+ "pid": 3518,
373
+ "status": "running",
374
+ "exitCode": null,
375
+ "signal": null,
376
+ "activity": "active",
377
+ "label": "Working",
378
+ "lastOutputAt": "2026-09-30T03:12:01.120Z",
379
+ "createdAt": "2026-09-30T03:10:44.001Z",
380
+ "startedAt": "2026-09-30T03:10:44.001Z",
381
+ "exitedAt": null,
382
+ "cols": 120,
383
+ "rows": 32,
384
+ "attachedClients": 1,
385
+ "model": { "name": "claude-opus-4-5", "displayName": "Opus 4.5", "source": "report" },
386
+ "toolSessionId": "581893e5-a93d-5e49-968b-1c1c277d3255",
387
+ "context": { "compactions": 2, "resets": 0 },
388
+ "reply": { "id": 3 },
389
+ "reporting": { "state": "active", "reason": null },
390
+ "agents": [ /* Agent */ ],
391
+ "shells": [{ "id": "shell-3", "kind": "shell" }]
392
+ }
393
+ ```
394
+
395
+ * `status` is `running` or `exited`. Exited sessions stay listed, with their
396
+ final screen, until a client removes them.
397
+ * `exitedAt` is null while running, then when the process exited.
398
+ * `pid` is null while the process is still starting and after it exits.
399
+ * `activity` is `active` while the terminal is producing output and `quiet`
400
+ after a short pause.
401
+ * `label` is what the session's card shows, for any client to show the same:
402
+ `Working` while running and `active`, else `Running`. Once exited, it is
403
+ `Closed` for a tmux or herdr session, `Exited (<signal>)` when a signal ended
404
+ it, `Exited (<exit code>)` for an exit code other than 0, and otherwise `Exited`.
405
+ * `resume` is the id of the tool's own session that was resumed, or null.
406
+ * `task` is `install` for a session that installs, updates or uninstalls a
407
+ tool, `upgrade` for the session that upgrades the manager (its
408
+ `provider` id is `agent-guild`), `clone` for a `git clone` (its `provider`
409
+ id is `github`), and null for a session that runs the tool itself.
410
+ * `clone` is `{ repo, path, accountId }` for a clone session, else null.
411
+ * `multiplexer` is `{ label, attach, reattachable }` for a Shell session in
412
+ tmux or herdr: the multiplexer's name, the command that reattaches its
413
+ session from a terminal, and whether the exited session can be reattached.
414
+ Null otherwise.
415
+ * `startedAt` is when the session's current process started: `createdAt`,
416
+ or later once a tmux or herdr session is reattached.
417
+ * `account` is the provider account the tool runs under, or null for an
418
+ install or upgrade session.
419
+ * `model` is the main model the tool is using, or null while unknown.
420
+ `source` is `report`, `screen` or `args`.
421
+ * `toolSessionId` is the id the tool gave its own session, or null. It names
422
+ the session in `GET /providers/:id/history`.
423
+ * `context` counts the times the tool compacted its context and started over
424
+ in a new session (`/clear`, `/new`) since the card started.
425
+ * `reply` names the answer that ended the agent's last turn by `id`, or is
426
+ null. Its text is read with `GET /sessions/:id/reply`.
427
+ * `reporting` says whether the tool's reporting hooks work, or is null for a
428
+ tool Agent Guild supplies no hooks to. `state` is `pending`, `active`,
429
+ `unavailable`, `setup_required` or `unsupported`; `reason` explains every
430
+ state but `active`.
431
+ * `shells` lists the shell commands the tool is running, each `{ id, kind }`
432
+ with `kind` `shell` or `monitor`. Nothing about the command itself is
433
+ included.
434
+
435
+ ### Upgrade
436
+
437
+ The manager's own version check, in `GET /info`, the `hello` message and
438
+ `manager.upgrade` events.
439
+
440
+ ```json
441
+ {
442
+ "version": "1.2.0",
443
+ "latestVersion": "1.3.0",
444
+ "available": true,
445
+ "command": "/usr/local/bin/npm install -g --prefix /usr/local @oddessentials/agent-guild@1.3.0",
446
+ "guidance": null,
447
+ "pendingVersion": null,
448
+ "installing": false,
449
+ "lastInstall": null,
450
+ "ptyBuild": null
451
+ }
452
+ ```
453
+
454
+ `version` is the running manager. `available` is true when a newer release
455
+ can be installed; `command` is then what `POST /upgrade` runs, or null with
456
+ `guidance`. `pendingVersion` is a newer version already on disk, which runs
457
+ after a restart (`POST /shutdown` with `restart`). `lastInstall` is the last
458
+ upgrade session's `{ outcome, exitCode, version, installedVersion, at }`, or
459
+ null. `installing` is true while an upgrade runs, and `available`, `command`,
460
+ `guidance`, `pendingVersion`, `lastInstall` and `ptyBuild` are withheld until
461
+ it ends.
462
+
463
+ After the upgraded manager starts, refresh every open browser page.
464
+ Reconnecting does not reload a page's JavaScript, and stale frontend code is
465
+ not guaranteed to work with the new manager.
466
+
467
+ `ptyBuild` is null where node-pty runs from its bundled builds. Elsewhere,
468
+ such as Alpine, it is `{ command, built }`: the command that compiles
469
+ node-pty, and whether that build is on disk. `POST /shutdown` with `restart`
470
+ answers 409 `pty_unavailable` until it is.
471
+
472
+ ### GitHub
473
+
474
+ The GitHub accounts signed in to Agent Guild, in `GET /github`. Tokens never
475
+ leave the manager.
476
+
477
+ ```json
478
+ {
479
+ "scopes": ["repo", "write:public_key"],
480
+ "appUrl": "https://github.com/settings/connections/applications/Ov23lif6qqYKtXZTb130",
481
+ "keysUrl": "https://github.com/settings/keys",
482
+ "newKeyUrl": "https://github.com/settings/ssh/new",
483
+ "tools": { "git": true, "ssh": true, "sshKeygen": true },
484
+ "signIn": { "status": "pending", "userCode": "WDJB-MJHT", "verificationUri": "https://github.com/login/device", "expiresAt": "2026-10-01T14:15:00.000Z", "accountId": null, "again": false, "error": null },
485
+ "accounts": [
486
+ {
487
+ "id": 4242, "login": "octo-cat", "name": "Octo Cat", "avatar": "data:image/png;base64,...", "scopes": ["repo", "write:public_key"],
488
+ "needsSignIn": false, "addedAt": "2026-10-01T14:00:00.000Z",
489
+ "ssh": { "status": "ready", "key": "/home/me/.config/agent-guild/github/keys/agent-guild-github-4242", "publicKey": "ssh-ed25519 AAAA... agent-guild github octo-cat (4242)", "verifiedAt": "2026-10-01T14:01:00.000Z", "settingUp": false, "error": null }
490
+ }
491
+ ]
492
+ }
493
+ ```
494
+
495
+ Sign-in uses GitHub's device flow: `signIn` is `pending` while the user
496
+ enters `userCode` at `verificationUri`, then `done` (with `accountId`, and
497
+ `again` when that GitHub user was already signed in), `expired`, `denied` or
498
+ `failed`; null when none was started or it was cancelled. Accounts are keyed
499
+ by GitHub's numeric user id; `login` is display only. `needsSignIn` is true
500
+ once GitHub refuses the account's token. `ssh.status` is `none`, `unverified`
501
+ or `ready`. `ssh.error` is `{ code, message, manual }`, with `manual` true
502
+ when the user must add `publicKey` on GitHub themselves.
503
+
504
+ ### Repository views
505
+
506
+ Issues, workflow runs and open pull requests of one repository, read with one
507
+ signed-in account. Each list holds one page of GitHub's results;
508
+ `truncated` is true when GitHub has more.
509
+
510
+ ```json
511
+ { "issues": [{ "number": 4, "title": "Dock is too narrow", "body": "Steps", "state": "open", "user": "octo-cat", "comments": 2, "updatedAt": "2026-10-01T00:00:00.000Z", "url": "https://github.com/octo-cat/agent-guild/issues/4" }],
512
+ "truncated": false, "url": "https://github.com/octo-cat/agent-guild/issues" }
513
+ { "runs": [{ "id": 11, "name": "CI", "title": "Fix the gate", "branch": "main", "event": "push", "status": "in_progress", "conclusion": null, "runNumber": 12, "updatedAt": "2026-10-02T00:00:00.000Z", "url": "https://github.com/octo-cat/agent-guild/actions/runs/11" }],
514
+ "running": true, "service": null, "truncated": false, "url": "https://github.com/octo-cat/agent-guild/actions" }
515
+ { "pulls": [{ "number": 8, "title": "Add viewer", "draft": true, "user": "ada", "head": "viewer", "base": "main", "updatedAt": "2026-10-02T00:00:00.000Z", "url": "https://github.com/octo-cat/agent-guild/pull/8" }],
516
+ "truncated": false, "url": "https://github.com/octo-cat/agent-guild/pulls" }
517
+ ```
518
+
519
+ `running` is true while any listed run has not finished. `service` is null
520
+ while GitHub reports Actions operational or its status cannot be read, else `{ status, incident, url }`
521
+ from githubstatus.com. 409 `github_sign_in` means the account must sign in
522
+ again.
523
+
524
+ Branches come a page at a time, as
525
+ `{ branches, nextPage, defaultBranch, metadataError, fetchedAt, url }`, each
526
+ branch `{ name, sha, protected, url }`. Follow `nextPage` until it is null,
527
+ even after a short or empty page. Only page one has `defaultBranch`;
528
+ `metadataError` says why it could not be read.
529
+
530
+ Activity is the account's own work in the repository from `since` (an ISO
531
+ 8601 time) on:
532
+
533
+ ```json
534
+ { "since": "2026-10-01T00:00:00.000Z",
535
+ "merged": [{ "repo": "octo-cat/agent-guild", "number": 8, "title": "Add viewer", "mergedAt": "2026-10-02T00:00:00.000Z", "url": "https://github.com/octo-cat/agent-guild/pull/8" }],
536
+ "closed": [{ "number": 4, "title": "Dock is too narrow", "closedAt": "2026-10-02T00:00:00.000Z", "url": "https://github.com/octo-cat/agent-guild/issues/4" }],
537
+ "passed": [{ "id": 11, "name": "CI", "branch": "main", "event": "push", "createdAt": "2026-10-02T00:00:00.000Z", "url": "https://github.com/octo-cat/agent-guild/actions/runs/11" }],
538
+ "truncated": false }
539
+ ```
540
+
541
+ `merged` are pull requests the account opened that were merged at or after
542
+ `since`, in the repository or, for a fork, from it into its parent (`repo`
543
+ says which). `closed` are issues the account closed as completed at or after
544
+ `since`. `passed` are workflow runs created at or after `since` by the
545
+ account that succeeded. A run created earlier does not count even if it
546
+ ran, or was re-run successfully, later. Accounts are matched by GitHub's
547
+ numeric user id; the login used to query runs is refreshed first.
548
+
549
+ Each listing reads at most three pages of 100, and at most 20 candidate
550
+ issue closures are checked. `truncated` is true when a cap leaves more to
551
+ examine, so matching work may be missing. Listings are read newest first
552
+ by update time for pull requests and issues, and creation time for runs.
553
+ An empty list is what GitHub returned; during a GitHub service incident it
554
+ has returned empty lists that were not complete, so it is not proof of no
555
+ activity.
556
+
557
+ ### Agent
558
+
559
+ An agent is a worker that the coding tool reports inside a session, such as a
560
+ Claude Code sub-agent. See [agent-reporting.md](agent-reporting.md).
561
+
562
+ ```json
563
+ {
564
+ "id": "hook-task-c9df2e9f7dd4e090",
565
+ "name": "codebase_investigator",
566
+ "kind": "subagent",
567
+ "status": "working",
568
+ "detail": "Map the auth flow",
569
+ "startedAt": "2026-09-30T03:11:02.000Z",
570
+ "updatedAt": "2026-09-30T03:11:02.000Z",
571
+ "source": "api"
572
+ }
573
+ ```
574
+
575
+ `status` is one of `working`, `waiting`, `idle` or `done`. An agent reported
576
+ as `done` stays visible for a few seconds and is then removed. `source` is
577
+ `api` for a report over HTTP, `terminal` for one written to the terminal, and
578
+ `herdr` for an agent in a herdr session's panes. All agents are cleared when
579
+ their session exits.
580
+
581
+ ## HTTP endpoints
582
+
583
+ All paths are under `/api/v1`.
584
+
585
+ | Method | Path | Body | Result |
586
+ | --- | --- | --- | --- |
587
+ | GET | `/health` | | `{ ok, name, version, pid }`. No token needed. |
588
+ | POST | `/sign-in` | | `201 { signIn: { code, device, expiresAt, interval } }`: a device sign-in to show as `code` and poll with `device` every `interval` seconds. No token needed. 429 `sign_in_busy` while five are live or one was made under two seconds ago. |
589
+ | POST | `/sign-in/poll` | `{ device }` | `{ signIn: { status } }`: `pending`, `approved` with `token`, or `denied`. An approved or denied answer is given once; after it, and after `expiresAt`, 404 `sign_in_unknown`. No token needed. |
590
+ | GET | `/sign-in` | | `{ signIn: { requests } }`: the pending device sign-ins, each `{ code, requestedAt, expiresAt }`. |
591
+ | POST | `/sign-in/approve` | `{ code }` | `{ signIn }` as above, after approving that code. 404 `sign_in_unknown` when no pending sign-in has it. |
592
+ | POST | `/sign-in/deny` | `{ code }` | `{ signIn }` as above, after denying it. |
593
+ | GET | `/info` | | `name`, `version`, `pid`, platform, start time, provider config warnings, `upgrade` (an Upgrade object), `launcher` (the double-click launcher in a checkout, else null), `folderOpener` (`{ available, label, reason }` for this client) and `remoteAccess` (`{ available: true }` when this manager can configure it, else null). |
594
+ | GET | `/remote-access` | | `{ remoteAccess }`: `revision`, `mode`, `busy`, `pending`, `problem` and connection details. `url` is the active address. `candidate` is the address enabling would use with Tailscale's current settings, `{ url, existing, remembered, standardUrl? }`: `remembered` when it is the address used before, and `standardUrl` when `standard: true` would choose another. |
595
+ | POST | `/remote-access/check` | | `{ remoteAccess }` after refreshing Tailscale's status. |
596
+ | PUT | `/remote-access` | `{ revision, action, adopt?, hosts?, origins? }` | `202`. `action` is `enable` (`adopt: true` manages a matching existing route; `standard: true`, only while remote access is off, skips the remembered address and takes port 443, else the next free port), `disable`, `custom` (with `hosts` and `origins`) or `forget` (leave an inactive route awaiting cleanup in Tailscale). Poll until `busy` is null and read `problem`. 409 `stale_settings` for an old `revision`. Needs the API token. Revoked WebSockets close with code 4403; their terminals keep running. |
597
+ | GET | `/folders?path=` | | `{ path, parent, home, segments, roots, entries, truncated, note }`: the subfolders of `path` (blank or `~` for the home folder). `entries` are `{ name, path, hidden }`. A missing `path` lists the nearest folder above it and names the requested path in `note`. |
598
+ | POST | `/folders` | `{ path, name }` | 201 with the `GET /folders` listing of the new folder `name`, made inside `path`. |
599
+ | GET | `/autostart` | | `{ autostart: { available, enabled, reason, note?, lastRun?, log? } }`: whether the manager starts when the user signs in. `reason` says why it is unavailable. `lastRun` is null or `{ at, outcome }`. On Linux outside WSL it also has `mode` (`off`, `sign-in`, `boot` or `both`) and `boot`, the systemd service's `{ available, enabled, reason, note, user, linger, state, commands }`. |
600
+ | PUT | `/autostart` | `{ enabled }` or `{ mode }` | `{ autostart }` as above. `mode` (`off`, `sign-in` or `boot`) makes that the one way the manager starts; `boot` is offered only on Linux with systemd. |
601
+ | POST | `/open-folder` | `{ cwd? }` | `{ ok }`: opens the folder in the computer's file manager. 403 `local_only` from clients that reach the manager by any address other than its own loopback ones. |
602
+ | GET | `/notes` | | `{ notes: { revision, text } }`. One notepad for every client of this manager. `revision` is null until the first save. |
603
+ | PUT | `/notes` | `{ revision, text }` | `{ notes }` with a new `revision`. `revision` is the one this edit started from, or null to create the notepad. 409 `stale_notes` when that revision is no longer current; `error.notes` is the current notepad, so a client can retry or adopt it. |
604
+ | POST | `/upgrade` | | `201 { session }`: a session with `task` `upgrade` running the Upgrade `command`. Sessions keep running; the new version is used after the manager restarts. |
605
+ | GET | `/providers` | | `{ providers: Provider[], revision }`. `revision` identifies the contents of `providers.json`, or is null when there is no file. |
606
+ | POST | `/providers/reload` | | `{ providers, revision, warnings }`: re-reads `providers.json` and checks every tool's versions again. New sessions use the new settings. |
607
+ | POST | `/providers/:id/accounts` | `{ label, revision }` | `201 { account, dir, providers, revision }`: adds an account to `providers.json`. Its id is made from `label` and does not change. |
608
+ | PATCH | `/providers/:id/accounts/:account` | `{ label, revision }` | `{ account, dir, providers, revision }`: renames an account, including `default`. |
609
+ | DELETE | `/providers/:id/accounts/:account?revision=` | | `{ account, dir, providers, revision }`: removes an account from `providers.json`. Its home folder, `dir`, stays on disk. 409 `default_account` for `default`; 409 `account_in_use` while a running session uses it. |
610
+ | POST | `/providers/:id/reporting` | `{ enabled }` | `{ provider }`: turns agent or model reporting on or off for a tool that has `reportingEnabled`. |
611
+ | POST | `/providers/:id/install` | `{ force? }` | `201 { session }`: a session that installs or updates the tool. 409 `provider_in_use` (with `running`, the session count) while the provider's sessions are running; retry with `force: true` after the user confirms. |
612
+ | POST | `/providers/:id/uninstall` | `{ path, force? }` | `201 { session }`: a session that removes the copy at `path`, one of the provider's `installs`. `force` as for `install`. |
613
+ | GET | `/environment?scope=&cwd=&id=` | | An environment snapshot; see [Manager environment](#manager-environment). |
614
+ | POST | `/environment/refresh` | `{ scope?, cwd?, id? }` | `202` with that scope's snapshot. |
615
+ | GET | `/usage` | | `{ usage: Usage[] }`, one per account of every provider with a `usageSource`. |
616
+ | GET | `/model-stats` | | `{ retrievedAt, stale, error, stats, pool, providers, models, sessions }`: benchmarks for each provider's models, from OpenRouter's public model list. `stats` describes each benchmark; `providers[id]` is `{ featured, models }`; `models[id]` holds a model's name, context, price and, per benchmark, its `value`, `rank`, `place`, `tied`, `of`, `level` (0-100) and `tier` (S to D); `sessions[id]` is the model id a session's model matched, or null. |
617
+ | GET | `/news` | | `{ refreshedAt, refreshing, sources, items }`. `items` are the built-in feeds' recent items, newest first, each `{ id, title, url, discussion, summary, source, sourceId, category, publishedAt }` with `category` `news`, `releases` or `research`. `sources` lists each feed as `{ id, name, category, error, okAt }`. A `news.updated` event follows a refresh. |
618
+ | GET | `/yard-art` | | `{ yardArt }`: the Yard's art. `state` is `ready`, `missing`, `downloading` (with `received` bytes), `installing`, `failed` or `unavailable` (each with `error`), or `cancelled`; `size` is the pack's size in bytes. A checkout serves `web/yard/assets` and is always `ready`. `/yard/assets/` answers 404 until the art is `ready`. |
619
+ | POST | `/yard-art/download` | | `202 { yardArt }`: starts the one download of the art pack named by the package, unless it is running or installed. Progress and the outcome arrive as `yard-art.updated`. |
620
+ | POST | `/yard-art/cancel` | | `{ yardArt }`: stops the download for every client; the state becomes `cancelled`. |
621
+ | GET | `/changelog` | | `{ refreshing, okAt, error, releases }`: Agent Guild's own releases, newest first, each `{ version, url, publishedAt, sections: [{ title, changes }] }`, where `title` may be null and a change is a list of text runs. A `changelog.updated` event follows a refresh. |
622
+ | GET | `/providers/:id/history?account=&limit=&cwd=&q=` | | `{ history }`: a History object for one account (default `default`). |
623
+ | GET | `/providers/:id/history/detail?account=&id=&cursor=` | | `{ detail }`; see [Saved messages](#saved-messages). |
624
+ | GET | `/providers/:id/flags` | | `{ flags, error }`: the provider's `launchFlags` that its installed tool's `--help` lists, each `{ flag, value, placeholder, about, risky, example }`, where `value` is `required`, `optional` or `none`, `placeholder` and `about` are the tool's own, `risky` marks a flag that skips approvals, and `example` is a sample value or null. `error` says why the help could not be read. |
625
+ | GET | `/providers/:id/memory?account=&cwd=` | | `{ memory }`: a Memory object for one account and the working folder `cwd` (the home folder when empty). |
626
+ | GET | `/providers/:id/memory/file?account=&cwd=&scope=&path=` | | `{ file: { scope, path, bytes, modified, truncated, text } }`: the start of one listed file. |
627
+ | GET | `/providers/:id/instructions?account=&cwd=` | | `{ instructions }`: an Instruction files object for one account and the working folder `cwd` (the home folder when empty). |
628
+ | GET | `/providers/:id/instructions/file?account=&cwd=&index=&path=` | | `{ file: { index, scope, path, bytes, modified, truncated, text } }`: the start of file `index` of a freshly resolved list. 404 `instruction_file_gone` when that entry is no longer `path`. |
629
+ | GET | `/github` | | `{ github }`: a GitHub object. |
630
+ | POST | `/github/sign-in` | | `202 { github }`: starts a device-flow sign-in; `github.updated` events follow. |
631
+ | DELETE | `/github/sign-in` | | `{ github }`: cancels it. |
632
+ | DELETE | `/github/accounts/:id` | | `{ github }`: forgets the account's sign-in. |
633
+ | POST | `/github/accounts/:id/ssh` | | `{ account }`: sets up the account's SSH key on GitHub. A failure is reported in `account.ssh.error`. |
634
+ | GET | `/github/accounts/:id/repos?parent=&refresh=1` | | `{ repos: { accountId, fetchedAt, truncated, owners, parent, repos } }`: the account's repositories, most recently pushed first, each `{ fullName, owner, ownerType, name, private, fork, archived, description, language, pushedAt, url, target, local }`. `owners` are where a new repository can be created. With `parent`, `target` is `<parent>/<name>` and `local` is `absent`, `cloned` or `conflict`. |
635
+ | POST | `/github/accounts/:id/repos` | `{ owner, name, description?, private?, readme? }` | `201 { repo }`: creates a repository, private unless `private` is false, with a README unless `readme` is false. |
636
+ | GET | `/github/repos?refresh=1` | | `{ repos, truncated, errors, fetchedAt }`: every signed-in account's repositories in one list, each with `accountId` and `login`. An account that fails adds `{ accountId, login, code, message }` to `errors`. |
637
+ | GET | `/github/origin?cwd=` | | `{ folder, repo }`: `repo` is the lowercase `owner/name` of the GitHub origin of the Git work tree holding `cwd`, else null. |
638
+ | GET | `/github/accounts/:id/repos/:owner/:name/issues?state=` | | Issues (see [Repository views](#repository-views)). `state` is `open` (default), `closed` or `all`. |
639
+ | POST | `/github/accounts/:id/repos/:owner/:name/issues` | `{ title, body? }` | `201 { issue }`. |
640
+ | PATCH | `/github/accounts/:id/repos/:owner/:name/issues/:number` | `{ title?, body?, state? }` | `{ issue }`. `state` is `open` or `closed`. |
641
+ | GET | `/github/accounts/:id/repos/:owner/:name/actions` | | Workflow runs (see [Repository views](#repository-views)). |
642
+ | GET | `/github/accounts/:id/repos/:owner/:name/branches?page=` | | Branches (see [Repository views](#repository-views)). |
643
+ | GET | `/github/accounts/:id/repos/:owner/:name/pulls` | | Open pull requests (see [Repository views](#repository-views)). |
644
+ | GET | `/github/accounts/:id/repos/:owner/:name/activity?since=` | | The account's own recent work (see [Repository views](#repository-views)). 400 `bad_since` unless `since` is a time. |
645
+ | POST | `/github/clone` | `{ account, repo, parent }` | `201 { session }`: a session with `task` `clone` running `git clone` for `repo` (owner/name) into `<parent>/<name>`. |
646
+ | GET | `/sessions` | | `{ sessions: Session[] }` |
647
+ | POST | `/sessions` | `{ providerId, account?, shell?, cwd?, cols?, rows?, name?, args?, resume? }` | `201 { session }`. 429 `too_many_sessions` at the session limit. |
648
+ | GET | `/sessions/:id` | | `{ session }` |
649
+ | GET | `/sessions/:id/reply` | | `{ reply: { id, text, spoken } }`, or `{ reply: null }`: the whole text of the answer `session.reply` names, and `spoken`, the words the page's Read aloud speaks for it: a line per paragraph line, heading, list item and table row, each fenced code block as `Code omitted.` |
650
+ | PATCH | `/sessions/:id` | `{ name }` | `{ session }`. |
651
+ | POST | `/sessions/:id/stop` | | `{ session }`: ends the process. The session stays listed as exited. |
652
+ | POST | `/sessions/:id/reattach` | | `{ session }`: runs an exited tmux or herdr session's multiplexer client again, keeping the session's id and screen. 409 `multiplexer_session_gone` while `multiplexer.reattachable` is false. |
653
+ | DELETE | `/sessions/:id` | | `{ ok: true }`: ends the process if needed and removes the session. |
654
+ | POST | `/sessions/:id/agents` | Agent report | `{ agent }`, or `{ agent: null }`. |
655
+ | POST | `/sessions/:id/model` | `{ model, displayName? }` | `{ model }`. Sets the session's model with source `report`. |
656
+ | POST | `/sessions/:id/tool-session` | `{ toolSessionId, source? }` | `{ toolSessionId }`. Records the id the tool gave its own session. A new id with `source` `clear`, `startup` or `new` counts a reset. |
657
+ | POST | `/sessions/:id/context` | `{ compaction, key? }` | `{ context }`. Counts a compaction, `manual` or `auto`. A repeated `key`, or with none a report within 2 s of the last, is the same one. |
658
+ | POST | `/sessions/:id/reply` | `{ reply }` | `{ reply: { id } }`, or `{ reply: null }`. `reply` is the text of the answer that ended the turn, as Markdown, or null. |
659
+ | POST | `/sessions/:id/reporting` | | `{ reporting }`: the tool's hooks announce themselves. |
660
+ | POST | `/sessions/:id/docker` | `{ event, sessionId, agentName?, toolName?, toolUseId?, command?, model?, reply? }` | `{ ok }`. One Docker Agent hook event. |
661
+ | POST | `/sessions/:id/shells` | `{ shell, key \| task, match?, agentId?, persist?, endsWithAgent?, kind?, tasks? }` | `{ ok }`. A shell command the tool started, ended or put in the background. |
662
+ | POST | `/shutdown` | `{ force?, restart? }` | `202 { ok, running, restart }`: stops the manager and every session, detaching tmux and herdr sessions. 409 `sessions_running` (with `running`) while other sessions run; retry with `force: true` after the user confirms. With `restart`, a new manager takes over on the same port and token, running the version on disk. |
663
+
664
+ `cwd` defaults to the user's home folder, and a leading `~` is expanded.
665
+ `args` are appended to the provider's configured arguments. `resume` is an
666
+ id or name of one of the tool's own sessions; one that starts with `-` is
667
+ refused with 400 `bad_resume`. `account` defaults to `default`.
668
+
669
+ The `POST /sessions/:id/…` report routes (`agents`, `model`, `tool-session`,
670
+ `context`, `reply`, `reporting`, `docker` and `shells`) also accept the per-session
671
+ report token instead of the API token, in an `X-Agent-Guild-Report-Token`
672
+ header. The manager gives that token only to the processes inside that
673
+ session.
674
+
675
+ Request bodies are limited to 64 KiB, except `PUT /notes`,
676
+ `POST /sessions/:id/reply` and `POST /sessions/:id/docker`, which accept
677
+ 1 MiB. Messages a client sends on a WebSocket are limited to 1 MiB.
678
+
679
+ ## WebSockets
680
+
681
+ Messages in both directions are JSON text frames.
682
+
683
+ ### `GET /api/v1/events`
684
+
685
+ This socket pushes changes to every session. A client sends nothing on it
686
+ but `{ type: "ping" }`, which the manager answers with `{ type: "pong" }`.
687
+
688
+ | Message | Meaning |
689
+ | --- | --- |
690
+ | `{ type: "hello", version, pid, platform, startedAt, launcher, folderOpener, remoteAccess, upgrade, yardArt, sessions, heartbeat, notesRevision, notesUnreadable }` | Sent first: the manager (as in `/info`), the full session list and the notepad's revision. |
691
+ | `{ type: "session.created", session }` | A session was started by any client. |
692
+ | `{ type: "session.updated", session }` | Status, activity, agents, name or size changed. |
693
+ | `{ type: "session.removed", sessionId }` | A session was removed. |
694
+ | `{ type: "providers.updated", providers, revision }` | The provider list changed. |
695
+ | `{ type: "environment.updated", environment }` | An environment check started or finished. |
696
+ | `{ type: "news.updated" }` | Fetch `/news` again. |
697
+ | `{ type: "yard-art.updated", yardArt }` | The Yard's art changed state or made download progress, as in `/yard-art`. |
698
+ | `{ type: "changelog.updated" }` | Fetch `/changelog` again. |
699
+ | `{ type: "github.updated" }` | Fetch `/github` again. |
700
+ | `{ type: "remote-access.updated" }` | Fetch `/remote-access` again. |
701
+ | `{ type: "sign-in.updated" }` | Fetch `/sign-in` again. |
702
+ | `{ type: "notes.updated", notes }` | The notepad was saved. `notes` is `{ revision, text }`. |
703
+ | `{ type: "manager.upgrade", upgrade }` | The manager's own version check changed. |
704
+ | `{ type: "manager.stopping", running, restart }` | A client asked the manager to stop; `running` sessions are being ended. `restart` is true when a new manager will take over. |
705
+ | `{ type: "manager.stopped", remaining, restart }` | The last event before the socket closes. `remaining` is how many sessions had not confirmed their exit. |
706
+
707
+ After a reconnect, treat `hello` as the new source of truth.
708
+
709
+ ### `GET /api/v1/sessions/:id/terminal`
710
+
711
+ This socket is the interactive terminal for one session. Any number of
712
+ clients may attach to the same session. A client that may size the terminal
713
+ adds `page=<id>` to the URL, an id of 1 to 64 letters, digits, `-` or `_` that
714
+ names one page load. The terminal has one size: the page that last sent
715
+ `claim` owns it, and only that page's `resize` messages under its latest claim
716
+ change it. The size stays when the owner disconnects.
717
+
718
+ Server to client:
719
+
720
+ | Message | Meaning |
721
+ | --- | --- |
722
+ | `{ type: "snapshot", data, cols, rows, generation, owner, session }` | Always first. `owner` says whether this page owns the size, and `generation` counts claims. `data` is a VT escape sequence stream that redraws the current screen and scrollback. Reset the terminal and write it. |
723
+ | `{ type: "data", data }` | Terminal output, in order, directly after the snapshot. |
724
+ | `{ type: "resize", cols, rows, generation, owner }` | The size changed or was claimed, or this page's own `resize` was refused or changed nothing. |
725
+ | `{ type: "exit", exitCode, signal }` | The process ended. Also sent after the snapshot when attaching to an exited session. |
726
+ | `{ type: "removed" }` | The session was removed. The socket then closes with code 4410. |
727
+ | `{ type: "pong" }` | The answer to a client's `ping`. |
728
+ | `{ type: "error", code: "not_found", message }` | The session does not exist. The socket then closes with code 4404. |
729
+
730
+ Client to server:
731
+
732
+ | Message | Meaning |
733
+ | --- | --- |
734
+ | `{ type: "input", data }` | Keystrokes or pasted text, exactly as a terminal would send them. |
735
+ | `{ type: "claim", cols, rows }` | Take the size for this page and set it. Needs `page`. |
736
+ | `{ type: "resize", generation, cols, rows }` | Resize the terminal. Refused unless this page owns the size and `generation` is the latest. |
737
+ | `{ type: "ping" }` | Ask for a `pong`, to check that the link is alive. |
738
+
739
+ ### Terminal queries: clients must not answer
740
+
741
+ Programs ask the terminal questions by writing escape sequences, for
742
+ example "where is the cursor?" (`CSI 6 n`). The manager answers these once
743
+ per session from its own copy of the screen, whether zero or many clients
744
+ are attached. A client that renders the terminal must therefore not answer
745
+ them itself, or the program receives duplicate replies as keyboard input.
746
+ A client built on xterm.js can copy `suppressQueryReplies` from `web/app.js`.
747
+
748
+ Close codes: `4404` means the session does not exist, `4410` means it was
749
+ removed, and `4008` means the client fell too far behind. After a `4008` or a
750
+ network drop, reconnect and the snapshot brings the client up to date.
751
+
752
+ ## The page in a VS Code tab
753
+
754
+ The manager's page may be framed only by VS Code webviews: its
755
+ `Content-Security-Policy` has `frame-ancestors vscode-webview: vscode-file:`.
756
+ The frame's `src` is the page URL with `#token=<token>`, as for a browser.
757
+ Adding `&view=yard` or `&view=cards` opens that view and keeps it as the
758
+ user's choice, as Settings does. Adding `&layout=compact` instead shows the
759
+ Yard's scene alone, to fill a small view, and keeps nothing: `&skin=<id>` and
760
+ `&theme=light` or `&theme=dark` show that look, and opening a session asks
761
+ the host for the full page. A host with no full page adds `&expand=none`, and
762
+ the page then neither shows its controls that ask for one nor sends `expand`.
763
+ A host whose device can't run the Yard adds `&yard=none`: the page shows the
764
+ Cards, doesn't offer the Yard, ignores `&view=yard`, `&layout=compact` and
765
+ the `view` message's `"yard"`, and never loads the scene or downloads the
766
+ Yard's art.
767
+ A browser ignores all of these.
768
+
769
+ A framed page cannot open windows, so it posts messages to `window.parent`.
770
+ A host must accept a message only from its own frame and the manager's
771
+ origin, and open only `http:` and `https:` URLs.
772
+
773
+ | Message | Meaning |
774
+ | --- | --- |
775
+ | `{ source: "agent-guild", type: "ready", version }` | The page reached the manager. `version` is the manager's. |
776
+ | `{ source: "agent-guild", type: "open", url }` | The user followed a link; open `url` in the browser. |
777
+ | `{ source: "agent-guild", type: "look", skin, theme }` | The page's skin and theme, on load and whenever they change. A compact page does not send it. |
778
+ | `{ source: "agent-guild", type: "expand", session }` | A compact page asks for the full page, showing the terminal of `session` when it is present. |
779
+
780
+ A host may post these messages to the page. The page accepts them only from
781
+ `window.parent`. Send them once the frame has fired `load`; the page listens
782
+ from then on.
783
+
784
+ | Message | Meaning |
785
+ | --- | --- |
786
+ | `{ source: "agent-guild-vscode", type: "view", view }` | Show `view`, `"cards"` or `"yard"`, and keep it as the user's choice. An open terminal closes first; its sessions keep running. A page opened with `&yard=none` ignores `"yard"`. |
787
+ | `{ source: "agent-guild-vscode", type: "session", id }` | Open the terminal of session `id` once, as soon as the page knows that session. |
788
+
789
+ ## The page in the Agent Guild app
790
+
791
+ The app for iPhone, iPad and Apple Vision Pro loads the page URL with
792
+ `#token=<token>` in a `WKWebView` and registers a script message handler
793
+ named `agentGuild`. When the page is not framed and that handler exists, the
794
+ page treats the app as its host: `&view=yard`, `&view=cards`,
795
+ `&layout=compact` and `&yard=none` work as for VS Code, and the page posts the same `ready` and `open` messages to
796
+ `window.webkit.messageHandlers.agentGuild` instead of `window.parent`.
797
+ Without the handler, the page behaves as in a browser.
798
+
799
+ The page's checks do not protect the app. A script message handler is
800
+ reachable from every frame, and the page embeds a YouTube frame, so the app
801
+ must:
802
+
803
+ - accept a message only when `WKScriptMessage.frameInfo.isMainFrame` is true
804
+ and the frame's `securityOrigin` is the manager's (CVE-2026-44698 is this
805
+ bug in Home Assistant's iOS app);
806
+ - accept only the messages in the table above, with a string `type` and the
807
+ fields it names;
808
+ - open only `http:` and `https:` URLs, outside the `WKWebView`, and keep the
809
+ `WKWebView` from navigating away from the manager's origin, redirects
810
+ included.
811
+
812
+ To show a view while the page is open, the app runs
813
+ `window.postMessage({ source: "agent-guild-app", type: "view", view }, location.origin)`
814
+ in the page with `evaluateJavaScript` once navigation has finished. The page
815
+ accepts it only from its own window at its own origin, with the behavior of
816
+ the VS Code message.
817
+
818
+ ### Android
819
+
820
+ The Android app loads the same URL in a `WebView` and registers a web message
821
+ listener named `agentGuild` with AndroidX WebKit's
822
+ `WebViewCompat.addWebMessageListener`, whose allowed origin rules name only the
823
+ manager's origin. When the page is not framed, has no Apple handler, and
824
+ `window.agentGuild.postMessage` is a function, the page treats the app as its
825
+ host, as above. That object carries only strings, so the page posts each
826
+ message as JSON text: `JSON.stringify({ source: "agent-guild", type, ... })`.
827
+
828
+ The page's checks do not protect the app, so the app must:
829
+
830
+ - accept a message only when `isMainFrame` is true and `sourceOrigin` is
831
+ exactly the manager's origin;
832
+ - parse it as JSON and accept only the messages in the table above, with a
833
+ string `type` and the fields it names;
834
+ - open only `http:` and `https:` URLs, outside the `WebView`, and keep the
835
+ `WebView`'s main frame on the manager's origin: its first load, redirects,
836
+ new windows and loads after a renderer restart included.
837
+
838
+ The app shows a view the same way, with `evaluateJavascript` once
839
+ `onPageFinished` has run.
840
+
841
+ ## The page on Meta Ray-Ban Display
842
+
843
+ `/glasses.html` is a 600 × 600 page for Meta's display glasses. It reads
844
+ `#token=<token>` as the page does, then makes only `GET /api/v1/sessions`
845
+ (every 5 seconds) and `GET /api/v1/sessions/:id/reply`, and speaks `spoken`.
846
+ It has no terminal and sends nothing, but the token it holds is the full API
847
+ token. **Settings → Meta glasses** shows a QR code of Meta's install link
848
+ for it, `fb-viewapp://web_app_deep_link?appName=Agent%20Guild&appUrl=<URL>`,
849
+ built from one of the manager's allowed HTTPS origins: the one the page is
850
+ open on, then the Tailscale address, then the other reverse proxy origins.