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
6 changes: 2 additions & 4 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,10 @@ market_data:

auto_trade:
mode: paper
# NOTE: currently UNUSED by any code path -- it is NOT a kill-switch and setting it
# true or false changes nothing. Use `keel kill` to halt trading.
enabled: false
interval_sec: 900
# how long a `keel arm-bypass` token stays valid (seconds) before bypass mode requires
# re-arming (Issue #60) -- `keel agent --bypass` still needs a fresh armed token even with
# the CLI passphrase gate satisfied.
bypass_arm_ttl_sec: 3600

promotion:
min_trades: 100
Expand Down
174 changes: 174 additions & 0 deletions docs/go-live-runbook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Go-live runbook — the first supervised live order

The purpose of this run is **not** profit. It is to prove the live execution path works, because
`place_order` has never been executed against the real Coinbase API. Everything up to now —
683 tests, a positive backtest, a passing verdict — says the *logic* is right. None of it says the
*plumbing* is. Treat the first order as an experiment whose result is "the order appeared on the
exchange, correctly, and we have a record of it".

**Budget the whole thing as money you are willing to lose outright.**

---

## 0. Before you start

| | check | why |
|---|---|---|
| ☐ | `keel --version` reports `[release]`, no `DIRTY`, no `[checkout]` | a build that matches no commit is not reproducible; do not run it against funds |
| ☐ | you have a **trade-enabled** CDP key (the read-only one cannot place orders) | |
| ☐ | `.env` holds `CDP_API_KEY` / `CDP_API_SECRET`, and `.env` is git-ignored | credentials live only here — there is no vault |
| ☐ | you are at a real terminal | confirmation and every halt-releasing command fail closed off a TTY |
| ☐ | funds are in **USDC**, not USD | rail 13 vetoes a BUY that is not funded from `quote_currency`, and never draws from a bank/ACH |

## 1. Bring the deployment up

```bash
keel migrate # existing database: apply outstanding schema migrations
# or, on a brand-new deployment:
keel init # writes config.yaml and seeds the rule library as `candidate`
keel init-config --live # optional: the production config (mode: confirm)
```

Then check the config you are actually about to run:

```bash
keel config show 2>/dev/null || grep -A3 '^auto_trade:' config.yaml
```

- `auto_trade.mode: confirm` — live, and asks before each order.
- `caps.max_exposure_usd` — set this to something you can lose. For a first run, small.
- `allowlist` — only assets you have screened and attested.

## 2. Attest what the rails need

Several rails **fail closed** until a human has attested something. This is deliberate: an
unattested venue or asset is treated as unknown, not as fine.

```bash
keel assets list # what is attested, and what is not
keel subscription show # rail 14's spend allowance comes from this record
keel withdrawals show # rail 17: withdrawal capability is a compliance precondition
```

Attest anything missing (`keel assets attest`, `keel subscription attest`,
`keel withdrawals attest`) before continuing. If a rail vetoes later, its message names the
command that fixes it.

## 3. Promote exactly one rule

Seeded rules are `candidate` and trade nothing. Promote **one**, on **one** product:

```bash
keel rules list
keel rules promote <id> # candidate -> paper -> live
```

Promoting to `live` is what the promotion floor exists to gate. If you are deliberately
short-circuiting it for this test, know that you are — and promote a single rule, not the library.

## 4. Run one cycle, in confirm mode, and watch

```bash
keel agent
```

Autonomy is **off** by default, so this is what you should see:

```
Rails PASSED. Coinbase order preview:
order_total: 5.00
...
Place this order? [y/N]:
```

**Read the preview before answering.** It is the broker's own numbers, not keel's estimate. Check
the product, the side, and the total. If anything surprises you, answer `N` — declining places
nothing and costs nothing.

The rails have already passed at this point. The prompt is an **additional** human gate, never a
replacement for them.

## 5. Verify against reality

Do not trust the tool's own success message alone. Check the exchange:

```bash
keel pnl
keel rules list
```

- The order appears in the Coinbase UI/app with matching product, side and size.
- `orders` has a row with the real broker order id.
- The fill price is sane against the market at that moment.

