feat(security): delete the vault and the passphrase gate; autonomy becomes a profile choice - #119
Merged
Merged
Conversation
Delete the vault (already dead code) and the passphrase gate; autonomy becomes a live-read profile choice. Rails unchanged -- this changes who is asked, never what is allowed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_profile() FAILS CLOSED -- an absent row reports autonomous=False, because the safe reading of 'no record' is that the user never opted into unattended trading. Single-row by CHECK(id=1); user_id is the seam for hosted multi-user. Retires the arm_bypass/is_bypass_armed/disarm_bypass token API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… a profile choice The vault was already dead code: cb_client/cli load credentials from .env via config.load_secrets, and it was reachable only through migrate_from_env. Deleting it removes a competing credential path rather than changing how creds are loaded. Drops the now-unused cryptography dependency. The passphrase gate is replaced by one rule -- dangerous actions need a human at a terminal, nothing needs a stored secret. Once placing a money-spending order needs only a typed confirmation, a remembered secret to reset a high-water mark is ceremony without a matching threat model. The four halt-releasing commands (resume, resume-entries, record-flow, reset-hwm) now demand a typed 'yes' and FAIL CLOSED off a TTY. There is deliberately no env/flag override for that TTY check: any such seam would be settable from cron and would defeat the fail-closed. Autonomy moves from a config mode + armed token to a live-read profile choice. _effective_mode returns 'autonomous' only when the config is live AND the profile says so, is re-read every cycle (so 'autonomy off' binds on the NEXT order), lives inside run_once so an in-process caller cannot obtain what the CLI would refuse, and fails closed on an absent row. Rails are untouched throughout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mode is now paper|confirm and an unknown value RAISES naming the key, where it previously degraded silently. A config saying 'mode: bypass' was explicitly asking for autonomy, and autonomy is now a profile choice -- quietly reinterpreting that request in either direction is worse than stopping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The runbook documented the arm-bypass/passphrase dance that no longer exists. Rewritten against the real flow, and honest that its purpose is proving the plumbing (place_order has never run against the real API), not profit. Supersedes PR #116. Marks spec §14's vault + passphrase gate as BUILT THEN REMOVED, with the reasoning, so neither is re-proposed from the old text. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the last descriptions of the vault, the passphrase gate and the arm-bypass token -- no live references to any of them remain anywhere in keel/, packages/, tests/ or scripts/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… fail-closed Addresses an independent review of this PR. Two blocking findings: 1. `keel withdrawals attest --enabled` RELEASES rail 17's entry halt and was ungated and scriptable. Its own docstring justified that with 'the confirm gate and the bypass-arm token still sit in front of it' -- this PR deletes the token, and with autonomy on the confirm gate is gone too, so cron could have re-permitted live entries with no human. Now gated like the other four; --suspended stays ungated (it only reduces capability). 2. Autonomy replaced a TTL-bounded authorization with an unbounded one -- a forgotten 'autonomy on' would grant unattended trading forever, where the old arm token expired in an hour. Adds optional 'keel autonomy on --for-hours N' (Profile.is_autonomous honours it); the default stays durable per the requirement, but 'on' now says plainly when no expiry is set, and 'show' reports remaining time or that it lapsed. profile is new on this branch, so the column lands in v7 rather than needing a v8. Also: get_profile catches sqlite3.Error so it actually implements the fail-closed contract it documents; _interactive_confirm uses the single _is_interactive predicate; disclaimer on all autonomy commands; record-flow shows the amount being confirmed; stale docstrings corrected; and config.yaml no longer implies auto_trade.enabled is a kill-switch (it is read by nothing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ow-ups Second review round. The expiry column was added to v7's CREATE TABLE IF NOT EXISTS, which does nothing for a database already stamped 7 -- it kept the old three-column table forever, so a recorded choice read back as OFF and 'keel autonomy off' (the DE-RISKING command) died on a missing column. Adds an idempotent v8 ALTER TABLE step. Reachable only on a developer database from this unmerged branch, but the command that reduces risk must never be the one that crashes. Also from the review: - end-to-end test that an EXPIRED profile yields mode=confirm out of run_once; the enforcement chain was previously only tested in pieces, so dropping the now_ts argument would have silently un-bounded autonomy again - get_profile logs when the profile is unreadable instead of swallowing it indistinguishably from 'the user never opted in' - --for-hours rejects 0/negative/inf/nan/overflow (0 previously wrote an already-lapsed row while printing 'autonomy ON until ...') - the halt-releasing inventory is FIVE commands, not four -- corrected in the CLI docstring and the runbook, which are what an operator actually reads - runbook: autonomy off binds on the next CYCLE, not the next order (up to 15 minutes at interval_sec 900) -- use 'keel kill' to stop immediately; and --for-hours is now documented where a supervised session is described Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…order' Third review round, all non-blocking: - --for-hours 1e-9 rounded to a zero-length window: it stored an already-lapsed row while printing 'autonomy ON until ...'. Same misleading message the last round closed for 0, just past the boundary. Minimum is now one second. - Four code docstrings still promised 'autonomy off takes effect on the next ORDER'; it is the next CYCLE, and agent.py contradicted itself in a single sentence. This is the one operational fact that matters when someone is trying to stop live trading, so they now also point at 'keel kill'. - Repository.profile_readable() lets 'autonomy show' tell a human the stored setting is UNKNOWN rather than printing a reassuring 'off'. Note that _open_repo's migrate() heals a merely missing table, so this covers damage migrate cannot -- the test lives at repository level for that reason. - logging.getLogger(__name__) per codebase convention; stale section banner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Removes both local security mechanisms from spec §14 and replaces them with one rule: dangerous actions need a human at a terminal; nothing needs a stored secret.
The rails are untouched. All 17 still run first, un-overridable, in every mode. This changes who is asked, never what is allowed. Every claim below has a test.
The vault is gone — and it was already dead code
keel/security/secrets.py(AES-GCMsecrets.enc) is deleted, along with the now-unusedcryptographydependency.Worth stating plainly, because it changes how you should read the diff: the vault was never on the live path.
data/cb_client.pyandcli.pyhave always loaded credentials viaconfig.load_secrets()from a git-ignored.env; the vault was reachable only throughmigrate_from_env. Deleting it removes a competing credential path rather than changing how credentials are actually loaded. (PR #115, which would have wired it up, was closed for this reason.)The passphrase gate is gone
keel/security/authz.pyis deleted. Of its four declared dangerous actions,raise_capsandunlock_vaultwere never used at all — caps are config-file-only, and the vault had no CLI surface.arm_bypassdisappears with bypass mode.That left four commands, all one idea — re-permitting trading after a safety halt:
resume,resume-entries,record-flow,reset-hwm. Each now demands a typedyes(not a barey) and fails closed off a TTY.The reasoning is coherence. Since #117, 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 — and the old gate's own docstring conceded it "does not stop an attacker who already holds the OS account". One rule is easier to audit than two.
Autonomy is a profile choice, not a config mode
New
profiletable (schema 6 → 7) holds the user'sautonomousflag.auto_trade.modecollapses topaper | confirm, so there are two independent switches rather than one enum conflating both:modeautonomouspaperconfirmfalse(default)confirmtrueFour properties, each tested:
_effective_modere-reads the profile every cycle, sokeel autonomy offbinds on the next order, not the next restart. (Mirrors rail 14's allowance, moved to the DB for the same reason.)run_once, not only at the CLI, so a caller drivingrun_oncedirectly cannot obtain autonomy the CLI would refuse.autonomy onrequires a terminal.offdeliberately does not: reducing risk must never be obstructed.Autonomy does not clear safety halts
The one place I deliberately did not honour "autonomous means no confirmation". The four halt-releasing commands still demand a human with autonomy fully on — there is a test named for it. "Trade without asking me" and "un-stick your own drawdown breaker" are different powers; a breaker that can reset itself is not a breaker.
A config carrying
mode: bypassorbypass_arm_ttl_secnow fails to load, naming the key and telling you what to do (mode: confirmpluskeel autonomy on). Previously an unknown mode degraded silently to confirm. A config sayingbypasswas explicitly asking for autonomy, and quietly reinterpreting that request — in either direction — is worse than stopping. Runkeel migrateto add theprofiletable; delete anysecrets.enc/authz.json(both are simply ignored now).Also
The go-live runbook is rewritten (supersedes #116, which documents the arm-bypass/passphrase dance that no longer exists) and is honest that its purpose is proving the plumbing —
place_orderhas still never run against the real API — not profit. Spec §14 is marked BUILT THEN REMOVED with the reasoning, so neither mechanism gets re-proposed from the old text.Verification
1277 tests pass, ruff clean. Grep-proven: zero live references to
authz,save_vault,arm_bypass,is_bypass_armed,bypass_arm_ttl_secorsecrets.encanywhere inkeel/,packages/,tests/orscripts/.Smoke-run against the real CLI, not just unit tests: a v6 database migrating 6 → 7;
autonomy onpiped (refused — "needs an interactive terminal");autonomy offpiped (allowed);resumepiped (refused); and amode: bypassconfig failing to load with the key named.Design:
docs/superpowers/specs/2026-07-21-security-simplification-design.md.