docs: what a daemon client needs that only the source said (#2490)

Someone writing a client for the daemon's HTTP API can find what they need in the reference docs instead of the source. A new page, docs/reference/daemon-http-clients.md, covers finding the daemon: rig status --json is the discovery surface, the status commands check OPENRIG_URL before any state file, start and stop read the state file first, and pid and port appear only when known. It also covers the /healthz fields and when each is present, the two bearer tokens with the routes each checks and their 401 bodies, the three event streams and their replay rules, and route details: the recent-transitions limit rule, urgent on POST /api/transport/send, the scope approve, proof judge and workflow instantiate bodies and refusals, the per-seat Claude provider usage readings (not proof that two seats share an account), and how the slice routes resolve a name two missions share. Existing pages now list every reason a send is retained, say that node inventory reads exited for a verified exited pane, say how to type into a seat without resizing it, and list the files OpenRig writes into a project, with what rig down does to each (protection is per shared file, and legacy block-only files can be removed). Docs only; no behaviour change. Limits: the API changes these reports also ask for are not part of this change; the status lookup summary doesn't name its last fallback, the configured endpoint, which rig status --json already uses. Reported in #2486 and #2487 by @ziadeh.
This commit is contained in:
Mike Schwarz authored and GitHub committed 2026-11-04 12:00:51 -08:00
1 parent c4b1181dc4
commit 432a6bcc41
5 files changed
+171 -4

No files matched your search

+23
View File
@@ -735,6 +735,29 @@ When authoring startup content, note which instructions are runtime-specific. Fo
---
## Files OpenRig writes into a project
These are the files OpenRig writes into a seat's working directory, and what `rig down` does with each. Paths are
relative to that directory.
| Path | When OpenRig writes it | What `rig down` does |
| --- | --- | --- |
| Managed blocks in `CLAUDE.md` (or the rig's `managed_blocks` file, such as `CLAUDE.local.md`) or `AGENTS.md` | Each launch merges the seat's guidance blocks. A fresh Codex launch that takes developer instructions puts them in its launch text instead: in `AGENTS.md` it adds only the seat-skills line, after your yes, and refreshes blocks already there. A fresh Pi seat writes its own agent-directory `AGENTS.md` outside the project, except when that folder or file is a link: then its guidance stays in the repository `AGENTS.md`. | Removes OpenRig's blocks, current or legacy, and keeps everything else. When nothing else is left, it deletes the file if the file began with an OpenRig block, or held only blocks in an older format. A block-only file isn't proof that OpenRig created it. The seat-skills line is kept if you edited it. |
| `.openrig/context-collector.cjs` | Claude seats, at launch. | Removes it while its bytes are still the shipped collector's; otherwise keeps it and says so. |
| `.claude/settings.local.json` | Claude seats, at launch: OpenRig's status line and its activity hooks are merged in, separately. Your own status line is never replaced, but the activity hooks are still merged beside it. A file that doesn't parse gets neither. | Removes OpenRig's status line and hooks and keeps the file, even if `{}` is all that's left. |
| `.claude/settings.local.json` fragments and `.mcp.json` | When a profile or spec declares `claude_settings_fragment` or `claude_mcp_fragment`. | Leaves the merged content in place. |
| `.claude/agents/`, `.claude/plugins/<id>/`, `.claude/extensions/<id>/`; `.agents/<id>.yaml`, `.codex/plugins/<id>/`, `.agents/extensions/<id>/` | When the seat's projection includes subagents, plugins or runtime resources (Claude, then Codex). | Leaves them in place. |
| `.claude/skills/<id>/`, `.agents/skills/<id>/` | Only for a seat without seat storage. New Claude and Codex seats keep their skills under `$OPENRIG_HOME/state/seats/`, outside the project. | Leaves them in place. |
| The managed compaction preparation folder described in [Claude compaction preparation directory](#claude-compaction-preparation-directory), with its `.gitignore` of `*` and each attempt's `RESTORE-MAP.md` | Managed Claude compaction, unless a preparation root is configured elsewhere. | Leaves it in place. |
| `.rigged-checkpoint.md` | A restore that didn't resume a seat's native conversation (by choice or not) and had a checkpoint to hand it. | Leaves it in place. |
`rig down` leaves a file alone while another seat still uses it: a running seat of the same rig, or any seat of
another rig that isn't archived. That is decided file by file. A guidance file is kept while another seat's
guidance targets the same file. The Claude settings and collector are kept while another Claude seat works in that
directory. So another rig's Codex seat keeps `AGENTS.md` without keeping `CLAUDE.md` or the Claude settings. It
removes `.openrig/` and `.claude/` only when they are empty.
`rig down --delete` does nothing more to project files. OpenRig doesn't edit your project's own `.gitignore`.
## Patterns and Anti-Patterns
### Good Patterns
+4 -1
View File
@@ -23,7 +23,10 @@ Only the activity axis is served today. The session and resumability values are
`activity-taxonomy.ts` but no surface reads them: `rig ps --nodes --full` shows the raw session
STATUS, both node views show a LIFECYCLE of `running`, `detached`, `recoverable` or `attention_required` (`run`, `det`, `rec`,
`att`), and node inventory serves `occupantLifecycle` (`active`, `retiring`, `retired`,
`context_walled`, `compacted`, `crashed`, `unknown`).
`context_walled`, `compacted`, `crashed`, `unknown`). Node inventory's `sessionStatus` is the
recorded session status, with two observed corrections: a running session whose pane is verified
to have exited (identity reason `pane_exited`) reads `exited`, with `startupStatus:
"attention_required"`, and a running session confirmed missing reads `detached`.
The axes never blend. Reachability is strict; revival optimism is a different field
(omnigent's strict-liveness split). `unknown` is a first-class honest value — the oracle
+123
View File
@@ -0,0 +1,123 @@
# Writing a daemon client
What a program that talks to the OpenRig daemon over HTTP needs and can't read from `rig --help`: how to find the
daemon, its health route, which bearer token each route family checks, the event streams, and some route
details. It describes the daemon as it is. It adds no promise beyond what each section says, and route shapes not
listed here can change.
## Finding the daemon
`rig status --json` is the discovery surface. Its `daemon` object gives `state` and `reachable`, and `pid` and
`port` when they are known: a stopped daemon's response has no `port`, and a running daemon found through
`OPENRIG_URL` has no `pid`. `reachable` is `true` when the daemon answered its health probe, `false` when it is
positively down, and `null` when that isn't confirmed (see `rig status` in
[the command reference](../as-built/cli-reference.md#status)). It reads the recorded state and probes; it never
starts a daemon.
The CLI itself finds the daemon in these orders:
- **Data commands** (everything that sends a request): `OPENRIG_URL` (or the legacy `RIGGED_URL`); else
`$OPENRIG_HOME/daemon.json` when it holds a valid `pid` and `port`; else the config keys `daemon.host` and
`daemon.port` (environment `OPENRIG_HOST` and `OPENRIG_PORT`), which default to `127.0.0.1:7433`.
- **Status** (`rig status`, `rig daemon status`): `OPENRIG_URL` (or `RIGGED_URL`) first, probed directly; else
the state file below.
- **Start and stop** (`rig daemon start`, `stop`): the state file, `$OPENRIG_HOME/daemon.json`, then the legacy
`~/.rigged/daemon.json`.
`daemon.json` holds `pid`, `port`, `host`, `db` and `startedAt`. `rig daemon start` writes a new daemon's record
only after its health check passes, so while a start is pending, the file holds no record of the new daemon: it
may be missing, or hold an earlier daemon's stale record. It is the CLI's own state file, not a published
contract: prefer `rig status --json`.
## `GET /healthz`
The liveness and identity read. It always returns `status: "ok"` and `pid`. Every other field is present only
in the case named:
| Field | When it is present |
| --- | --- |
| `euid` | On platforms with effective user ids. A self-report for the CLI's owner check, not authentication. |
| `databaseWarnings` | When the daemon opened its database with warnings. |
| `semver`, `commit`, `dirty`, `builtAt` | On a stamped (released) build; a development run has none. |
| `selfHostId`, `selfHostIdSource` | After the boot reconcile has set this host's id. |
| `eventLoop` | When the event-loop monitor runs: `lagMeanMs`, `lagP99Ms`, `utilization` (0 to 1), `lastTickAgeMs` and `healthy`. |
| `routeTimings` | With `eventLoop`. |
| `slowOperations` | When the slow-operation recorder runs. |
| `bind` | The bind plan: `mode` (`explicit` or `default`), `hosts` (the addresses bound), `tailscaleDetected`, and `ignoredRoutingHost` when a routing value was ignored. |
| `stuckSweep`, `wakeLadder` | The heartbeats of the stuck sweep and the wake ladder, when they run. |
## Bearer tokens
The daemon reads two tokens from its environment at start:
- **`OPENRIG_AUTH_BEARER_TOKEN`** checks Mission Control's write routes (`POST /api/mission-control/action` and
`/notifications/test`), the `/api/hosts` writes (`add`, `update`, `remove`, `pair`), and the `/api/rig-mode`
binding writes. `rig host pair` hands this token to the paired machine, so a daemon that will be paired with
needs it set (see `rig host pair` in [the command reference](../as-built/cli-reference.md)).
- **`OPENRIG_TERMINAL_BEARER_TOKEN`** checks every `/api/transport` and `/api/compaction` route, `POST
/api/terminal/follow` and `/viewer/ensure`, and every `GET /api/terminal/<name>` route, the terminal websocket
included. When the daemon is bound to an explicit address (`rig daemon start --host`) that isn't loopback or a
tailnet address, and this token isn't set, these routes check `OPENRIG_AUTH_BEARER_TOKEN` instead.
A route whose token isn't set lets every request through. Send a token as `Authorization: Bearer <token>`; the
`GET /api/terminal/<name>` routes, the websocket included, also take `?token=<token>`. The CLI sends the terminal token from `OPENRIG_TERMINAL_BEARER_TOKEN`,
or else from the file `$OPENRIG_HOME/terminal-token`, which the person writes; the daemon doesn't create it.
A wrong or missing token gets HTTP 401. The routes that check `OPENRIG_AUTH_BEARER_TOKEN`, and `/api/transport`
and `/api/compaction`, answer `{ "error": "unauthorized", "message", "what_failed", "why_it_matters", "what_to_do" }`.
The `GET /api/terminal/<name>` routes, the websocket included, answer `{ "error": "unauthorized", "hint" }`. A token
doesn't get past the browser boundary's 403 refusals, which [Browser access](browser-access.md) describes.
## Event streams
All three are server-sent events (`text/event-stream`):
- **`GET /api/events`**: the daemon's event log. `?rigId=` limits it to one rig. Each event carries `id: <seq>`.
Without `Last-Event-ID`, `?replay=<n>` first sends the newest `n` stored events (`0`: none, live events only);
`?replay=all`, no `replay`, or any other value sends the whole log first. A `Last-Event-ID` header (what
`EventSource` sends when it reconnects) sends the events after that id instead, whatever `replay` says.
Live events that arrive during the replay follow it, without duplicates.
- **`GET /api/activity/events`**: live only, no replay or ids. Each message is named after its event type:
`seat.activity_changed`, `seat.rung_health`, `proof.judged` or `proof.sources_changed`. It tells a view to read
again; it carries no derived state.
- **`GET /api/queue/sse`** (alias `/api/queue/watch`): live only, with `id: <seq>`, for `queue.created`,
`queue.handed_off`, `queue.claimed`, `queue.unclaimed`, `qitem.fallback_routed`, `qitem.closure_overdue`,
`inbox.absorbed` and `inbox.denied`.
## Route details
- **`GET /api/queue/recent-transitions`** returns at most 20 rows. A positive `limit` above 20 is capped at 20;
a missing, zero, negative or unparseable `limit` means 20.
- **`POST /api/transport/send`** takes `urgent: true`, the same as `rig send --urgent`. The daemon records who
marked it from the sender identity, not from the body.
- **`POST /api/scope/approve`** is what `rig scope slice approve` and `rig scope mission approve` call. The body
takes `scopeTier` (`slice` or `mission`), `scopePath`, and optionally `approvalScope` (`spec` or `delivery`, the
default), `missionsRoot`, `onBehalfOf`, `reApprove`, `reason` and `lockedArtifacts`. The approver is the
request's sender identity (`actorSession` in the body is recorded beside it, and a different value is
superseded). Success is 201 with `scopeTier`, `scopeId`, `scopePath`, `approvalScope`, `approvedBy`,
`approvedAt`, `onBehalfOf`, `actionId`, `freezeFired` and `reApproved`. Refusals include 400 for an invalid
`scopeTier`, `scopePath` or `approvalScope`, or a missing actor (`actor_required`); 503 `action_log_unavailable`;
and, from the approval itself, 404 `scope_not_found`, 409 `already_approved` or `nothing_to_reapprove`, 503
`workspace_not_configured`, 500 `audit_write_failed`, and 400 for its other refusals. An unexpected error is 500
`internal`.
- **`POST /api/proof/judge`** is what `rig proof judge` calls. The body takes `scope`, `item`, `verdict`
(`accept`, `reject` or `withdraw`), `reason`, `expectedRevision` and `expectedPrevious` (a judgment id or
`null`), and optionally `evidence`, `expectedEvidence`, `subject`, `operationId`, `replace`, `project` and
`projectRoot`. The judge is the request's sender identity. The response is `judgment`, the slice's `readiness`,
`replayed` and `notification`: 201 for a new judgment, 200 when an `operationId` replays an earlier one.
- **`POST /api/workflow/instantiate`** requires `specPath`, `rootObjective` and `createdBySession`, and takes
`entryOwnerSession`, `targetRig` and `scope` (see [Project workspace](project-workspace.md) for `scope`).
Success is 201 with `instance`, `spec`, `entryQitemId` and `entryOwnerSession`, any `advisories`, and
`scopeIgnored` when a scope wasn't applied.
- **`GET /api/provider/usage`** returns `{ hostUsage }`, this host's usage rows (`host: "local"`, a `provider`
and a `state`). A Claude row's `windows` are the seats' status-line readings, one entry per reading, each with
its `window`, `usedPercent`, `resetsAt`, `asOf` and `seatSession`. A Codex row has `windows: []`. Treating the
readings as one account rests on the deployment assumption the row's `provenance` names (one account per
host), not on a measured identity. Readings whose reset times disagree are reported as `anomalies`, and the
row's `state` can then be `explicit_unknown`. Show the row's `state`, `anomalies` and each reading's seat and
`asOf` as reported, rather than merging readings that happen to match.
- **Slices are found by folder name alone.** `GET /api/slices/:name`, `/:name/doc/*`, `/:name/proof-asset/*` and
`GET /api/review/slice/:name` take only the name; the detail route reads `?mission` only together with
`?project`. When two missions have a slice folder of the same name, the daemon keeps the first one it finds:
that one is listed and served, and the other doesn't appear in `GET /api/slices` at all. Keep slice names unique
across missions, as [Project workspace](project-workspace.md) says.
+16 -3
View File
@@ -10,7 +10,7 @@ type: `openrig-urgent`, `urgent-only` and `sends-only`. Sibling seats keep their
| `off` (default) | Existing delivery and prompt checks | Existing send result |
| `draft-aware` | Paste into a recognized empty composer; check ownership again before Enter | Hold input and retain this attempt for inspection when bookkeeping is available |
| `openrig-urgent` | OpenRig's own messages (watchdogs, scheduled wakes, notices) and anything marked urgent type, never over a draft | Other agents' messages are held, the sender is told, and held messages stay readable |
| `urgent-only` | Only messages marked urgent type (`rig send --urgent`, or a queue item at priority urgent or critical), never over a draft | Everything else is held, the sender is told, and held messages stay readable |
| `urgent-only` | Only messages marked urgent type (`rig send --urgent`, `urgent: true` on `POST /api/transport/send`, or a queue item at priority urgent or critical), never over a draft | Everything else is held, the sender is told, and held messages stay readable |
| `sends-only` | Deliberate sends from people and agents type, as with `off`; a terminal member's typing isn't protected, because its shell input can't be read | OpenRig's own notices are held. A terminal member defaults to this level |
| `hold` | No automatic terminal input, even at an empty prompt | Retain for inspection outside the pane |
@@ -141,8 +141,21 @@ to finish. The API may return HTTP 202 with `pending: true`; wait for
and effective modes and retains the text `Typing guard: on (hold)` for hold.
A held send immediately returns `outcome: "retained"`, `sent: false`, its exact
reason (such as `draft_input_busy`) and the saved attempt's `outboxIds`. No automatic
retry is scheduled. Each explicit draft-aware request is a new attempt, including
reason and the saved attempt's `outboxIds`. No automatic
retry is scheduled. The reasons are:
- `typing_guard_enabled`: the seat's mode holds this send (`hold`, or a level that
holds this kind of message).
- `runtime_switch_in_progress`: the seat is switching runtime.
- In `draft-aware` mode, a refusal before any input was written:
`draft_input_busy` (the input holds unsubmitted text), `draft_input_unknown` (the
input can't be read), `draft_input_changed` (the input or its target changed during
the delivery), `draft_wake_superseded`, `target_needs_input`,
`wait_for_idle_timeout`, `target_activity_unknown`, `target_runtime_unverified`,
`target_runtime_not_running`, `target_runtime_conflict`, `invalid_wait_for_idle`,
`invalid_dangerously_interact`, `dangerously_interact_requires_reason`,
`prompt_override_audit_unavailable`, `transport_unavailable`, `session_missing` or
`tmux_unavailable`. Each explicit draft-aware request is a new attempt, including
one with a reused `deliveryId`; the result warns older callers that this field no
longer deduplicates input. The CLI does not generate a deduplication key.
+5
View File
@@ -312,6 +312,11 @@ implies ignore-size), and the printed manual attach commands keep
the agent's terminal size. Observers of a Herdr terminal do not themselves resize it;
a controlling browser can. No cross-version resize guarantee is implied.
To type into a seat without changing its size, send the text through OpenRig
(`rig send`, or `POST /api/transport/send`): it is written to the pane with tmux
buffers and keys, and no client attaches. There is no supported way to attach a
writable client that is sure never to resize the agent's window.
## Following a slice or mission's current work