If the order did **not** appear but keel thinks it placed one, stop and investigate before
running anything else. That is the exact failure this run exists to catch.

## 6. Halting

```bash
keel kill # engage the kill-switch: halts all trading immediately
keel resume # release it -- asks for a typed "yes" at a terminal
```

The kill-switch is checked first on every cycle and **defaults to engaged**, so a damaged or
unreadable state halts trading rather than permitting it.

**Five** commands re-permit trading after a halt. Each demands a typed `yes` from a terminal and
**cannot be run from a script or cron job** — a breaker that can reset itself is not a breaker.

| command | releases |
|---|---|
| `keel resume` | the kill-switch |
| `keel resume-entries` | a consecutive-loss halt (rail 16) |
| `keel record-flow --amount ±N` | rebases rail 11's high-water mark |
| `keel reset-hwm` | rail 11's high-water mark, clearing a stuck drawdown halt |
| `keel withdrawals attest --enabled` | rail 17's entry halt |

The de-risking direction of each is always allowed and needs no terminal: `keel kill`,
`keel withdrawals attest --suspended`, `keel autonomy off`.

## 7. Only afterwards: autonomy

Do **not** turn this on for the first run.

```bash
keel autonomy show
keel autonomy on # asks for a typed "yes"; requires a terminal
keel autonomy off # always allowed, works anywhere, needs no terminal
```

Autonomy stops keel asking before each order. It changes **who is asked, never what is allowed** —
every hard rail still runs first, and it does **not** let the agent clear a safety halt.

The setting lives in your profile in the database and is re-read **at the start of every cycle**.
So `keel autonomy off` takes effect on the **next cycle**, not the next restart — but note that
with `interval_sec: 900` a cycle already in flight can still place orders for up to 15 minutes.
**If you need trading to stop immediately, use `keel kill`, not `autonomy off`.**

**Prefer a time-bounded session:**

```bash
keel autonomy on --for-hours 4 # lapses on its own; nothing to remember
```

Without `--for-hours` autonomy has **no expiry** and stays on until you turn it off — a forgotten
`autonomy on` will still be trading unattended weeks later. `keel autonomy show` tells you which
you have and how long is left.

Turn it on only once you have watched several supervised cycles behave correctly.

---

## What can still go wrong

- **`place_order` has never run against the real API.** This runbook is that test. Expect the
unexpected on the first attempt, and keep the size trivial.
- **A rail vetoes and you disagree.** Read the veto message; it names the rail and the command
that clears it. Do not work around a rail — the rails are un-overridable by design, including
in autonomous mode.
- **Confirm mode places nothing when run headless.** That is not a bug: with no TTY the
confirmation declines. Use a terminal, or turn autonomy on deliberately.
- **Equity moved because you deposited or withdrew.** Tell the tool (`keel record-flow --amount
±N`), or rail 11 will read the movement as drawdown and veto entries on an account that lost
nothing.
78 changes: 78 additions & 0 deletions docs/superpowers/plans/2026-07-21-security-simplification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Security simplification — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. Steps use `- [ ]` checkboxes.

**Goal:** Delete the vault and the passphrase gate; make autonomy a live-read profile choice.

**Architecture:** A new single-row `profile` table (schema 6→7) holds `autonomous`, read live on every order decision. `auto_trade.mode` collapses to `paper|confirm`. The four halt-releasing commands swap the passphrase for an interactive typed confirmation. Rails are untouched throughout.

**Tech Stack:** Python 3.12, click, sqlite3, pytest.

## Global Constraints

- **The 17 rails do not change.** This changes *who is asked*, never *what is allowed*.
- Every safety invariant in the spec's "Core safety invariants" gets a test.
- Autonomy fails closed (absent row ⇒ `False`); `autonomy on` needs a TTY; `autonomy off` never does.
- Autonomy **never** clears a safety halt.
- Full suite + `uv run ruff check keel tests packages scripts` green before each commit.

---

### Task 1: `profile` table + Repository API (schema 6→7)

**Files:** `keel/data/db.py`, `keel/data/repository.py`, `keel/types.py`, `tests/data/test_repository.py`

