Skip to content

Tier 1 gated actions in the web UI: release the three operator-reviewed halts, loopback-only, behind a typed confirmation #781

Description

@eaitbrahim

The first admission of TTY-gated actions into the browser. Policy decided; this is Tier 1 only.

Supersedes the open question in #436 ("D3 — A GUI human gate"), which was closed as completed with nothing built. keel serve's own docstring already promised this: "the browser gets its own gate in a later change, and it will be a gate rather than a bypass."

Scope: three actions, and only these three

action CLI rail what it releases
resume keel resume kill-switch trading resumes after the kill-switch halted it
resume-entries keel resume-entries 16 new entries resume after the consecutive-loss halt
reset-hwm keel reset-hwm 11 the drawdown breaker is re-seeded against current equity

Explicitly not in Tier 1: autonomy on, record-flow, the three attestations, update.

Two corrections to the action list as briefed

1. Engaging the kill switch is not a gated action and must not become one. keel kill calls engage_kill_switch with no gate — its docstring says "Always allowed (safe action)" — and it is absent from the nine in keel/capabilities.py. Only the disengage (keel resume) is gated. Putting a confirmation ceremony in front of stopping the engine would make the stop slower than the start, which is the wrong direction for the one control an operator reaches for in a hurry. Halt should be a plain button, ungated, and should ship in Tier 1 alongside these three.

2. All three Tier 1 actions increase capability; none reduces risk. They release a brake that a rail applied. The phasing decision stands — these are the right three to do first — but the rationale should be stated as what it is, because the issue text will be read later as justification:

Tier 1 restores normal operation after a halt the operator has reviewed. The capability it grants is bounded (the rails that fired can fire again) and instantly reversible (keel kill). Tier 2 grants standing capability — autonomy converts every later prompt into an automatic yes — or rebases the drawdown baseline itself, which capabilities.py calls out as "the one direction a circuit breaker must not fail in".

reset-hwm is the closest of the three to Tier 2 on that reading, since it touches the same baseline record-flow does. It stays in Tier 1, but it should be the last of the three built and should carry the strongest confirmation phrase.

The security invariant: loopback only

Every state-changing POST /api/gates/* refuses unless the request originated on loopback:

if not is_loopback_request(request):
    return 403, {"error": "GATED_ACTION_LOOPBACK_ONLY",
                 "message": "Gated actions are restricted to local loopback sessions."}

The refusal is on the connection's peer address, never on a header — X-Forwarded-For and Host are attacker-controlled through a tunnel, which is the whole point. A deployment started with --host 0.0.0.0 or any --external-host therefore serves the read surface and refuses every gated action, and says so on the page rather than only in the response.

This holds until #648 (remote-exposure security pass) and #656 (device pairing) land. It is not a placeholder to be relaxed quietly: relaxing it is those issues' job and should be their PR.

The confirmation ceremony

  • A modal requiring the operator to type an exact phrase — RESUME TRADING, CLEAR STREAK HALT, RESET HIGH WATER MARK. Typed, not clicked: it is the browser's equivalent of the terminal ceremony, and the console already types back the asset code for attestations.
  • POST, a valid session token, and X-Keel-Client: 1 — a header a plain HTML form cannot set, which is what _api_client_header_ok already relies on for the setup write surface.
  • The write token minted into the document that carries the action, as /api/setup already does; never into a GET that could be cached, logged or pasted.

What must not change

_is_interactive stays exactly as it is, and the CLI path keeps requiring a real TTY. This is a second gate with the same fail-closed property satisfied by different evidence — never an override, never an env var, never a --yes. Any of those would be settable from cron and would dissolve every rail built on the first gate.

Acceptance

  • A request from curl with no session, from a scheduled job, or from any non-loopback peer is refused for all three actions, with the same message shape as the TTY refusal.
  • A request with a valid session but the wrong typed phrase is refused.
  • keel/capabilities.py gains a second Gate and the three rows move to it, so keel capabilities and the Gates page show which gate covers what. The audit surface is the point: an auditor must be able to see, in one place, that six actions remain TTY-only.
  • A test asserts no new code path reaches any of the nine actions without passing one of the two gates.
  • A test asserts the six Tier 2 actions are not reachable from the web package at all — the existing disjointness test over keel.commands.setup.ACTIONS extended, not replaced.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions