--- id: e-stop title: An operator can halt the swarm when nothing else is working status: open crates: [didbot-serve, didbot-pds] dependsOn: [] exitCriterion: > A signed-in operator halts all provisioning and every token this server checks, from the dashboard or from a shell, and can see what is being refused. --- # e-stop Two settings, because they answer different questions. **Pause** refuses every token, new or already issued, and all provisioning, and ends nothing: releasing it lets every token work again. **Revoke** refuses the same and ends every token already issued, which releasing it does not undo. A service-auth token is the exception to both: either setting refuses every new one and every proxied call, but one already minted works at the service it names until its `exp`, at most an hour, or a minute for the ones the proxy mints. The reason this is its own epic is immediacy, not mechanism. Policy lives in the operator's repository and arrives on a poll, so a policy-level stop waits for a network, a server somebody else runs, and an interval. That is correct for a rule and useless for an emergency. **The stop is local state, and it is the only control that is.** One way to throw it, reached two ways: an operator signs in and presses it, from the dashboard or from a shell, both over the same route. A host-level stop is `systemctl stop`, which stops strictly more. - [ ] **Scope it narrower than everything.** The latch has one setting for the whole deployment. What exists beside it: a lock on an account (`didbot_pds::lockout`) stops that account, including creating beneath itself; deleting the operator's record for a root pauses the root and everything beneath it at the next poll after the grace window; and stopping one app is [app-allowlist](app-allowlist.md)'s, a policy edit consulted in process at every pushed authorization request. Whether any of those belongs on this latch, with its immediacy, is open. ## Done - [x] **One way to throw it**, reached two ways. `didbot_pds::Estop` is the latch; `POST /dashboard/api/estop/throw` and `.../release` are the route, behind the operator sign-in in `didbot_serve::operator`, and `didbot estop --pause|--revoke|--release` is the same route from a shell. The throw checks `ServerPolicy::may_halt`, so a server that has not finished arriving still refuses one. - [x] **A latch both gates read**, at issuance and at the write. `didbot_pds::Estop::check_issue` gates `POST /oauth/token` and `bot.did.createAccount`; `Estop::check_use` gates every repository write route and `uploadBlob`, checked fresh on every request rather than cached. - [x] **Say what it is refusing.** `Estop::status` reports tokens, operations and accounts refused since the latch last engaged, reset when it clears, and `Cause` says whether an operator threw it or the server did. - [x] **Log each throw and release.** One `info` line per change to a latch, naming the mode and whose latch it is; the dashboard's routes, which `didbot estop` calls too, add the operator's DID through a span. - [x] **Tell agents distinctly.** A halted request answers `503 Halted` with a message naming whose latch it is. An operator's stop is not a transient failure, and retrying will not help. This server's own pause names the operator's missing record and clears when it is back: `didbot_pds::Halted` carries the `Cause`, and its `ApiError` mapping words the message by it. - [x] **Pause and Revoke as distinct settings**, with Pause refusing every token until it is released and ending none, and Revoke ending them, so releasing a Revoke does not resurrect what it ended (`Mode` and `revoke_hook` in `crates/didbot-pds/src/estop.rs`, and `crates/didbot-serve/tests/write_gates.rs`). - [x] **A restart comes back to the latch the operator left.** A `--data` run writes both latches to `pds.estop` on every throw and release, before a Revoke's teardown starts, and a restored Revoke runs its teardown again — `Estop::kept_in` and `crates/didbot-pds/tests/estop_restart.rs`.