**Produces:** `Profile(autonomous: bool, updated_ts: int)`; `Repository.get_profile() -> Profile`; `Repository.set_autonomous(value: bool, now_ts: int) -> None`; `db.SCHEMA_VERSION == 7`.

- [ ] Failing tests: absent row ⇒ `autonomous False`; set/get round-trips; `set_autonomous` upserts (never a 2nd row); a v6 DB migrates to v7 and gains the table; `SCHEMA_VERSION == 7` literal tripwire.
- [ ] Run → FAIL. Implement table + `_migrate_v7_profile` + repo methods + `Profile`. Run → PASS. ruff. Commit.

### Task 2: Delete the vault and the passphrase gate

**Files:** delete `keel/security/`, `tests/security/`; `pyproject.toml`; `keel/cli.py`

**Produces:** `_require_interactive_confirmation(action: str, detail: str) -> None` in `cli.py` (typed `yes`, fails closed off-TTY).

- [ ] Failing tests: each of `resume`/`resume-entries`/`record-flow`/`reset-hwm` proceeds on typed `yes`, aborts on anything else, aborts with no TTY.
- [ ] Run → FAIL. Delete both modules + their tests + the `cryptography` dep; remove `_require_authz`, `--passphrase`, `--authz-path`, `DEFAULT_AUTHZ_PATH`; add `_require_interactive_confirmation` and wire the four commands. Run → PASS. ruff. Commit.

### Task 3: Autonomy in the agent and executor

**Files:** `keel/agent.py`, `keel/execution/executor.py`, `tests/test_agent.py`, `tests/execution/test_executor.py`

**Consumes:** `Repository.get_profile()`.
**Produces:** `agent._effective_mode(config, repo) -> Literal["confirm","autonomous"]`; executor `mode: Literal["confirm","autonomous"]`.

- [ ] Failing tests: `autonomous=True` places with no `confirm_fn`; `autonomous=False` still fails closed without one; **a rail veto never reaches placement in autonomous mode**; profile re-read per cycle (flip between two `run_once` calls); `mode: paper` places nothing even when autonomous; kill-switch short-circuits first.
- [ ] Run → FAIL. Replace `_confirm_or_bypass` with `_effective_mode`; retire `arm_bypass`/`is_bypass_armed`/`disarm_bypass` and `bypass_refused_reason`; rename the executor literal. Run → PASS. ruff. Commit.

### Task 4: `keel autonomy on|off|show`

**Files:** `keel/cli.py`, `tests/test_cli.py`

- [ ] Failing tests: `show` prints off by default; `on` with typed `yes` enables and persists; `on` aborts off-TTY and on a non-`yes` answer; `off` disables **without** a TTY; **`on` does not let the four halt commands skip their confirmation**.
- [ ] Run → FAIL. Add the `autonomy` group; remove `arm-bypass`/`disarm-bypass`/`--bypass`. Run → PASS. ruff. Commit.

### Task 5: Config — validate `mode`, drop `bypass_arm_ttl_sec`

**Files:** `packages/keel-core/keel_core/config.py`, `config.yaml`, `keel/templates/config.yaml`, `keel/templates/config.live.yaml`, `tests/fixtures/config_golden_*`, `tests/test_config.py`

- [ ] Failing tests: `mode: bypass` raises `ConfigError` naming the key; `paper`/`confirm` load; `bypass_arm_ttl_sec` is gone from the parsed config.
- [ ] Run → FAIL. Validate mode, drop the field, update all three configs (**root and dev template must stay byte-identical**) and the golden fixtures. Run → PASS. ruff. Commit.

### Task 6: Docs — go-live runbook + spec §14

**Files:** `docs/go-live-runbook.md`, the main design spec's §14

- [ ] Write the runbook against the real flow: `.env` → `keel migrate`/`init` → promote a rule → `keel agent` (confirm) → one tiny supervised order → optional `keel autonomy on`. Amend §14 to record the removal and why. Commit.

### Task 7: Verification

