Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 89 additions & 5 deletions docs/operator-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,86 @@ If the deployment runs on a schedule (LaunchAgents, cron), a new build takes eff
cycle with nothing to restart — each cycle is a fresh process. A **long-running** process is the
exception: a `keel tui` left open keeps the build it started with until you quit and relaunch it.

### Self-update: `keel update` and the console's update view

The four commands above — plus the per-database `keel migrate --db` step the updater also
runs (the four commands don't include it; it runs for every `keel*.db` with the new build,
between install and verify) — are what `keel update` runs for you (issue #415) — same order,
same tools, one service (`keel/commands/update.py`) behind two front-ends: the `keel update`
CLI command and the Account menu's `update` entry in the TUI console (see "The TUI console"
for the ceremony). `keel update --check` mutates nothing: it prints current vs latest and the
whole plan.

**What it does, in the manual procedure's own order.** It reads the latest release from the
public GitHub API (no auth, no tokens — an unauthenticated read is rate-limited to 60/hour per
IP, which a human-gated check never approaches; a rate-limit or network failure is an honest
error, not a guessed "up to date"). It downloads exactly the **four production wheels** —
`keel_core`, `keel_broker_api`, `keel_broker_coinbase`, `keel_trader`, by exact name, never
`Release/*.whl`, so the fake and Robinhood wheels can never ride along — into `Release/` in the
launch folder, verifying each file landed non-empty (and bounding the read at 200 MiB, far above
the ~1 MiB wheels — a mis-pointed URL is refused, not streamed to disk; a failed download or
install removes the partial files so a torn wheel cannot poison a later rollback).
**Backups first**: every `keel*.db` in the
launch folder is copied to `<db>.bak-before-<version>-<timestamp>` before anything is installed
— through SQLite's own backup API, a consistent snapshot even with a writer mid-transaction,
where a plain file copy can be torn — and the backups are **never deleted** — not on success,
not on failure. It installs the four
wheels by path into the RUNNING
venv with `uv pip install --python <venv> --find-links Release <the four paths>` — the manual
command exactly, `--find-links Release` and all — uv is a **deployment dependency**
of self-update for exactly the reason the manual procedure uses it; an absent uv is an honest
error naming this section. It runs `keel migrate --db` for each database **with the new build**,
then **verifies** with the new build's `keel versions` — every keel distribution must report the
new version, the check that can actually fail. Only a verified success removes the **superseded
wheels** (the old version's four) from `Release/`; the new four stay for the next update.

**Never automatic from what keel ships — always typed.** The full run demands a typed `yes` at
a terminal (the CLI's own confirmation gate, called inside the service before any mutation;
both shipped front-ends — the CLI and the console's update view — hand it exactly that gate),
and that gate fails closed off a TTY, so a scheduled job, which has no terminal, cannot confirm.
The service underneath is a Python API, and an operator's own code could call it with its own
gate — just as the CLI itself can be driven with scripted input on a real TTY; the guarantee is
about what keel ships, not about what is physically expressible. The wording
names the version pair, the launch folder, and that the running binary is replaced.

**A failure is loud and phase-true, never papered over.** pip replaces the packages at install
time, so there is no cheap rollback. When the failure IS the install (uv absent, a timeout, a
corrupt wheel), the updater says the venv was **not updated — or is half-updated** (`keel
versions` shows exactly what is installed), removes the downloaded files, and attempts no
reinstall. When the failure is AFTER a finished install (migrate, verify), the updater says the
new wheels **are** installed, re-installs the **previous** wheels best-effort when they are
still in `Release/` (they are — cleanup only happens on success), and names this section as the
manual recovery — pointing at the `.bak-before-*` backups as the data recovery (the old build
opening the migrated databases is the migrations-are-additive assumption, not a guarantee). The
backups are untouched either way.

**It refuses everything that is not the deployment.** The plan refuses when the running build
is not a release install, when no keel distributions are installed (an `uv run keel` checkout),
and when the running `keel` package does not resolve from the launch folder's own `.venv`
site-packages — a source `keel/` directory under the launch folder (deploying wheels would
shadow the tree, not update it), a package resolving from outside the launch folder (a repo
run: the wheels would land in a venv that is not this deployment's), or an install whose origin
is not the wheels. From a checkout, this section's four commands by hand
remain the procedure.

**The relaunch split.** On a verified success the **TUI relaunches itself** — it replaces its
own process with the new build's `keel` entry (`os.execv`, the terminal restored first, the
original invocation's arguments carried over VERBATIM after argv[0] — wrappers that exec
`keel --config X --db Y tui` relaunch with exactly those flags in that order; only a wrapper
invoked with no arguments at all relaunches as plain `keel tui`), because a console left
running would keep the replaced
binary it started with. A relaunch that cannot exec at all renders the manual `keel tui` start
and holds it for an Enter retry that re-installs nothing. The **CLI prints the command instead**
and does NOT relaunch anything:
`keel update` ends by telling you to run `keel tui` (or your deployment wrapper). A wrapper
invoked directly (`./keel-live tui`) relaunches through the venv's `keel` entry with the same
flags.

**The manual fallback is unchanged.** The four commands at the top of this section still work
and remain the documented procedure when uv is absent, the API is rate-limiting, or you simply
prefer the hand run: `gh release download`, `uv pip install --python .venv` the four wheel
paths, `keel versions`, `keel status`.

## Paper vs. live

A deployment such as `~/keel` runs **two of them side by side**, and they share nothing: separate
Expand Down Expand Up @@ -565,10 +645,11 @@ next open/close, `24/7` for always-open venues, and **CLOCK UNAVAILABLE** render
the recorded clock is absent or stale, exactly as `fetch --check` treats it. The banner reads the
recorded session state; there is no TUI-side calendar.

**The typed contracts: six of the CLI's own, two the console adds.** Six actions run the
**The typed contracts: seven of the CLI's own, two the console adds.** Seven actions run the
CLI's own typed prompt in-console, word for word (curses suspends around it so the prompt
renders at the terminal): `resume`, `resume-entries`, `record-flow`, `reset-hwm`,
`withdrawals attest --enabled`, and `autonomy on` — each the same
`withdrawals attest --enabled`, `autonomy on`, and `update` (the self-update run — the same
gate `keel update` demands; see "Self-update" under "Deploying a new version") — each the same
`_require_interactive_confirmation` gate the CLI command runs, demanding a typed `yes` and
failing closed off a TTY. Two more typed prompts are **ceremony the console adds on top of
an ungated CLI action** — deliberately *stricter* than the CLI, not identical to it: asset
Expand Down Expand Up @@ -605,9 +686,12 @@ from the rule classes themselves.

**Safety design notes.** Re-entering any sub-menu resets its cursor to the top — a remembered
row is a loaded one (leave Trading with the cursor on kill and a replayed Enter would engage the
halt with no ceremony). The Account menu (pnl — the FIFO report over imported transactions, with
an honest empty state until `keel db import` has loaded any — and versions, the deploy check) is
read-only; there is no write path in it at all. The console runs no loop of its own and schedules
halt with no ceremony). The Account menu's pnl (the FIFO report over imported transactions, with
an honest empty state until `keel db import` has loaded any) and versions (the deploy check)
entries are read-only views; the branch's **one** write path is the update entry (issue #415) —
an ARMED view whose run demands the CLI's own typed gate, and which on a verified success
relaunches the console on the new build ("Self-update" under "Deploying a new version" is its
procedure). The console runs no loop of its own and schedules
nothing: it is a front-end over the same services, and closing it never stops a deployment's own
scheduled cycles.

Expand Down
11 changes: 11 additions & 0 deletions keel/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@
from keel.commands.trading import record_flow as record_declared_flow
from keel.commands.trials import trials_group
from keel.commands.tui import tui_cmd
from keel.commands.update import update_cmd
from keel.commands.versions import versions_cmd
from keel.commands.withdrawals import withdrawals_group
from keel.compliance import purification as purification_mod
Expand Down Expand Up @@ -1317,6 +1318,16 @@ def simulate(
cli.add_command(versions_cmd)


# -- update (self-update: pull the latest release into the launch folder) -------------------------

# The self-update service (issue #415): ONE service (`keel.commands.update`) behind
# TWO front-ends -- this `keel update` / `keel update --check` surface and the
# console's Account-menu update view. Never automatic (a typed gate at a terminal),
# backups-first, verify-before-cleanup; the runbook's manual procedure remains the
# documented fallback.
cli.add_command(update_cmd)


# -- brokers (venues/brokers visibility: capability display over the adapter registry) ------------

# The O7 brokers service (issue #394 C7) -- one payload over `discover_brokers()` and the
Expand Down
Loading