Make the live deployment's config and rule set reproducible - #161
Merged
Conversation
`config.live-sandbox.yaml`, `config.paperforward.yaml`, both run scripts, both launchd plists and the `keel-live`/`keel-paper` wrappers were git-ignored (c564db7 and earlier) on the reasoning that they are operational and never shipped. Both halves of that are still true -- but "never shipped" is enforced by the packaging config, which ships only `keel/`, not by `.gitignore`. Ignoring them bought no shipping guarantee and cost the one that matters: The only copy of the running deployment's configuration lived in `~/keel` on a single laptop. No history, no offsite copy, no way to see what changed or when. The nearest thing to a backup was `~/keel/.backup-20260728/`, already stale -- it predates the 2026-07-29 switch to `logging.verbose: true` on the live side. That matters because a fresh deployment cannot reconstruct this config from the release: the release asset `config.yaml` is `keel/templates/config.live.yaml`, a DIFFERENT file with a different allowlist and different caps than the sandbox actually in use. They hold no credentials -- those stay in `.env`, still ignored (line 2). Verified by grep for key/secret/token/passphrase/PEM material across all eight before staging. The repo is private, so tracking them exposes the caps and allowlist to nobody who cannot already read the code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A deployment's strategy library lives in the DATABASE, not in any config file, and nothing version-controlled describes it. An in-place upgrade preserves it -- the schema steps are additive (`CREATE TABLE IF NOT EXISTS rules`) and `keel migrate` never seeds. A FRESH deployment does not: `keel init` seeds every rule at `candidate` from each rule kind's CONSTRUCTOR DEFAULTS. So the live DCA rule, deliberately sized to `budget_usd: 25`, comes back on a rebuilt box as the built-in `50`, unpromoted -- twice the money, on a machine that otherwise looks correctly provisioned. Nothing errors. That is the failure this closes. `deploy/live-rules.json` is the committed snapshot. `scripts/rule_manifest.py export` regenerates it; `apply` rebuilds a database from it. Two refusals, both deliberate: - It never rewrites an existing rule's params. Matching is by `(kind, product_id)`, the same key `keel rules seed` uses; a rule that exists with different params is reported as DRIFT and the run exits 1. A manifest that could resize a LIVE rule by file edit would route around exactly the review the promotion ladder exists to force. - It refuses to create `live` rules without `--allow-live`, and warns loudly per rule when it does -- seeding straight to `live` is the same gate bypass as `keel rules seed --status live` and `rules promote --force`, so it reads like them. Dry-run is the default; `--apply` writes. Promotion stays `keel rules promote`'s job. Eight tests, including one asserting the CHECKED-IN manifest still rebuilds and still says 25, so a stale manifest fails CI rather than a future deployment. Also documents two things that cost a failed command to find: a DCA rule can never clear the promotion gate (no stop and no target, so `backtest()` opens a position that never closes, leaving `n_trades=0` against `min_trades: 100`), and it needs `--granularity ONE_DAY` because `Dca` never sets `self.granularity` the way `TurtleBreakout` does. Co-Authored-By: Claude Opus 5 (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.
A rebuilt keel deployment would not come back the same. Two independent gaps, both silent.
The rule set is not in version control
A deployment's strategy library lives in the database, not in any config file. An in-place upgrade preserves it — the schema steps are additive (
CREATE TABLE IF NOT EXISTS rules) andkeel migratenever seeds. A fresh deployment does not:keel initseeds every rule atcandidatefrom each rule kind's constructor defaults.So the live DCA rule, deliberately sized to
budget_usd: 25, comes back on a rebuilt box as the built-in50, unpromoted — twice the money, on a machine that otherwise looks correctly provisioned. Nothing errors, and nothing in the repo records that 25 was ever the intent.deploy/live-rules.jsonis now the committed snapshot of the live rule set.scripts/rule_manifest.py exportregenerates it;applyrebuilds a database from it.Two refusals, both deliberate:
(kind, product_id)— the same keykeel rules seeduses — and a rule that exists with different params is reported as DRIFT with the run exiting non-zero. A manifest that could resize a live rule by file edit would route around exactly the review the promotion ladder exists to force.liverules without--allow-live, warning loudly per rule when it does. Seeding straight toliveis the same gate bypass askeel rules seed --status liveandrules promote --force, so it reads like them.Dry-run is the default;
--applywrites. Promotion stayskeel rules promote's job.Eight tests, including one that asserts the checked-in manifest still rebuilds and still says 25 — so a stale manifest fails CI rather than a future deployment.
The operator configs were git-ignored
config.live-sandbox.yaml,config.paperforward.yaml, both run scripts, both launchd plists and thekeel-live/keel-paperwrappers were ignored (c564db7 and earlier) as "operational, never shipped". Both halves are still true — but "never shipped" is enforced by the packaging config, which ships onlykeel/, not by.gitignore. Ignoring them bought no shipping guarantee and cost the one that matters: the only copy of the running deployment's configuration lived in~/keelon a single laptop. No history, no offsite copy. The nearest thing to a backup,~/keel/.backup-20260728/, is already stale — it predates the 2026-07-29 switch tologging.verbose: trueon the live side.A fresh deployment cannot reconstruct this from the release either: the release asset
config.yamliskeel/templates/config.live.yaml, a different file with a different allowlist and different caps than the sandbox actually in use.They hold no credentials — those stay in
.env, still ignored. Verified by grep across all eight before staging. The repo is private, so this exposes the caps and allowlist to nobody who cannot already read the code.Also documented
Two things that each cost a failed command to discover, now in the go-live runbook:
backtest()opens a position that never closes; every trade staysopenand the aggregates seen_trades=0againstmin_trades: 100. A DCA go-live isrules promote <id> --force, twice — structural, not a judgement call.--granularity ONE_DAY, becauseDcanever setsself.granularitythe wayTurtleBreakoutdoes.Verification
_build_rule: all six rules, correct statuses,budget_usd = Decimal('25').keel-live.db:in sync: 6 rule(s).The manifest currently records the DCA rule as
candidate, which is its actual state — the--forcepromotions are still pending.🤖 Generated with Claude Code