- [ ] `uv run pytest -q` green; `uv run ruff check keel tests packages scripts` clean.
- [ ] Grep-prove the surface is gone: no `authz`, `save_vault`, `arm_bypass`, `bypass` outside history/docs.
- [ ] Smoke: `keel migrate` on a v6 DB; `keel autonomy show/on/off`; `keel autonomy on` piped (must refuse).

## Self-Review

- **Spec coverage:** §3.1→T2, §3.2→T2, §3.3→T1+T3+T5, §3.4→T4, §3.5→T6. Invariants 1,3,6→T3; 2→T4; 4→T4; 5→T3.
- **Placeholders:** none. **Type consistency:** `Profile`, `get_profile`, `set_autonomous`, `_effective_mode`, `_require_interactive_confirmation` used identically across tasks.
31 changes: 21 additions & 10 deletions docs/superpowers/specs/2026-07-15-keel-autotrade-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,16 +272,27 @@ Enforced in `guards.py` **before every order**, **un-overridable in any mode (in
losable slice (§22.3).
- **No classic user authentication.** For a local single-user CLI/daemon, logins/accounts/sessions are YAGNI
(OS access already opens that door). We invest instead in the two controls below.
- **Portable encrypted secrets vault** (`security/secrets.py`): the CDP key/secret (and any remote-control token)
live in an **AES-GCM-encrypted `secrets.enc`** unlocked by a **master passphrase** (passphrase → scrypt KDF →
key). One file, **copyable between machines** (explicitly *not* the machine-bound OS Keychain, so it's
transportable). `chmod 600`; secrets never logged; `.enc`/`.env` git-ignored. An optional per-machine keychain
*cache* of the derived key may be added for convenience, but the portable `.enc` is the source of truth.
- **Dangerous-action authorization gate** (`security/authz.py`): a passphrase (stored as a scrypt hash,
rate-limited) required **only** to **arm bypass/autonomous mode**, **raise caps above config maxima**, or
**disable the kill-switch / resume**. Read-only commands and confirm-mode trades need none. This is local
*authorization*, not a login — it forces intentionality and blocks casual/accidental/shoulder-surf misuse of
the autonomy capability.
- ⛔ **SUPERSEDED 2026-07-21 — the vault and the passphrase gate were BUILT and then REMOVED.**
See `2026-07-21-security-simplification-design.md`. Both are gone; do not re-propose them without
reading that spec first. What replaced them:
- **Credentials come from a git-ignored `.env`** (`config.load_secrets`). The encrypted
`secrets.enc` vault (`security/secrets.py`) was deleted. It had in fact never been on the live
path: `cb_client`/`cli` always loaded credentials from `.env`, and the vault was reachable only
via `migrate_from_env` — so it was a *competing* credential path, not the real one.
- **The scrypt passphrase gate (`security/authz.py`) was deleted.** Of its four declared
actions, `raise_caps` and `unlock_vault` were never used at all. Once placing a real,
money-spending order needs only a typed confirmation, requiring a remembered secret to reset a
high-water mark is ceremony without a matching threat model. One rule replaced it: **dangerous
actions need a human at a terminal; nothing needs a stored secret.** The four halt-releasing
commands (`resume`, `resume-entries`, `record-flow`, `reset-hwm`) demand a typed `yes` and fail
closed off a TTY, with deliberately no env/flag override (any such seam would be settable from
cron and would defeat the fail-closed).
- **Autonomy is a profile choice, not a config mode.** `auto_trade.mode` is now `paper|confirm`;
the `profile` table (schema v7) holds the user's `autonomous` flag, re-read live on every order
so `keel autonomy off` binds on the *next* order. It fails closed on an absent row, cannot be
enabled without a terminal, and **never clears a safety halt**.
- Real user authentication arrives with the future **server-hosted** deployment; a local
single-user vault + passphrase was ceremony in the meantime.
- **Honest boundary:** on a single-user machine the real security boundary is the **OS account + full-disk
encryption**. These controls are defense-in-depth (reduce plaintext exposure, prevent casual misuse); they do
**not** stop an attacker who already holds the OS account. Stated so they aren't mistaken for more.
Expand Down
Loading
Loading