design-cohort-activation: scope the backfill, done and freeze to one activation cohort; empty first activation instead of Plan 5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+331
−0
docs/superpowers/specs/2026-10-19-cohort-activation-design.md+331 −0
CI passed — run details
CI log
=== gate ===
go: downloading github.com/aws/aws-sdk-go-v2/service/autoscaling v1.74.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ssm v1.74.0
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.011s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok github.com/carlosframework/platform 303.273s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.697s
ok github.com/carlosframework/platform/cmd/moveprobe 93.390s
ok github.com/carlosframework/platform/cmd/zddprobe 0.707s
ok github.com/carlosframework/platform/internal/activator 250.713s
ok github.com/carlosframework/platform/internal/agentsock 0.015s
ok github.com/carlosframework/platform/internal/alerting 0.003s
ok github.com/carlosframework/platform/internal/analytics 0.018s
ok github.com/carlosframework/platform/internal/appconfig 2.474s
ok github.com/carlosframework/platform/internal/apperr 0.081s
ok github.com/carlosframework/platform/internal/applog 0.133s
ok github.com/carlosframework/platform/internal/awsinv 0.004s
ok github.com/carlosframework/platform/internal/boxchoice 0.004s
ok github.com/carlosframework/platform/internal/boxid 0.627s
? 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.010s
ok github.com/carlosframework/platform/internal/caps 0.013s
ok github.com/carlosframework/platform/internal/certwarm 0.004s
ok github.com/carlosframework/platform/internal/cgroup 0.698s
ok github.com/carlosframework/platform/internal/changelog 0.331s
ok github.com/carlosframework/platform/internal/claimactivation 0.783s
ok github.com/carlosframework/platform/internal/clamd 0.056s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 404.351s
? 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 5.780s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.009s
ok github.com/carlosframework/platform/internal/dbverify 0.005s
ok github.com/carlosframework/platform/internal/dnsrec 0.002s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.387s
ok github.com/carlosframework/platform/internal/doorkey 0.005s
ok github.com/carlosframework/platform/internal/economics 0.003s
ok github.com/carlosframework/platform/internal/edge 45.185s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.008s
ok github.com/carlosframework/platform/internal/fleetapi 0.135s
ok github.com/carlosframework/platform/internal/gate 1.656s
ok github.com/carlosframework/platform/internal/genselect 0.014s
ok github.com/carlosframework/platform/internal/geo 0.010s
ok github.com/carlosframework/platform/internal/geoip 0.030s
ok github.com/carlosframework/platform/internal/historyreadback 0.152s
ok github.com/carlosframework/platform/internal/host 181.404s
ok github.com/carlosframework/platform/internal/hostactors 6.117s
ok github.com/carlosframework/platform/internal/hostactors/awsmembership 0.014s
ok github.com/carlosframework/platform/internal/hostbox 2.720s
ok github.com/carlosframework/platform/internal/hostclaim 0.587s
ok github.com/carlosframework/platform/internal/hostenv 0.013s
ok github.com/carlosframework/platform/internal/hostevidence 0.013s
ok github.com/carlosframework/platform/internal/hostexec 5.211s
ok github.com/carlosframework/platform/internal/hostfence 0.041s
ok github.com/carlosframework/platform/internal/hostfiles 10.602s
ok github.com/carlosframework/platform/internal/hostfloor 0.015s
ok github.com/carlosframework/platform/internal/hostguard 1.028s
ok github.com/carlosframework/platform/internal/hostops 1.599s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.014s
ok github.com/carlosframework/platform/internal/hostquarantine 0.013s
ok github.com/carlosframework/platform/internal/hostrelocate 0.464s
ok github.com/carlosframework/platform/internal/hostresolve 0.140s
ok github.com/carlosframework/platform/internal/hostwrite 0.013s
ok github.com/carlosframework/platform/internal/infraview 0.015s
ok github.com/carlosframework/platform/internal/instancectl 5.252s
ok github.com/carlosframework/platform/internal/instanceexec 2.748s
ok github.com/carlosframework/platform/internal/instancefs 0.019s
ok github.com/carlosframework/platform/internal/instancekey 0.015s
ok github.com/carlosframework/platform/internal/instanceupdates 0.020s
ok github.com/carlosframework/platform/internal/layout 1.536s
ok github.com/carlosframework/platform/internal/lbhealth 0.002s
ok github.com/carlosframework/platform/internal/lease 9.824s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.008s
ok github.com/carlosframework/platform/internal/ledger 0.013s
ok github.com/carlosframework/platform/internal/mail 0.165s
ok github.com/carlosframework/platform/internal/maintenance 0.082s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.498s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.077s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 21.304s
ok github.com/carlosframework/platform/internal/moves/archive 0.797s
ok github.com/carlosframework/platform/internal/objstore 0.195s
ok github.com/carlosframework/platform/internal/ops 0.186s
ok github.com/carlosframework/platform/internal/park 0.210s
ok github.com/carlosframework/platform/internal/privatefile 0.003s
ok github.com/carlosframework/platform/internal/provision 37.372s
ok github.com/carlosframework/platform/internal/publicrelease 0.012s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 5.560s
ok github.com/carlosframework/platform/internal/relay 7.225s
ok github.com/carlosframework/platform/internal/release 24.301s
ok github.com/carlosframework/platform/internal/rollout 3.926s
ok github.com/carlosframework/platform/internal/rolloutwords 0.003s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/rootparticipation 0.057s
ok github.com/carlosframework/platform/internal/rootstate 0.026s
ok github.com/carlosframework/platform/internal/runs 0.574s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.003s
ok github.com/carlosframework/platform/internal/schedule 2.501s
ok github.com/carlosframework/platform/internal/seeds 0.011s
ok github.com/carlosframework/platform/internal/sim/hostlife 10.165s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.833s
ok github.com/carlosframework/platform/internal/status 0.062s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.240s
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.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.003s
ok github.com/carlosframework/platform/internal/updatewords 0.004s
ok github.com/carlosframework/platform/internal/usage 0.004s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.580s
=== 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: 49414306 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.022s
=== RUN TestT4StorageProbeMinio
2026/10/19 22:26:21 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E00EAB8B8DD4F3, 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.020s
minio contract: PASS
diff --git a/docs/superpowers/specs/2026-10-19-cohort-activation-design.md b/docs/superpowers/specs/2026-10-19-cohort-activation-design.md| new file mode 100644 |
| index 000000000..df976e88d |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-10-19-cohort-activation-design.md |
| @@ -0,0 +1,331 @@ |
| +# Cohort activation: one claims-on box inside a populated deployment |
| + |
| +Status: draft for platform-1e and Paul, 2026-10-19. Nothing here is built. |
| +Code citations are to main at `96db0efe` unless they name branch |
| +`claim-step8-readiness` (`232baff2`, parked). |
| + |
| +The dev trial is paused at the end of step 7. Activation as built assumes the |
| +whole deployment turns on at once. The trial turns on ONE box (leo, roster pool |
| +`cohort1`) while sophie (roster pool `console`, claims off, serving pool-less |
| +records and the console) and dennis (outside the roster) keep running main's |
| +code. This note scopes the backfill, the adopt step, `done` and the freeze to |
| +that cohort, and recommends an empty first activation instead of Plan 5. |
| + |
| +## Assumptions |
| + |
| +- A1. The only cohort-pool record in the dev bucket is the legacy `claimtrial`. |
| + wakecheck and rollcheck-0261 are pool-less (runbook P5 and step 4). |
| +- A2. Neither sophie nor dennis sets `CARLOS_POOLS` (step 4's record for |
| + sophie; dennis "serves nothing"). This is read again before the window |
| + (section 1). |
| +- A3. Turning sophie's console claim runtime off and on again is safe while no |
| + claim and no backfill plan exist. Not exercised. |
| +- A4. `apps create`, `ship`, `promote` and store-mode `instances enable` work |
| + for a new app on a claim-mode console. Not exercised on dev. |
| +- A5. A claims-on box acts only on hosts it holds a claim for: |
| + `WorkSource.Select` refuses a host with no claim (`internal/hostbox/work.go:69-71`). |
| +- A6. A claims-off box ignores claims and mints whatever its own host.env scope |
| + matches: `inScope` and `poolServed` (`internal/provision/reconcile.go:401-414`, |
| + `:2155-2160`). With no `CARLOS_POOLS` it serves pool `""` only. |
| + |
| +## 1. Scoping |
| + |
| +**Recommendation.** The backfill plan (`control/hosts-backfill.json`) gains an |
| +additive `scope`: `{namespace, regions, pools}`, the same three fields as |
| +`hostactors.Scope` (`internal/hostactors/census.go:44-54`). One predicate |
| +decides membership everywhere: |
| + |
| +- a **box** is in scope when the create census would require it: roster |
| + namespace, region and pool all match (`census.go:99-101`); |
| +- a **record** is in scope when its pool is a cohort pool and its region, |
| + resolved for home the way `StartCreate` does (`production_create.go:222-225`), |
| + is a cohort region. |
| + |
| +Every consumer then reads only the in-scope part: |
| + |
| +| Consumer | Today | Scoped | |
| +|---|---|---| |
| +| `backfill-collect` (branch, `claimactivation/backfill.go:136-177`) | freezes every record in the bucket | freezes in-scope records only | |
| +| Migration census (`rootmigration_census.go:65-95`) | every record must be frozen, by etag | in-scope records must be frozen; `s.Records` holds only them, so out-of-scope writes cannot move the snapshot fingerprint (`rootmigration.go:82-92`) | |
| +| `census`, `Publish`, `VerifyAdopting` (`backfill.go:217-264`) | a report from every roster box | reports from in-scope boxes only | |
| +| `ReconcileClaims` (`backfill_controller.go:89-113`) | tries to adopt every unclaimed record | skips out-of-scope records | |
| +| `CheckCreateFreeze` (`create_freeze.go:12-28`) | refuses every write until `done` | see section 2 | |
| + |
| +A plan without `scope` keeps today's deployment-wide behaviour byte for byte. |
| +An older reader of a scoped plan fails closed: it freezes the whole deployment |
| +until `done`, and `done` still arrives. |
| + |
| +`collect` refuses a cohort-pool record that is everywhere-class or has |
| +replicas. Such a record crosses regions on claims-off boxes, and `Adoption` |
| +already refuses it (`backfill.go:370-376`). The cohort pool must be a named |
| +pool. It is never `""`: pool `""` is what every unpooled claims-off box serves |
| +(A6), which is the two-writer hazard H1. |
| + |
| +**What must be proven about an out-of-scope box.** The single-writer property |
| +for a cohort host is this: the only boxes that can serve it are claims-on |
| +boxes holding its claim. Claims-on boxes are covered by A5. For claims-off |
| +boxes, there are three parts: |
| + |
| +1. **Record placement (mechanical).** The host's record carries a cohort pool, |
| + and its placement is not everywhere-class, as above. |
| +2. **Signed roster (mechanical).** Every roster box whose roster pool is a |
| + cohort pool is a cohort box, and its signed report says |
| + `HostClaimEnabled` (`census.go:149`). `backfill-done` checks this. |
| +3. **Signed placement statement (attested).** A claims-off box's roster pool |
| + says nothing about what it serves. Sophie is roster pool `console` but mints |
| + every pool-less record. Its real scope is host.env, and nothing signs that |
| + today. So the operator reads `CARLOS_POOLS`, `CARLOS_REGION` and |
| + `CARLOS_HOST_CLAIM` on every box whose role can read the deployment bucket |
| + (for dev: sophie, dennis and leo). The read is over SSM and read-only. The |
| + readings go into the plan as `outside: {box: {region, pools, read_at}}`, so |
| + the root signature covers them. `collect` refuses if a roster box outside |
| + the scope has no entry, or if any entry lists a cohort pool. This is an |
| + operator statement, not box evidence. Production replaces it (section 5). |
| + |
| +**Sophie and its pool-less records.** Sophie serves pool `""`; the cohort |
| +serves `cohort1`. `poolServed` keeps them disjoint, in both directions. So |
| +sophie's records are never frozen, never adopted and never need its report, |
| +and sophie never mints a `cohort1` record. Sophie stays the console's compute |
| +host. The create census still verifies that binding through sophie's box |
| +report (`census.go:171-174`, `:385-421`), which is the only thing the cohort |
| +needs from it. |
| + |
| +**Cost.** One additive plan field, one predicate and five call sites. The |
| +attestation is trusted for the trial: if someone later adds `cohort1` to a |
| +claims-off box's host.env, that box becomes a second writer, which main allows |
| +today too. |
| + |
| +**Rejected.** |
| +- A census key and the migration census on sophie (S3 option a). Sophie's |
| + named-unit console route trips the activation preflight |
| + (`claimactivation_preflight.go:25-42`). It would also turn on root census |
| + work on a box the trial keeps claims-off. |
| +- Taking sophie out of the roster (option c). The console's compute binding |
| + must name a roster box (`census.go:395-399`). |
| +- A "holds nothing" report signed by sophie. That needs a census key and new |
| + code on a claims-off box (ruling 6). |
| +- Using roster pools as the proof for claims-off boxes. They are not what the |
| + reconciler reads. |
| + |
| +## 2. The `done` path and the freeze |
| + |
| +**Who writes `done`, and when.** A new signer phase, `claims run |
| +backfill-done`, run on the trusted signer like `collect` and `adopt`. It |
| +writes If-Match on the adopting plan's etag. The plan is root-signed |
| +(`create_freeze.go:21`), so neither the console nor a box can write it. In the |
| +trial it runs straight after `backfill-adopt` (new 8.2c) and before step 9. |
| + |
| +**What `done` requires.** |
| +- The plan reads `adopting`, with the named id, at the current roster revision. |
| +- `VerifyAdopting` still passes on the in-scope reports (branch, |
| + `backfill_publish.go`). |
| +- Every roster box in scope is active and reports `HostClaimEnabled`. |
| +- The in-scope records listed now are exactly `plan.Hosts`, so no cohort record |
| + appeared or moved. |
| +- Every host in `plan.Hosts` holds a serving claim whose holder matches the |
| + frozen key and acquisition, executed by the box the plan names. |
| + |
| +An in-scope host that no in-scope box reported is a refusal. It is a cohort |
| +record that only an outside box could be serving, which breaks part 3 above. |
| +Out-of-scope hosts are never in `plan.Hosts`, so they cannot block `done`. |
| +That removes the "unadopted hosts block it forever" trap. In the empty trial |
| +`plan.Hosts` is empty, so the last condition holds trivially. |
| + |
| +**What the freeze covers.** `CheckCreateFreeze` takes the record being written |
| +and refuses (`ErrPending`) only when the plan is not `done` and either has no |
| +scope or the record is in scope. Two callers get the record: create |
| +(`production_create.go:38`) and record writes (`record_writes.go:90`). The |
| +callers that have no record keep the deployment-wide check until `done`: the |
| +effect runner (`effect_runner.go:59`) and the move grants. That is bounded by |
| +`done`. Moves are off. On dev, no legacy app has an admission, so no effects |
| +run. |
| + |
| +**For how long.** In the trial, from `collect` to `done`. Because the cohort is |
| +empty, `collect` can run in the signing session before the window. The freeze |
| +then covers `cohort1` only, and no `cohort1` create can happen anyway until |
| +leo is claims-on (the create census needs leo's `HostClaimEnabled`, |
| +`census.go:149`). A plan that sticks at `collecting` or `adopting` blocks |
| +nothing outside the cohort, so no abandon phase is needed for the trial. |
| + |
| +**What an operator sees.** An in-scope write gets the claim door's 409 and its |
| +busy message, with `Retry-After: 2` (`claim_response.go:23-24`, `:29-33`). |
| +Out-of-scope writes behave as now. The phase can be read from `claims run`'s |
| +summary, or from the plan object's head (runbook P-B1). |
| + |
| +**Rejected.** |
| +- A deployment-wide freeze with a quick `done`. It still freezes sophie's |
| + tenants, and a stop between phases freezes dev until someone acts. |
| +- No freeze. A cohort record could then appear between `collect` and |
| + `adopt`, past the census. |
| +- The console writing `done` when adoption settles. The console has no release |
| + key. |
| + |
| +## 3. S4: an empty cohort box is the first activation |
| + |
| +**Recommendation.** Yes. Activate leo empty and create the acceptance host |
| +under claims. That removes Plan 5 from the trial, and the binding scope |
| +supports it: "Historical-record conversion for records that cannot exist at |
| +first activation: FREEZE (explicit refusal is enough)" |
| +(`activation-scope-2026-10-16.md`). So does the approved seed copy: "A proven |
| +new empty host needs no files seed" (runbook line 2397). |
| + |
| +**What "empty" means, checked by machine:** |
| +- E1. There is no in-scope record and no claim. `collect` in empty mode |
| + refuses unless `plan.Hosts` is empty and `control/hosts/` is empty. |
| +- E2. Leo's signed migration report lists zero installations, and the census |
| + completes. That means no retained installation, no launch, no move journal |
| + and no unfinished reservation (`rootmigration_census.go:146-215`). |
| +- E3. Leo's registry has no row. The branch's routes reader is reused, but any |
| + row that no claim owns refuses the census. It is no longer reported as a |
| + legacy installation. |
| +- E4. Leo's managed roots hold only shared classes: the window masks (a), the |
| + certificate cache (b) and the box files (c), all from the branch. There is no |
| + retained-unowned class (e). This matters after activation as well. The new |
| + host's first install runs the per-host census, which refuses every |
| + unclassified file of any non-claimed installation |
| + (`empty_census.go:233-241`; only claimed siblings are excused, |
| + `rootinventory_sibling.go:22-85`). |
| + |
| +**Getting leo empty (before the window, claims still off on leo):** |
| +1. Delete the legacy `claimtrial` instance. The claim-mode console cannot do |
| + it: `claimtrial` has no admission, and a record with no claim "waits for |
| + backfill" (`claim_retire.go:33-71`). The delete verb is console-only |
| + (`instances_cli.go:620`). So sophie's console runs with its claim runtime |
| + off for this one delete: `console-config` without |
| + `--enable-console-runtime`, then a restart, the delete, the runtime back on, |
| + and `console-report` (A3; Paul's go). Leo's claims-off removal phase then |
| + retires the row. |
| +2. Move claimtrial's and wakecheck's leftovers aside on leo, with the |
| + delta's move-aside recipe applied to both hosts. Certificates stay: the |
| + `acme` tree is a shared class. |
| +3. Read-only checks: P4 shows 0 rows, P6 passes, no `cohort1` record exists, |
| + and the `outside` readings are taken (section 1). |
| + |
| +**The acceptance host under claims (after step 9).** The legacy app |
| +`claimtrial` cannot get an admission. A legacy grant needs an adopting plan |
| +(`backfill.go:166-175`, `backfill_publish.go:126-168`), and its prefix is not |
| +free for a new enrollment (`enroll.go:94-99`). So the acceptance runs on a new |
| +app, `claimtrial2`, whose admission comes from |
| +`createClaimAppRow`/`EnrollNewApp` (`claim_app_enroll.go:16-43`): |
| +1. `apps create`, `ship` wc4, `promote` to `edge`, and store-mode |
| + `instances enable --pool cohort1`, as step 4 does. |
| +2. `console-report` on sophie, immediately before the create. The create |
| + census reads the console process from sophie's box report, which the agent |
| + overwrites within about 6 minutes (#312, `census.go:400-418`). |
| +3. `instances create`. The claim path (`apiinstances.go:1623-1630`) selects |
| + leo, the only active `cohort1` box (`production_create.go:136-152`), takes |
| + the scoped census (`:226`) and starts a create with one participant and no |
| + park obligations (`hostclaim/create_selection.go:8-23`). Leo's first |
| + install passes the per-host census through its new-host branch |
| + (`empty_census.go:130-132`). |
| +4. Only after leo reports the install is the certificate hello sent. A mirrored |
| + certificate naming the host before its first install would refuse that |
| + install (branch `claimcensus.go`, `claimBoxDataCensus` comment). |
| + |
| +Step 10's gate, "a real launch and two cold wakes with real origin evidence", |
| +holds unchanged. Only the expected counts change: `hits=` starts after the |
| +first wake's own rows, not at 4. Step 8.3 (files seed) is skipped, so H6 goes |
| +away. |
| + |
| +**Cost.** One console-runtime off/on on sophie. The files seed's KEEP items |
| +(the reboot race, retained parks) go unexercised until the first populated |
| +cohort. |
| + |
| +**Rejected.** |
| +- Building Plan 5 now. The per-host census would have to accept legacy rows |
| + and files, and the legacy-row trust (G,J4) is unruled. Adoption also leaves |
| + `park-local` chores on sophie, which a claims-off box never discharges |
| + (`backfill.go:410-414`, `hostclaim/adopt.go:23-28`). |
| +- Keeping legacy `claimtrial` but out of scope. Its files refuse every new |
| + install on leo. |
| +- A raw bucket delete of the record. It goes around the door and leaves the |
| + domain claim behind. |
| + |
| +## 4. Smallest build that unblocks step 8 |
| + |
| +From `claim-step8-readiness`: |
| +- **Reused:** both signer phases (extended below); `Publish`'s owners from the |
| + census, and `VerifyAdopting`; `journal-migrate --window` and the mask class |
| + (a); the box classes (b) and (c) in `claimGlobalSharedPaths`; the R3 umask |
| + fixes; the S5 absent-journal fix; the witness and the leo snapshot loader. |
| +- **Moved to Plan 5:** reporting a legacy route row as a route installation |
| + (`legacyRouteRow`). Only the routes reader stays. |
| +- **Thrown away:** the wakecheck retained-unowned class (e), replaced by the |
| + move-aside; and the witness variant with sophie's stand-in report. |
| + |
| +Tasks: |
| +1. `hostops`: add `scope` and `outside` to `BackfillPlan`, and one exported |
| + in-scope predicate for boxes and records. |
| +2. `backfill-collect --scope … --empty`: freeze in-scope records only; refuse |
| + everywhere-class or replica cohort records, missing or cohort-pool |
| + `outside` entries, and in empty mode any in-scope record or claim. |
| +3. `census`/`Publish`/`VerifyAdopting`: in-scope roster boxes only. |
| +4. Migration census: in-scope records only, and no unowned registry row. |
| + Needs a census overlay ruling from platform-1e. |
| +5. `backfill-done` phase, as in section 2. |
| +6. Scoped `CheckCreateFreeze` at the two record call sites. |
| +7. `ReconcileClaims` skips out-of-scope records. |
| +8. Tests: |
| + - unit tests for each refusal, mutation-checked; |
| + - an unscoped-plan test proving today's behaviour unchanged; |
| + - the ruling 6 pin test extended to the new entry points; |
| + - one root-runner witness on leo's emptied tree: masks, `collect`, |
| + `journal-migrate --window`, `adopt`, `done`, claims-on root, then a |
| + production-constructor create and first install. |
| +9. Runbook delta: the new prechecks and attestation, the console-runtime |
| + window, the move-aside, 8.1b moved before the window, 8.2b and 8.2c, no |
| + 8.3, the acceptance create, and the new step 10 counts. |
| +10. Copy review for the new refusal strings. |
| + |
| +## 5. Deferred to production cohorts |
| + |
| +- Plan 5 populated backfill: installing adopted legacy hosts, the |
| + legacy-row trust ruling, the files seed over adopted hosts, and scoping |
| + Adoption's park chores to in-scope boxes. |
| +- Plan 15's independent writer, key rotation, and the rollback export (already |
| + deferred for the trial). |
| +- Box-signed placement. Each box's signed report carries its reconcile scope, |
| + and IAM exclusivity keeps non-roster boxes out of the deployment bucket. |
| + Together they replace the `outside` attestation. |
| +- Plan lifecycle across cohorts: replacing a `done` plan, per-app scoping of |
| + the effect-runner and move-grant freeze, and an abandon phase. |
| +- Deployment-wide lifecycle censuses. Delete, purge and app lifecycle require |
| + every namespace pool (`production_delete.go:23-42`, |
| + `production_held_start.go:54-75`), so with sophie claims-off a claimed host |
| + cannot be deleted. In the trial, `claimtrial2` lives until a teardown design |
| + exists (runbook open question 2). |
| +- #312, so the console process evidence lasts. |
| +- 4a: the named-unit console box, which brings sophie in. |
| + |
| +## 6. Ruling 6 |
| + |
| +Every change sits on a claim-mode path: |
| +- the signer phases exist only in `claims run`; |
| +- `journal-migrate` refuses unless `hostwrite.Enabled`; |
| +- `CheckCreateFreeze`'s record callers are the claim create door and the |
| + conditional record store, which only the console's claim runtime builds |
| + (`internal/console/claim_runtime.go:42`); |
| +- `ReconcileClaims` runs only with `HostClaim` (`hostclaimbackfill.go:40-48`). |
| + |
| +Nothing touches `provision`, the edge, status reports or edges files. The |
| +`outside` readings are SSM reads by the operator, not box code. A claims-off |
| +box (sophie, dennis, and leo until 8.2) and a no-census box run main's code |
| +unchanged. A plan without `scope` behaves exactly as today. |
| + |
| +## Questions for Paul |
| + |
| +1. The empty-cohort route: delete legacy `claimtrial` through a claims-off |
| + console window on sophie, move its leftovers and wakecheck's aside, skip |
| + the files seed, and accept on a new app `claimtrial2`. The seed's KEEP items |
| + then go unexercised in this trial. |
| +2. Is an operator-read placement, signed into the plan, enough proof for the |
| + trial that sophie and dennis cannot serve `cohort1`? Box-signed scope and |
| + IAM exclusivity would be owed before production. |
| +3. With sophie claims-off, the acceptance host cannot be deleted under |
| + claims. Is that acceptable for the trial? |
| +4. #312: run `console-report` right before the create (the recommendation), or |
| + fix #312 first? |
| +5. Noted while reading, not exercised: dev's claim-mode console has had no |
| + executor for pool-less creates since step 7. No roster box is in pool |
| + `default` (`production_create.go:123-152`). Does that affect platform-d8's |
| + work on dev? |