You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(control): richer /status contract for control-upgrade — started, noop, throttled, version echo (#320) (#323)
* feat(control): richer /status contract for control-upgrade — started, noop, throttled, version echo (#320)
Consumer findings from pithead#597 against the v1.11.2 contract:
- Write one non-terminal {status:"started"} record right after the D8
claim, so a poller can tell mid-build from queued — and a started
record that outlives the oneshot means the run was lost (poll-forever
otherwise).
- already-on-target reports noop (idempotent, not red on a dashboard);
the throttle window reports throttled (retry-later) — both were an
undifferentiated failed distinguishable only by prose reason.
- applied's reason echoes the landed version, so the consumer no longer
cross-reads :8081/2/summary and hopes no second change raced it.
Additive to the ADR 0002 D3/D6 model (D6 amended in place); documented
in Operations › Writable control path. Unit suite covers the started
mid-run record (incl. the changes/<cid> index), the noop and throttled
statuses, and the version echo.
Closes#320
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(control): sweep the remaining status-vocabulary drift after #320
Docs-consistency review before merge: ADR 0002 D4 still claimed every
guard failure is terminal failed (noop/throttled exist now), D3 predated
the started record, _control_status's comment said 'outcome' only,
control-server.py's docstring omitted /upgrade and the non-terminal
record (and its 202 note read as if already-on-target were a refusal),
and pithead-integration.md's ?change_id status list could be misread as
the endpoint's full vocabulary for upgrade change ids.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/adr/0002-remote-worker-upgrade.md
+5-3Lines changed: 5 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -33,7 +33,7 @@ The receiver (`util/control-server.py`) gains a `POST /upgrade` distinct from `/
33
33
34
34
### D3. Unprivileged receiver stages only; the privileged oneshot fetches and runs
35
35
36
-
As in ADR 0001 D2, the network-facing receiver stays unprivileged (`DynamicUser`): it authenticates, checks the flag, and **stages** the request; it never fetches or runs code itself. The privileged path-triggered oneshot does the fetch-and-upgrade through the existing gated flow. The response is `202 Accepted` with a change id; the consumer polls `/status`for the terminal outcome.
36
+
As in ADR 0001 D2, the network-facing receiver stays unprivileged (`DynamicUser`): it authenticates, checks the flag, and **stages** the request; it never fetches or runs code itself. The privileged path-triggered oneshot does the fetch-and-upgrade through the existing gated flow. The response is `202 Accepted` with a change id; the consumer polls `/status`to a terminal outcome (a non-terminal `started` appears first while the oneshot runs — #320, see D6).
37
37
38
38
### D4. Dashboard-supplied target, bounded by monotonic + reachability guards
39
39
@@ -43,7 +43,7 @@ The staged body is tiny: `{"version": "vX.Y.Z"}`, and it **is** the target. The
43
43
-**Reachable-from-main** — refuse a commit that is not an ancestor of `origin/main` (D10; amended by #318).
44
44
-**Immutable releases + tag protection** (D10) make the tag→commit binding a platform guarantee.
45
45
46
-
The rig still `git`-fetches the tag directly from GitHub — the code has to come from somewhere, and that fetch happens only on an actual upgrade (rare, throttled), not on every trigger — but it makes no separate version-check dial. Anything that fails a guard is terminal `failed`. This trades away rig-*independent* confirmation of "is this THE latest" (the dashboard, over Tor, is the deriver) for privacy, fewer moving parts, and no fleet rate-limit coupling; the anti-rollback + reachability guards are what make trusting a supplied target safe.
46
+
The rig still `git`-fetches the tag directly from GitHub — the code has to come from somewhere, and that fetch happens only on an actual upgrade (rare, throttled), not on every trigger — but it makes no separate version-check dial. Anything that fails a guard ends terminal:`failed`, except the two benign refusals distinguished since #320 — `noop` (already on the target) and `throttled` (inside the D6 window). This trades away rig-*independent* confirmation of "is this THE latest" (the dashboard, over Tor, is the deriver) for privacy, fewer moving parts, and no fleet rate-limit coupling; the anti-rollback + reachability guards are what make trusting a supplied target safe.
47
47
48
48
### D5. Trust model: hash-only, git-tag checkout, no signing (settled)
49
49
@@ -55,6 +55,8 @@ GitHub is therefore accepted as the trust root. The residual risk is explicit: a
55
55
56
56
`control-upgrade` is fail-closed and every step is rollback-guarded, reusing the upgrade flow's existing rebuild-only-if-the-pin-changed logic. A **pre-dial throttle stamp** on the rig bounds how often it will reach out to GitHub, so a looping or hostile consumer cannot turn the fleet into a beacon or a request amplifier. Outcomes use the same `/status` surface as control-apply, extended with a **`failed`** terminal (with reason) alongside `applied` / `rolled_back`, so a pre-apply refusal (non-latest, throttled, fetch/build error) surfaces a clear terminal result to the consumer's poll rather than hanging in a non-terminal state.
57
57
58
+
*Amended (#320, consumer feedback from pithead #597):* the status vocabulary grew three additive members so a poller never has to string-match `reason`. A non-terminal **`started`** is written the moment the oneshot claims the intent (D8 move done), so "mid-run" and "oneshot died mid-run" are distinguishable from "queued"; **`noop`** replaces `failed` for the already-on-target refusal (idempotent, not an error); **`throttled`** replaces `failed` for the D6 throttle refusal (retry-later, not an error). `applied`'s `reason` echoes the landed version. All other refusals stay `failed`.
59
+
58
60
### D7. Auth and source-pinning inherited from ADR 0001 D4/D9
59
61
60
62
`/upgrade` reuses the control path's Bearer `ACCESS_TOKEN` and the `api_allow_from` source pin (which D1 already requires for `control: enabled`), and the same nftables scoping (#142). The stack host is the only trusted writer; the miner never accepts an upgrade trigger from a miner-advertised or arbitrary host. The cleartext-token-on-LAN posture (ADR 0001 D9) applies unchanged — the mining LAN is the trust boundary.
@@ -100,7 +102,7 @@ Low-cost integrity controls that need no standing signing key, shoring up the Gi
100
102
- New opt-in surface, off by default, fail-closed. A rig that does not enable `control_upgrade` carries zero new capability; a rig on `control` alone is unchanged.
101
103
- Residual risk accepted: GitHub-account/release compromise → fleet-wide root RCE (D5). Mitigation is account hardening, not signing.
102
104
- Producer for pithead #597. Backwards compatible (a new MINOR capability). It must ship in a RigForge release before pithead #597 can run end-to-end; pithead #596 (version badge) has no RigForge dependency and ships first.
103
-
- Consumer contract: `POST :8082/upgrade {"version":"vX.Y.Z"}` → `202 Accepted` + change id → poll `:8082/status` for `applied` / `rolled_back` / `failed`. The host never trusts a host/port/token from the intent; the rig bounds the dashboard-supplied target with monotonic anti-rollback + reachable-from-main guards (D4/D10) rather than making its own version-check dial.
105
+
- Consumer contract: `POST :8082/upgrade {"version":"vX.Y.Z"}` → `202 Accepted` + change id → poll `:8082/status` for a non-terminal `started`, then terminal `applied`(reason names the landed version) / `rolled_back` / `noop` (already on target) / `throttled` (retry after the window) / `failed` (#320 amendment, D6). The host never trusts a host/port/token from the intent; the rig bounds the dashboard-supplied target with monotonic anti-rollback + reachable-from-main guards (D4/D10) rather than making its own version-check dial.
104
106
105
107
## Open questions (resolved 2026-07-17 — see Resolution above)
"note": "queued; the rig refuses anything that isn't a real, reachable release newer than the one installed (it does not independently verify the target is the newest). poll GET /status"})
232
+
"note": "queued; the rig refuses anything that isn't a real, reachable release newer than the one installed (posting the installed version ends 'noop'; it does not independently verify the target is the newest). poll GET /status"})
0 commit comments