Skip to content

Make the live deployment's config and rule set reproducible - #161

Merged
eaitbrahim merged 2 commits into
mainfrom
chore/reproducible-deployment-config
Aug 3, 2026
Merged

Make the live deployment's config and rule set reproducible#161
eaitbrahim merged 2 commits into
mainfrom
chore/reproducible-deployment-config

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

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) 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, and nothing in the repo records that 25 was ever the intent.

deploy/live-rules.json is now the committed snapshot of the live rule set. 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 — 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.
  • It refuses to create live rules without --allow-live, warning 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 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 the keel-live/keel-paper wrappers 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 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. The nearest thing to a backup, ~/keel/.backup-20260728/, is already stale — it predates the 2026-07-29 switch to logging.verbose: true on the live side.

A fresh deployment cannot reconstruct this from the release either: 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. 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:

  • A DCA rule can never clear the promotion gate. It has no stop and no target, so backtest() opens a position that never closes; every trade stays open and the aggregates see n_trades=0 against min_trades: 100. A DCA go-live is rules promote <id> --force, twice — structural, not a judgement call.
  • It needs --granularity ONE_DAY, because Dca never sets self.granularity the way TurtleBreakout does.

Verification

  • Rebuilt a fresh DB from the committed manifest and read it back through the agent's own _build_rule: all six rules, correct statuses, budget_usd = Decimal('25').
  • Dry-run against the real keel-live.db: in sync: 6 rule(s).
  • Full suite 1678 passed; mypy clean on 197 files; ruff clean.

The manifest currently records the DCA rule as candidate, which is its actual state — the --force promotions are still pending.

🤖 Generated with Claude Code

eaitbrahim and others added 2 commits August 3, 2026 18:44
`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>
@eaitbrahim eaitbrahim added the tooling Dev/release tooling (Docs, CI & tooling) label Aug 3, 2026
@eaitbrahim
eaitbrahim merged commit 35eb506 into main Aug 3, 2026
1 check passed
@eaitbrahim
eaitbrahim deleted the chore/reproducible-deployment-config branch August 3, 2026 22:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tooling Dev/release tooling (Docs, CI & tooling)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant