carlos / platform Public

durability design: every byte off the box, every box rebuildable (draft)

From the 2026-10-22/23 live audit of carloku.com: the console database had
no copy anywhere, the Irish box's Litestream config is hand-written and
stale, instance keys and several secrets live only on their box, and
nothing in the account has a copy outside it. Six workstreams, cheapest
risk reduction first; questions for Paul in §9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev ee2cd5e5c523825da2666d22826e88bfc9741121 parent d18e619
1 file changed, +368 −0
  • docs/superpowers/specs/2026-10-23-durability-design.md +368 −0

CI cancelled — superseded by a newer push — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (2 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.034s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (19 s)
=== gofmt (0 s)
gofmt -l .: clean
=== rest (174 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.608s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.883s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.745s
ok  	github.com/carlosframework/platform/internal/activator	60.633s
ok  	github.com/carlosframework/platform/internal/agentsock	0.043s
ok  	github.com/carlosframework/platform/internal/alerting	0.010s
ok  	github.com/carlosframework/platform/internal/analytics	0.043s
ok  	github.com/carlosframework/platform/internal/appconfig	2.935s
ok  	github.com/carlosframework/platform/internal/apperr	0.199s
ok  	github.com/carlosframework/platform/internal/applog	0.284s
ok  	github.com/carlosframework/platform/internal/awsinv	0.014s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.007s
ok  	github.com/carlosframework/platform/internal/boxid	0.831s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.029s
ok  	github.com/carlosframework/platform/internal/caps	0.031s
ok  	github.com/carlosframework/platform/internal/certwarm	0.007s
ok  	github.com/carlosframework/platform/internal/cgroup	0.760s
ok  	github.com/carlosframework/platform/internal/changelog	0.535s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.228s
ok  	github.com/carlosframework/platform/internal/clamd	0.066s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	8.167s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.016s
ok  	github.com/carlosframework/platform/internal/dbquery	0.016s
ok  	github.com/carlosframework/platform/internal/dbreads	2.251s
?   	github.com/carlosframework/platform/internal/dbreads/dbreadstest	[no test files]
ok  	github.com/carlosframework/platform/internal/dbverify	0.008s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.010s
ok  	github.com/carlosframework/platform/internal/docsite	0.030s
ok  	github.com/carlosframework/platform/internal/domains	2.954s
ok  	github.com/carlosframework/platform/internal/doorkey	0.015s
ok  	github.com/carlosframework/platform/internal/economics	0.008s
ok  	github.com/carlosframework/platform/internal/edge	45.803s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.012s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.177s
ok  	github.com/carlosframework/platform/internal/gate	1.722s
ok  	github.com/carlosframework/platform/internal/genselect	0.049s
ok  	github.com/carlosframework/platform/internal/geo	0.017s
ok  	github.com/carlosframework/platform/internal/geoip	0.065s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.249s
ok  	github.com/carlosframework/platform/internal/host	144.226s
ok  	github.com/carlosframework/platform/internal/hostactors	5.486s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.005s
ok  	github.com/carlosframework/platform/internal/hostbox	3.065s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.787s
ok  	github.com/carlosframework/platform/internal/hostenv	0.007s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.012s
ok  	github.com/carlosframework/platform/internal/hostexec	5.331s
ok  	github.com/carlosframework/platform/internal/hostfence	0.069s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.322s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.023s
ok  	github.com/carlosframework/platform/internal/hostguard	1.066s
ok  	github.com/carlosframework/platform/internal/hostops	2.014s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.006s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.007s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.690s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.188s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.004s
ok  	github.com/carlosframework/platform/internal/infraview	0.003s
ok  	github.com/carlosframework/platform/internal/instancectl	5.243s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.833s
ok  	github.com/carlosframework/platform/internal/instancefs	0.010s
ok  	github.com/carlosframework/platform/internal/instancekey	0.005s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.009s
ok  	github.com/carlosframework/platform/internal/layout	1.694s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.003s
ok  	github.com/carlosframework/platform/internal/lease	10.140s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.496s
ok  	github.com/carlosframework/platform/internal/ledger	0.018s
ok  	github.com/carlosframework/platform/internal/mail	0.313s
ok  	github.com/carlosframework/platform/internal/maintenance	0.102s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.533s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.115s
ok  	github.com/carlosframework/platform/internal/metrics	0.014s
ok  	github.com/carlosframework/platform/internal/moves	22.516s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.028s
ok  	github.com/carlosframework/platform/internal/objstore	0.306s
ok  	github.com/carlosframework/platform/internal/ops	0.228s
ok  	github.com/carlosframework/platform/internal/park	0.270s
ok  	github.com/carlosframework/platform/internal/plans	0.013s
ok  	github.com/carlosframework/platform/internal/privatefile	0.007s
ok  	github.com/carlosframework/platform/internal/provision	23.628s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.010s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.666s
ok  	github.com/carlosframework/platform/internal/relay	7.234s
ok  	github.com/carlosframework/platform/internal/release	26.314s
ok  	github.com/carlosframework/platform/internal/rollout	3.837s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.069s
ok  	github.com/carlosframework/platform/internal/rootstate	0.027s
ok  	github.com/carlosframework/platform/internal/runs	0.602s
ok  	github.com/carlosframework/platform/internal/s3compat	0.006s
ok  	github.com/carlosframework/platform/internal/safefile	0.007s
ok  	github.com/carlosframework/platform/internal/schedule	2.651s
ok  	github.com/carlosframework/platform/internal/seeds	0.028s
ok  	github.com/carlosframework/platform/internal/sim/engines	2.725s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	10.841s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.751s
ok  	github.com/carlosframework/platform/internal/status	0.078s
ok  	github.com/carlosframework/platform/internal/steering	0.007s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.308s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.005s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.008s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.008s
ok  	github.com/carlosframework/platform/internal/usage	0.007s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.699s
=== internal_console@0 (85 s)
ok  	github.com/carlosframework/platform/internal/console	84.013s
=== internal_console@1 (41 s)
ok  	github.com/carlosframework/platform/internal/console	40.086s
=== internal_console@2 (44 s)
ok  	github.com/carlosframework/platform/internal/console	42.741s
=== internal_console@3 (43 s)
ok  	github.com/carlosframework/platform/internal/console	41.225s
=== internal_console@4 (45 s)
ok  	github.com/carlosframework/platform/internal/console	43.981s
=== internal_console@5 (42 s)
ok  	github.com/carlosframework/platform/internal/console	40.514s
=== internal_console@6 (40 s)
ok  	github.com/carlosframework/platform/internal/console	38.398s
=== internal_console@7 (36 s)
ok  	github.com/carlosframework/platform/internal/console	35.018s
=== root@0 (107 s)
ok  	github.com/carlosframework/platform	105.872s
=== root@1 (90 s)
ok  	github.com/carlosframework/platform	88.448s
=== root@2 (80 s)
ok  	github.com/carlosframework/platform	78.822s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 50528418 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
  minio: downloaded, digest verified
  mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
  mc: downloaded, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.023s
=== RUN   TestT4StorageProbeMinio
2026/10/23 11:09:08 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E12409131FB052, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.022s
=== RUN   TestS3StoreMinioContract
=== RUN   TestS3StoreMinioContract/conditional_get_and_etag_listing
=== RUN   TestS3StoreMinioContract/bump_through_every_mutation_path
--- PASS: TestS3StoreMinioContract (0.03s)
    --- PASS: TestS3StoreMinioContract/conditional_get_and_etag_listing (0.01s)
    --- PASS: TestS3StoreMinioContract/bump_through_every_mutation_path (0.02s)
PASS
ok  	github.com/carlosframework/platform/internal/release	0.030s
Bucket created successfully `local/dbf-deploy`.
Bucket created successfully `local/dbf-data-ie`.
Bucket created successfully `local/dbf-data-uk`.
local/dbf-deploy versioning is enabled
Created policy `dbf-console` successfully.
Created policy `dbf-ukbox` successfully.
Added user `dbfconsole` successfully.
Added user `dbfukbox` successfully.
Attached Policies: [dbf-console]
To User: dbfconsole
Attached Policies: [dbf-ukbox]
To User: dbfukbox

amadan: job cancelled — a newer push superseded it
diff --git a/docs/superpowers/specs/2026-10-23-durability-design.md b/docs/superpowers/specs/2026-10-23-durability-design.md
new file mode 100644
index 000000000..bd507feb2
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-23-durability-design.md
@@ -0,0 +1,368 @@
+# Durability: every byte off the box, every box rebuildable
+
+Status: DRAFT, 2026-10-23. Written from a live audit of carloku.com on
+2026-10-22 and 23. Platform-wide: carloku.com is the first deployment it
+lands on, and Tito's install has the same gaps (account 665106592142, being
+handled from the Tito side).
+
+## 1. Why
+
+Paul asked three questions on 2026-10-22. Which keys aren't backed up or in
+1Password? Which databases aren't on Litestream? If we lost every Carloku
+box, could we recover? The audit's answer was "tenants mostly yes, the
+console no". The console database, which holds every account, had no copy
+anywhere. Several other things lived only on a box.
+
+Paul's terms, 2026-10-23:
+
+- "If the Irish box disappeared, there's nothing hypercritical on it, but
+ anything more than a few hours worth of data would be a massive pain."
+- "Low hanging fruit first, and by end of session, everything should have
+ resilience and durability built in."
+- Standing rule: carloku.com consumes the platform like any self-hoster.
+ If carloku needs something, the platform provides it, and nobody works
+ around the platform by hand on a box.
+- Standing rule: AWS features are adapters. A deployment on one
+ S3-compatible bucket plus hosts plus DNS must get the same durability.
+
+## 2. Targets
+
+| | Target | Notes |
+|---|---|---|
+| Data loss (RPO), any database | seconds to minutes | Litestream, as today for hibernating instances |
+| Data loss (RPO), app files on disk | at most 1 hour | new; see §5.3 |
+| Data loss, everything, if the AWS account is lost | at most 1 hour behind | the off-account copy, §5.5 |
+| Downtime (RTO), one box | under 4 hours, by runbook | Paul: "nothing hypercritical" |
+| Downtime (RTO), every box | one working day | same runbook, run per box |
+| Proof | every copy restore-tested on a schedule, failures alert | a backup nobody has restored doesn't count |
+
+The 1-hour file RPO and the 4-hour RTO are my call, sized to "a few hours".
+
+## 3. Goals, non-goals, anti-goals
+
+**Goals**
+
+1. Everything a box holds that isn't rebuildable from elsewhere has an
+ up-to-date copy off the box: databases, app files, keys, box settings.
+2. Everything in the AWS account that matters has a copy outside it.
+3. A box can be rebuilt from those copies using platform verbs and a
+ written runbook, and that has been rehearsed.
+4. When any copy stops being current, an alert fires and reaches a person.
+5. All of it is platform behaviour, on by default for new deployments. A
+ self-hoster gets it by configuring a destination, not by copying
+ carloku's scripts.
+
+**Non-goals**
+
+- Hot standby or automatic failover. The region-HA plans (2a, 2b merged
+ dormant) cover that separately. This design is about losing nothing and
+ being able to rebuild.
+- An AWS Organizations or multi-account setup for carloku.
+- Backing up what is rebuildable: release binaries in `/opt/carlos/versions`,
+ static sites in `/data/static` (served from the bucket), GeoIP data,
+ certificates (republished from `control/certs/`), ClamAV signatures,
+ amadan's `amadan-materialized/repo.git` trees (3.5 GB; "pure scratch:
+ rebuildable anytime from the store, never replicated", amadan
+ `internal/repod/githttp.go`).
+
+**Anti-goals** (would make this a failure even if it works)
+
+- Any config on a box maintained by hand. Today's `/etc/litestream.yml` on
+ the Irish box is exactly the thing that dropped the console database.
+- A copy that no drill ever restores.
+- An alert rule that can never fire (today's lag rule is wired to nil).
+- Two Litestream writers on one database's WAL; it corrupts the replica.
+- Requiring AWS for any of it.
+- Secrets printed to terminals, logs, or SSM command history.
+
+## 4. Where things stand (2026-10-23)
+
+Done this session:
+
+- `console.db`, the `proving-bots` sidecar, Kass's fourth instance database
+ and Woodstar's `push.db` and `unfurl.db` now replicate. They were added by
+ hand to the Irish box's existing hand-written `/etc/litestream.yml`
+ (backup `/etc/litestream.yml.bak-20261023`), and restore-tested: integrity
+ ok, 58 of 58 accounts. Interim only: §5.1 replaces the file.
+- `carloku-infrastructure/scripts/backup-to-1password.sh` (uncommitted)
+ copies the 12 `/carlos/*` SSM secrets and an encrypted per-box archive
+ (`/etc/carlos`, units and drop-ins, instance `.env` files, Woodstar seeds)
+ into 1Password. Paul runs it; agents can't read the values.
+
+Still open, from the audit:
+
+| Gap | Where |
+|---|---|
+| The Irish Litestream config is hand-written, runs v0.3.13 (24h history, the 7-day setting is ignored), and nothing regenerates it | IE |
+| UK, US and AU have no box-wide Litestream, so a non-hibernating instance there has no backup | UK, US, AU |
+| `welp.bae`'s data (2.6 GB: `identity.db`, `registry.db`, a mailbox tree) is unreplicated; the route's DBPath names a file that doesn't exist | IE |
+| App files beside databases have no copy: `/data/blobs` (1.1 GB, shared by flat apps), `comic-cleaner` blobs (256 MB), others | IE |
+| Instance keys and admin tokens (`/data/<host>.env`, 295 on IE) exist only on their box | all |
+| Relay masters, Keymail push keys, `release.env`: box-only | IE |
+| `host.env` has no copy. The platform's `carlos system host-env` SSM escrow is built for region-HA group boxes: its required list includes the claim and HA switches carloku's boxes don't set, and its allowlist refuses credentials such as `CARLOS_DNSIMPLE_TOKEN` | all |
+| Release, CLI-update and apt signing keys, Kass peppers: SSM only, one account | account |
+| No copy of anything outside the AWS account; tofu state is in the same account | account |
+| `carlos-store-*` tenant buckets and `keymail-blobs` aren't versioned | account |
+| The Irish box role can `s3:DeleteObjectVersion` on its data bucket (region-host v0.23.0+) | IE |
+| No EBS snapshots | all |
+| No alert for Litestream down, a failed drill, a stale replica, or an unreplicated database; restore-verify publishes nothing; hibernate-verify isn't armed | all |
+| No restore-from-snapshot verb for the registry; no carloku box-loss runbook; never rehearsed | all |
+| Hand-made units on IE (console, Woodstar, keymail-relay, amadan-backup) aren't in any repo; Keymail's IMDS uid rule isn't in `harden-host.sh` | IE |
+| amadan-hub's instance replica stopped 2026-08-08; covered only by the hourly `amadan-backup.timer` from an unmerged branch | IE |
+
+## 5. The design
+
+Six workstreams, ordered so the cheapest risk reduction lands first (§6).
+
+### 5.1 The platform owns the whole replication set
+
+The box-wide replication config is generated, kept current automatically,
+and covers every database on the box.
+
+- **Routes can name more than one database.** DBPath becomes a list. Any
+ entry may be a directory, meaning every non-empty `*.db` beneath it (the
+ existing `treeDBs` rule), for any backing, not just `BackingUnit`. Set by
+ `carlos add --db` (repeatable) and a new `carlos route --db` for addr
+ routes. This lets Kass (a tree in its state dir) and Woodstar (services
+ proxied by addr, databases in `/data/woodstar/`) be described.
+- **Exec instances replicate their whole instance directory's databases.**
+ For a nested-layout instance, every `*.db` under
+ `/data/instances/<id>/` is claimed, not only `<host>.db`. That fixes
+ `welp.bae`. For hibernating instances the activator's embedded
+ replicator runs one Litestream DB per file. Box-wide replication still
+ never touches a hibernating instance's files.
+- **The generator runs itself.** The agent re-runs the `ops litestream`
+ generation on route changes and every 10 minutes, and restarts the
+ replicator only on "changed". The shrink guard stays. A shrink still
+ needs an operator decision, and it now raises an alert instead of
+ failing quietly in a log.
+- **Every box runs box-wide Litestream.** Bootstrap installs the pinned
+ binary (0.5.15, checksum-verified, from the release bucket rather than
+ GitHub), writes `/data/litestream.env`, and enables the unit. The unit
+ runs as root with the capabilities unit tenants need: `CAP_DAC_OVERRIDE`,
+ `CAP_DAC_READ_SEARCH`, `CAP_FOWNER` and `CAP_CHOWN`. That's today's
+ hand-made `user.conf` drop-in, moved into the shipped unit. `CAP_CHOWN`
+ was added on IE on 2026-10-23, after Tito's review found it missing: when
+ root is the first to open a database whose `-wal` and `-shm` are missing,
+ SQLite chowns them to the database's owner, and without the capability
+ they stay root-owned and the app can't write them.
+- **Cutover on the Irish box** happens once the generator describes
+ everything the hand file names. Moving 0.3.13 to 0.5.15 changes the
+ replica format, so every box-wide database starts a new lineage. The old
+ 0.3 replicas stay in the versioned bucket until the new ones have passed
+ a restore drill, then expire through a lifecycle rule. The plan written
+ on 2026-08-15 (`2026-08-15-flagship-litestream-0.5-upgrade.md` in
+ platform-infrastructure) is the starting point.
+
+### 5.2 Proof: drills and alerts that fire
+
+- `restore-verify` and `hibernate-verify` are armed by bootstrap on every
+ box. A drill proves currency, not just shape. A replica stuck at its
+ first snapshot passes `integrity_check` and a table count, so the drill
+ keeps its change-counter freshness check and never treats schema-only
+ success as current. They publish their verdicts to `control/verify/<box>.json`, and the
+ console shows them.
+- New alert rules, through the existing alert delivery (SNS email today;
+ the webhook stays optional):
+ - box-wide Litestream not active, or restarting (slice A of #284 already
+ adds `restarts@<unit>`)
+ - a drill failed, or hasn't run in 36 hours
+ - a database on disk that no replication claims, older than 15 minutes
+ (the generator's "claimed by no route" list, turned into an alert)
+ - a shrink the guard refused
+ - the off-account copy (§5.5) more than 2 hours behind
+ - file snapshots (§5.3) more than 2 hours behind
+- Replica freshness, meaning "this replica is still tracking its source",
+ is #290's open question. This design needs a freshness signal and takes
+ whichever design Paul rules for on #290. Until then the daily drill's
+ existing freshness check (`internal/ops/freshness.go`) is the signal.
+- The lag rule's nil wiring (`metrics_loop.go:492`) gets fixed or the rule
+ removed. My call: removed, since the freshness alert replaces it.
+
+### 5.3 App files on disk
+
+Apps write files beside their databases: mailboxes, uploads, blobs. The
+platform has never said whether those are durable, and they aren't.
+
+My recommendation, which Paul needs to confirm (§9, question 1): back them
+up.
+
+- An hourly, incremental, content-addressed snapshot of each instance's
+ directory to the bucket, under `instances/<host>/files/`, skipping
+ databases (Litestream has those), `-wal`, `-shm`, `.env` (§5.4) and the
+ replicator's own dot-directories. Unchanged files cost nothing to
+ re-snapshot. Keep 7 days of snapshots.
+- The flat layout's shared `/data/blobs` can't be attributed to one host
+ (`internal/layout/extras.go`). It's snapshotted as one box-level set
+ until the layout pass moves its contents into instance directories.
+- `carlos instances files restore <host> [--at <time>]` restores them.
+ The wake path restores them too when a hibernating instance wakes on a
+ box that lacks them, as it already does for the database.
+- Moves reuse it: an offline move's archive could be built from the latest
+ snapshot. That's a later simplification, not part of this work.
+
+### 5.4 Keys and box settings
+
+- **Instance `.env` escrow.** When `<host>.env` is minted or changed, the
+ platform seals it to the deployment's secrets public key
+ (`appconfig.SealBound`, bound to account and app) and writes it to
+ `instances/<host>/env.sealed` in the bucket. Restore needs
+ `CARLOS_SECRETS_KEY`, which is in SSM in four regions and in 1Password,
+ so a rebuilt box can unseal it. Nothing on a box can read another
+ instance's escrow without that key.
+- **Platform-owned box secrets** (relay masters, `release.env`): the same
+ sealing, at `control/box-secrets/<box>/<name>.sealed`. Bootstrap restores
+ them.
+- **`host.env`:** the existing `carlos system host-env` escrow becomes
+ usable by any box, not only region-HA group boxes. The claim and HA
+ switches move from required to optional for a box that isn't in a group.
+ Credentials its allowlist refuses on purpose (`CARLOS_DNSIMPLE_TOKEN`)
+ go to the sealed box secrets above instead. The bucket-sealed copy is
+ the non-AWS fallback (§7).
+- **Signing keys** (release, CLI update, apt) **and Kass peppers:**
+ 1Password through the script. They stay operator-held; the platform
+ doesn't escrow its own signing keys.
+- **Tenant secrets the platform doesn't own** (Keymail's APNs key in
+ `/etc/keymail-push/`, Woodstar's bridge seeds) belong in the tenant's
+ sealed app config. Until each app moves them there, the 1Password script
+ is their copy. That's carloku's housekeeping, not a platform feature.
+- **Narrow who can read `/carlos/*` SSM.** Every box can read every
+ parameter today, including the release key. Fix it, with a separate path
+ for the release key that only the console's role can read.
+
+### 5.5 The copy outside the account
+
+A platform verb, `carlos ops offsite`, keeps a second S3-compatible
+destination in step with the deployment's buckets.
+
+- **What it copies:** the deployment bucket, every region's data bucket,
+ every `carlos-store-*` tenant bucket, and anything named in
+ `CARLOS_OFFSITE_EXTRA` (on carloku: the tofu state bucket and
+ `keymail-blobs`). It copies current versions only; history lives in
+ versioning at both ends.
+- **How:** incremental by listing, comparing ETag and size, then Get from
+ the source and Put to the destination. Server-side copy can't cross
+ providers. Litestream's LTX files are immutable, so most runs move only
+ new segments. It runs every 15 minutes, on the box holding the
+ housekeeper lease, so exactly one box does it.
+- **The destination:** Hetzner Object Storage (Paul, 2026-10-23: "we use
+ our Hetzner account"), versioned, with object lock in governance mode
+ and a 30-day default retention (my call). The destination credentials
+ can write but not delete locked versions, so a compromised box can't
+ destroy the copy.
+- **Encryption:** objects are sealed client-side to the deployment's
+ secrets key before upload (my recommendation, §9 question 2). Then
+ Hetzner holds only ciphertext, and the copy still works with
+ `CARLOS_SECRETS_KEY` from 1Password.
+- **Configuration:** `CARLOS_OFFSITE_ENDPOINT`, `_BUCKET` and `_REGION`,
+ plus credentials sealed in the deployment config. Not `CARLOS_S3_ENDPOINT`:
+ that's process-wide and would redirect the primary.
+- **It publishes** `control/offsite.json` (last complete pass, objects and
+ bytes behind) for the alert in §5.2.
+- **Restore:** `carlos ops offsite restore --to <bucket>` reverses it, so a
+ new account or provider can be seeded from it.
+- **Route53:** the console exports each zone's records to
+ `control/dns/<zone>.json` daily, so the copy includes DNS.
+
+### 5.6 Rebuilding a box
+
+- **`carlos system restore-registry --from <box-label>`** copies the dead
+ box's latest registry snapshot into place on a fresh box before the edge
+ starts, and refuses to start if the label is a live box (the clobber risk
+ `snapshot_loop.go` notes). It replaces the hand-run `aws s3 cp` step.
+- **Bootstrap restores, in order:** `host.env` from the host-env escrow;
+ sealed box secrets; the registry (when told to, with `--from`); then it
+ starts the edge. Tenants restore themselves on first wake. Their database
+ comes from Litestream, their files from §5.3, their `.env` from §5.4.
+- **Hand-made units on IE** move into code. The console unit is the
+ platform's to ship, since the console is part of the platform. Woodstar,
+ keymail-relay and amadan-backup are tenant services and move into
+ `carloku-infrastructure` as files plus an install script. The Keymail
+ IMDS uid rule moves into `harden-host.sh`.
+- **EBS snapshots:** a daily DLM policy on every carloku box, 7 days kept,
+ in `carloku-infrastructure`. Carloku's AWS-only safety net. It catches
+ anything this design missed, and it's not a platform feature.
+- **Runbook** in `carloku-infrastructure/RUNBOOK-box-loss.md`, adapted
+ from Tito's `RUNBOOK-edge-failover.md`, using only platform verbs.
+- **Rehearsal:**
+ - First, rebuild a dev box (`carlos-dev-*`, account 640012953448) from
+ nothing, timed.
+ - Then restore one carloku database and one instance's files from the
+ Hetzner copy alone.
+ - No live flagship box is destroyed to rehearse.
+
+### 5.7 Smaller fixes
+
+- Version the `carlos-store-*` buckets at creation (`EnsureBucket`), with a
+ 30-day noncurrent-version expiry, and backfill existing ones. The
+ console's bucket-delete path must then delete every version and delete
+ marker first, or deleting a store breaks. That has to land before or
+ with the versioning. Version
+ `keymail-blobs` in tofu.
+- Drop `s3:DeleteObjectVersion` from the region-host data-bucket statement
+ unless something needs it. The builder checks who added it in 3ca43d33
+ and why.
+- Add a noncurrent-version lifecycle to the data buckets (30 days), so
+ versioning's cost is bounded.
+- Merge `amadan-backup` properly or replace it. Once §5.1 lets amadan
+ declare its databases (a tree under `/data/amadan/instances`), it's
+ ordinary replication.
+- Remove leftovers the generator lists as unclaimed, after a check:
+ `opgate-a5wpd4.bet` (renamed), `seapointish-purge-backup.db` (410 MB),
+ `contra-fresh-*`.
+
+## 6. Order
+
+| Step | What | Who | Size |
+|---|---|---|---|
+| 0 | console.db and four others replicating (done); 1Password script (done) | done | |
+| 1 | Run the 1Password script | Paul | minutes |
+| 2 | Commit the script; IMDS rule into `harden-host.sh`; EBS snapshots (DLM); version `keymail-blobs` | carloku ops | small |
+| 3 | §5.1 multi-DB routes, whole-instance-dir replication, self-running generator, Litestream on every box; then the IE cutover to 0.5.15 | platform | large |
+| 4 | §5.2 drills armed and published; new alert rules | platform | medium |
+| 5 | §5.4 `.env` and box-secret escrow; SSM read narrowing | platform | medium |
+| 6 | §5.5 `carlos ops offsite` to Hetzner | platform | large |
+| 7 | §5.3 app-file snapshots | platform | large |
+| 8 | §5.6 `restore-registry`, bootstrap restore order, units into code, runbook, rehearsal | platform + carloku | medium |
+| 9 | §5.7 smaller fixes | both | small |
+
+Each platform step goes through the usual spec review, plan, build, Astra
+review, release and roll. Paul's "by end of session" is the goal. I won't
+skip review rounds to meet it. Steps 1 and 2 are today. Steps 3 to 9 are
+separate plans, written and built in this order.
+
+## 7. Provider boundary
+
+| Piece | Core (any S3-compatible store) | AWS adapter | Without AWS |
+|---|---|---|---|
+| Replication, drills, alerts, file snapshots, escrow, offsite, restore-registry | all core | none | same |
+| `host.env` escrow | bucket-sealed fallback | SSM (existing) | uses the sealed bucket copy |
+| SSM read narrowing | n/a | IAM | n/a |
+| EBS snapshots | n/a | carloku tofu only | operator's choice |
+| Route53 export | DNS provider interface | Route53 | the deployment's DNS provider, or none |
+
+Core packages don't import the AWS SDK, enforced by the existing import
+test. The offsite destination is any S3-compatible endpoint. Hetzner is
+just carloku's choice.
+
+## 8. Left to the builder
+
+- Package layout, names of internal types, unit file details.
+- The file-snapshot format, as long as it's incremental, content-addressed,
+ restorable at a point in time and sealed when it leaves the account.
+- How the agent triggers regeneration (route-change hook plus timer, or
+ timer only), as long as a new database is claimed within 10 minutes.
+- Exact alert thresholds within the targets in §2.
+
+## 9. Questions for Paul
+
+1. **App files (§5.3):** back up files apps write on disk (my
+ recommendation), or declare them not durable and move apps like `welp`
+ to object storage?
+2. **Off-account encryption (§5.5):** seal objects client-side before they
+ reach Hetzner (my recommendation), or rely on Hetzner's server-side
+ encryption?
+3. **Retention:** 30 days of object lock at Hetzner and 7 days of
+ point-in-time history in-account. Longer?
+4. **#290:** this design needs a replica-freshness signal; the design there
+ is still awaiting your ruling.