0028: A person clears a gate from the browser through a closed list of CLI commands
- Status: Accepted (Navjyot, 2026-10-06)
- Date: 2026-10-06
- Related: 0024 (every write is authenticated), 0026 (the allowlist pattern), 0027 (answering, which this does not repeat), 0001 (Orion owns what happens next)
Contextβ
The gate board (mockup 02) shows every place Orion waits on a person. Answering is already possible from the page (ADR 0027). The other gates are not:
- Merge approval and plan confirmation clear only by a reaction on a
Slack message (
internal/collect/approval.go,internal/decide). There is no command that records either, so a button has nothing to call. - Request changes and prioritise already have commands
(
orion request-plan-changes,orion prioritise).
The web package holds no tracker credential (ADR 0027) and must not gain a way to run arbitrary commands.
Decisionβ
- Four actions, and no others: approve, confirm plan, request changes, prioritise. Answering stays as ADR 0027 decided. Adding an action is a change to this ADR.
- Each action is the CLI command that owns it, run as a child process with an
explicit argument list and no shell. The page sends only the action name and
the ticket key. The server builds the argument list from a closed table and
validates the key (
answers.ValidKey); it never takes any part of a command line from the request. - The server checks the gate before it acts. It re-reads the log at request time and refuses an action the ticket is not waiting for (approve on a ticket not at approval, confirm on a ticket with no pending plan). A stale page cannot approve something that has moved on.
orion approve KEYandorion confirm-plan KEYrecord the same decision the Slack reaction records, under the same rules: a rejection beats every approval, the bot's own reactions never count, and a second approval is a no-op. Whoever may approve is a policy question the Slack path answers with an allowlist of Slack users; the local path has no Slack identity, so local approval is off unlessorion.jsonturns it on (a named switch, default off), and with it off the button is not drawn and the command refuses.- One action at a time per ticket, and a repeat is refused. The server binds the idempotency key (action, ticket, when the gate began); the client cannot supply one.
- The person is told what happened in a sentence. The command's output is scrubbed of secrets, escaped and capped before it reaches the page; the page shows the outcome and, on failure, why, leaving the gate in place.
- Every request carries the token and passes the Origin and Host checks (ADR 0024). The page shows what will run and asks for a confirmation first.
Consequencesβ
- A merge can be approved with no Slack message. That is a new way to approve a merge, which is why it is off by default and why a rejection still beats it.
- The page cannot do more than the four commands do. Anything the CLI refuses, the page refuses, with the CLI's own reason.
- A browser and a Slack reaction can both act on one gate; the first one wins and the other is told it already happened.
Rejectedβ
- Call the collect and decide code in the web process. Faster, but it moves the approval rules into a process that faces a browser and duplicates what the CLI validates.
- A general "run this command" endpoint. One bug away from remote execution on a machine that holds the user's credentials.
- Keep approval Slack-only. Safe, but it leaves the gate board unable to clear the gate it exists to show.