Decisions › D07
Every action dry-runs first, carries a danger class, and only a person runs the real one
Context
Zach's question was whether the panel is read-only or can also command. If it commands, it is more complicated because he would have to deal with permissions, so that it does not interfere with the thing it controls. The converged design split the problem in two planes: a status plane that only reads, and an action plane where the driver registers utilities and the driver runs them.
The decision
Every real action starts as a dry run: the plugin prints the exact command it would run and the preconditions it would check, and runs nothing. Each action has a danger class: query reads only, interrupts stops or resets what runs, changes writes stored state, moves makes hardware move. Everything except a query needs an explicit confirmation for that device.
Only a person runs the real thing. With stdin not a terminal (an agent, a pipe, a cron job) the host refuses everything except a dry run. From the page a real run needs the single-use token the dry run issued (good for 5 minutes, bound to that device, action and node), and a moves action needs a ticked physical precondition such as “the joint is free to spin”.
Added on 24 October: when a moves run ends any way except finished (stopped, timed out, failed, a typed Ctrl-C), the host itself runs the plugin's own stop verb. For a motor that is mode 0. It needs no confirmation, because stopping is the safe direction.
Why
“If commanding its more complicated beacuse now you need to deal with like permissions... so you don't intefer with the thing...”
Zach Yamaoka, notes, 22 Oct 2026
“but then it now needs to like pipe into the actual system and confirm etc may may more complicated.”
Zach Yamaoka, notes, 22 Oct 2026
“I think we can have place holders for buttons”
Zach Yamaoka, notes, 22 Oct 2026
No line of Zach's says “dry run first”; it is the design that answers his permissions worry, and it is recorded in the vocabulary and the API docs. The decision page says so rather than inventing a quote.
Options considered
| Option | Verdict | Reason |
|---|---|---|
| A read-only dashboard | Rejected | Zach wanted buttons: “we can have place holders for buttons”, and the printer model has them. |
| Every action confirmed in a modal (that morning's default in the converged design) | Replaced | Round 2 made every click a dry run first, so the confirmation shows the exact command and the preconditions. |
| Dry run, then a single-use token, a danger class and a physical precondition | Chosen | A dry run is how an agent proves a write button without writing. |
| Agents may press real buttons | Rejected | Written into the API doc: “only a person runs an action; agents can't click”. |
Consequences
- The Utilities panel's one write endpoint is gated three ways (the page's own header, same origin, a JSON body) and by the token, and the doc says an agent must not replay the flow.
- During the v1 build a test mutation removed the passive flag and ran a driver's checks against the real adapter for about 3 seconds. They were query-only and went through the plugin's own gate: no motion, and the boards were unpowered. A guard now sits in front of every live-passive test and refuses any diagnose without the passive flag and any act. See port ownership.
Evidence
- Round 2: the panel on the real rackReport, 2026-10-22. Evidence for this decision.
- IO diagnostics v1: the first end-to-end buildReport, 2026-10-22. Evidence for this decision.
- Runs and Identify: captures from the appReport, 2026-10-24. Evidence for this decision.
- One action vocabulary across drivers (record v0.1)Contract doc, 2026-10-22. The standard action names, their one label each, the four danger classes, the dry run and the model_id identity key.
- Server API v1Contract doc, 2026-10-22. What the page may rely on: state, activity, plugins, actions and their single-use tokens, history, settings.
- Runs v1: a live terminal for every utilityContract doc, 2026-10-24. The seam for runs: a real action on a pseudo-terminal, where records go, the output ring and recordings, Stop and the plugin's own stop.
Commits in the app's repository (private):
95ee3742026-10-22 Every click a dry run first; moves needs a ticked precondition42e71f72026-10-22 Host: a non-TTY may only dry-run693cb802026-10-24 Runs, host side: the plugin's own stop for a moves run that did not finish