design-cohort-activation rev 2: Astra's review and the coordinator's rulings
Excluded-box inventory from the platform's own sources, bound into the plan; done after the claims-on agent's fresh runtime report, behind the barrier; release gate and canonical-serialization fixtures for scoped plans; empty-only behind an explicit mode; admission inventory, enrollment freeze and abandon; staged acceptance with durable baselines; #312 fixed in the build; pool-less create note and question.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+381
−287
docs/superpowers/specs/2026-10-19-cohort-activation-design.md+381 −287
CI cancelled — superseded by a newer push — run details
CI log
=== gate === 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.476s ? github.com/carlosframework/platform/cmd/changelog [no test files] ok github.com/carlosframework/platform/cmd/console 2.718s ok github.com/carlosframework/platform/cmd/moveprobe 93.223s ok github.com/carlosframework/platform/cmd/zddprobe 0.711s ok github.com/carlosframework/platform/internal/activator 250.876s ok github.com/carlosframework/platform/internal/agentsock 0.015s ok github.com/carlosframework/platform/internal/alerting 0.004s ok github.com/carlosframework/platform/internal/analytics 0.019s ok github.com/carlosframework/platform/internal/appconfig 2.462s ok github.com/carlosframework/platform/internal/apperr 0.074s ok github.com/carlosframework/platform/internal/applog 0.134s ok github.com/carlosframework/platform/internal/awsinv 0.004s ok github.com/carlosframework/platform/internal/boxchoice 0.006s ok github.com/carlosframework/platform/internal/boxid 0.689s ? 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.008s ok github.com/carlosframework/platform/internal/caps 0.017s ok github.com/carlosframework/platform/internal/certwarm 0.002s ok github.com/carlosframework/platform/internal/cgroup 0.707s ok github.com/carlosframework/platform/internal/changelog 0.313s ok github.com/carlosframework/platform/internal/claimactivation 0.775s ok github.com/carlosframework/platform/internal/clamd 0.054s ? github.com/carlosframework/platform/internal/cliflag [no test files] amadan: job cancelled — a newer push superseded it
diff --git a/docs/superpowers/specs/2026-10-19-cohort-activation-design.md b/docs/superpowers/specs/2026-10-19-cohort-activation-design.md| index df976e88d..520912340 100644 |
| --- a/docs/superpowers/specs/2026-10-19-cohort-activation-design.md |
| +++ b/docs/superpowers/specs/2026-10-19-cohort-activation-design.md |
| @@ -1,331 +1,425 @@ |
| # 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 |
| +Status: draft rev 2 for platform-1e and Paul, 2026-10-19. Revised after |
| +Astra's review (3 HIGH, 5 MEDIUM) and the coordinator's rulings. 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. |
| +code. This note scopes the backfill, adopt, `done` and the freeze to that |
| +cohort. The first implementation supports **empty cohorts only**. |
| ## 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: |
| +- A2. Dev's console claim runtime is switched OFF for the pause, on Paul's |
| + ruling (see "Pool-less creates" below). |
| +- A3. 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. |
| +- A4. A claims-off box ignores claims (`internal/hostwrite/write.go:33`, `:52`). |
| + It mints what its agent's startup config matches (`internal/host/agent.go:3202`; |
| + `inScope`/`poolServed`, `internal/provision/reconcile.go:401-414`, |
| + `:2155-2160`). It never removes a row outside its current scope |
| + (`reconcile.go:1518-1520`). So host.env alone does not say what a box serves; |
| + its running process and its retained rows do. |
| +- A5. `apps create`, `ship`, `promote` and store-mode `instances enable` work |
| + for a new app on a claim-mode console. Not exercised on dev. |
| ## 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: |
| +**Plan shape.** The backfill plan (`control/hosts-backfill.json`) gains three |
| +optional fields: |
| +- `mode`: absent, or `"empty-cohort"`; |
| +- `scope`: `{namespace, regions, pools}`, the fields of `hostactors.Scope` |
| + (`internal/hostactors/census.go:44-54`); |
| +- `outside`: the excluded-box inventory, below. |
| +A plan without `mode` keeps today's behaviour exactly. That includes the |
| +parked branch's legacy-row reporting if it lands |
| +(`232baff2:internal/host/rootmigration_census.go:236-260`). Every refusal |
| +below is gated on `mode == "empty-cohort"`. |
| + |
| +**One predicate** decides membership, so every consumer agrees: |
| - 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. |
| +- a **record** is in scope when its pool is a cohort pool and its home-resolved |
| + region (`production_create.go:222-225`) is a cohort region. |
| -Every consumer then reads only the in-scope part: |
| +In empty-cohort mode: |
| -| Consumer | Today | Scoped | |
| +| Consumer | Today | Empty-cohort mode | |
| |---|---|---| |
| -| `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. |
| +| `backfill-collect` (branch, `claimactivation/backfill.go:136-177`) | freezes every record | refuses any in-scope record, any claim under `control/hosts/`, and an incomplete or cohort-touching `outside` inventory; writes `hosts: {}` | |
| +| Migration census (`rootmigration_census.go:65-95`) | every record frozen by etag | no in-scope record may exist; out-of-scope records stay out of `s.Records`, so they cannot move the fingerprint (`rootmigration.go:82-92`); any registry row no claim owns refuses | |
| +| `census`/`Publish`/`VerifyAdopting` (`backfill.go:217-264`) | a report from every roster box | reports from in-scope boxes only; refuses a nonempty `hosts` | |
| +| `ReconcileClaims`, on the console and on leo (`backfill_controller.go:89-113`, `internal/host/claimbackfill.go:16-31`) | tries every unclaimed record | skips out-of-scope records | |
| +| `CheckCreateFreeze` (`create_freeze.go:12-28`) | refuses everything until `done` | section 2 | |
| + |
| +The cohort pool must be named. It is never `""`, which every unpooled |
| +claims-off box serves (hazard H1). |
| + |
| +**Excluded-box inventory (HIGH 1).** The single-writer property for a cohort |
| +host is this: the only boxes that serve it are claims-on boxes holding its |
| +claim. A3 covers claims-on boxes. Every other box with deployment credentials |
| +has to be shown to hold and run nothing in the cohort. |
| + |
| +*Which boxes.* Take the union of three platform sources: |
| +1. the roster's boxes; |
| +2. every box that writes under `status/boxes/`; |
| +3. every EC2 instance, running or stopped, whose instance profile is one of |
| + the deployment's box roles. For dev those are `carlos-dev-host`, |
| + `carlos-dev-host-b` and `carlos-dev-host-uk`, read with `describe-instances` |
| + filtered by instance profile. |
| + |
| +For dev that union is leo, sophie and dennis. `collect` refuses unless every |
| +member of the union is either a cohort box or has an `outside` entry, so |
| +dennis cannot be left out. |
| + |
| +*What each entry records.* The operator observes each excluded box read-only |
| +over SSM. The signer hashes the observations into the entry: |
| +- **Identity.** Instance ID, account, region and role ARN; the digest of its |
| + `control/boxes` record if one exists; the boot ID. |
| +- **Effective config.** The edge agent's MainPID and start time. The digest of |
| + its process environment (`/proc/<pid>/environ`), plus the values of |
| + `CARLOS_POOLS`, `CARLOS_REGION`, `CARLOS_HOST_CLAIM`, `CARLOS_FLEET` and |
| + `CARLOS_DEPLOYMENT_BUCKET`. The agent logs the same scope at start |
| + (`agent.go:3204`). |
| +- **Retained routes.** A digest of registry rows (host, pool, region, |
| + provisioned), with zero rows in a cohort pool. |
| +- **Installations.** Digests of `/data/.instances` and `/opt/carlos/live`, |
| + with nothing for a cohort host. |
| +- **Processes.** `carlos-instance*` units, none for a cohort host. |
| +- **Pending operations.** Stage, restart and update request markers, and move |
| + journals. |
| +- **`observed_at`.** |
| + |
| +`collect` refuses an entry whose effective pools contain a cohort pool, or |
| +whose retained state names a cohort host. |
| + |
| +*Stability.* From the first observation until acceptance ends, the excluded |
| +boxes are held: no host.env edit, no edge or agent restart, no reboot, no |
| +system update. platform-d8 is told, as with H2. Observation 2 comes before |
| +`done` (window step 9.3). Observation 3 comes after acceptance. Each must match |
| +observation 1 on boot ID, agent PID and start time, environment digest, and |
| +zero cohort state. A mismatch before `done` is stop and hold. |
| + |
| +*Sophie.* Its agent serves pool `""`. `poolServed` keeps that apart from |
| +`cohort1` in both directions. So its records are never frozen or adopted, and |
| +its report is never required. The create census still verifies its console |
| +compute binding (`census.go:171-174`, `:385-421`). |
| + |
| +*What this proves, plainly.* The inventory is an **accepted dev-trial |
| +assumption**, signed by the operator. It does not prove: |
| +- that no other credential holder exists; |
| +- that nothing changed between observations; |
| +- that bucket access is exclusive. |
| + |
| +Production needs box-signed evidence. Each box signs, with its census key and |
| +for the activation's reservation: |
| +- its running agent's effective scope and boot ID; |
| +- a retained-state census (routes, installations, processes, pending |
| + operations) showing no cohort state. |
| + |
| +On top of that, IAM exclusivity so only enrolled boxes hold deployment |
| +credentials, and the console refusing any claims-off box whose signed scope or |
| +retained state touches a cohort pool. That puts census code on claims-off |
| +boxes, which needs an explicit amendment to ruling 6. |
| **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). |
| +- A census key and the migration census on sophie: the named-unit preflight |
| + refuses it (`claimactivation_preflight.go:25-42`). |
| +- Removing sophie from the roster: the console's compute binding must name a |
| + roster box (`census.go:395-399`). |
| +- host.env or roster pools as the evidence: neither is what the running |
| + reconciler uses (A4). |
| + |
| +## 2. `done`, the freeze and recovery |
| + |
| +**Who writes `done`.** A new signer phase, `claims run backfill-done`. It |
| +writes If-Match on the adopting plan. The plan is root-signed, so neither a |
| +console nor a box can write it. |
| + |
| +**What it requires (empty-cohort mode):** |
| +- The plan is `adopting`, with the named id, at the current roster revision, |
| + and `hosts` is still empty. |
| +- No in-scope record and no cohort claim exists. |
| +- `VerifyAdopting` passes on the in-scope reports. |
| +- Every in-scope roster box has a **fresh verified runtime report**: |
| + - `HostClaimEnabled` is true; |
| + - its `generated_at` is after the claims-on agent started; |
| + - its boot ID matches; |
| + - it passes `VerifyReport` (`census.go:63-83`, 15-minute freshness, floor |
| + digest and guard ready). |
| + |
| + The flag comes from the running agent's environment |
| + (`internal/host/claimreport.go:38`). The migration report does not carry it |
| + (`backfill.go:37-50`). So `done` can only follow the agent's start. |
| +- Excluded-box observation 2 matches observation 1. |
| +- The admission inventory is unchanged (below). |
| + |
| +**What the freeze covers.** It applies while a plan exists that is not `done` |
| +(or `abandoned`): |
| +- in-scope record writes and creates refuse (`production_create.go:38`, |
| + `record_writes.go:90`); |
| +- the effect runner stays deployment-wide (`effect_runner.go:59`, reached from |
| + app mutations through `app_mutation_runner.go:108`), and so do the move |
| + grants; |
| +- **new app enrollments refuse.** `EnrollNewApp` checks the plan. Today an app |
| + create enrolls before its SQL mutation completes |
| + (`internal/console/claim_app_enroll.go:24`). |
| + |
| +So a stuck plan **does** block app effects and new apps across the |
| +deployment. To bound that, the trial runs on a **verified admission |
| +inventory**. The signer lists every `apps/*/*/admission.json` with its digest |
| +and binds the list into the plan at `collect`. `done` refuses if it changed. |
| +The list is expected to be empty, or a known set Paul accepts as frozen for |
| +the window. |
| + |
| +**For how long.** From 8.1b to 9.4 below: roughly T0+10 to T0+60 minutes, |
| +inside the window's deadline. |
| + |
| +**What an operator sees.** A frozen write gets the claim door's 409 and busy |
| +message with `Retry-After: 2` (`claim_response.go:23-24`, `:29-33`). The |
| +signer's phase summary names the plan state. |
| + |
| +**Recovery from an abandoned plan.** A new signer phase, `backfill-abandon`, |
| +writes If-Match to phase `abandoned`. It runs only in empty-cohort mode, with |
| +`hosts` empty and no cohort claim. The freeze then lifts as it does for |
| +`done`. Leo itself stays stop-and-hold if 8.2 had already begun. A later |
| +`collect` may replace an `abandoned` or `done` plan by If-Match, with a new id. |
| **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. |
| +- A deployment-wide freeze with no inventory: its reach is unknown. |
| +- No freeze: a cohort record could appear past the census. |
| +- The console writing `done`: it has no release key. |
| + |
| +## 3. Compatibility gate (HIGH 3) |
| + |
| +An older reader rejects a scoped plan for good. Root-envelope verification |
| +disallows unknown fields and re-marshals and compares the payload |
| +(`internal/release/rootenvelope.go:66-92`). `CheckCreateFreeze` then returns |
| +`ErrIdentity` before it reads the phase (`create_freeze.go:21-23`), and `done` |
| +cannot release that reader. |
| + |
| +So: |
| +- **Gate.** Before any scoped plan is published, every claim-mode plan |
| + consumer runs a release R that understands `mode`, `scope`, `outside`, |
| + `abandoned` and the #312 fix. The consumers are: |
| + - sophie's console (claim runtime); |
| + - leo's router: its agent backfill pass and its root migration census; |
| + - the signer's binary. |
| + |
| + `collect` enforces the gate. It refuses unless each consumer's verified |
| + report (`VerifyReport`) names a release at or above R. The runbook checks |
| + the same thing first. Claims-off boxes never read the plan |
| + (`claimbackfill.go:17-19`). |
| +- **Serialization.** The new fields are `omitempty`. An unscoped plan then |
| + marshals to exactly the old canonical bytes, and existing signed plans |
| + still verify. |
| + |
| +## 4. S4: an empty cohort box, and the window order |
| + |
| +**Why empty.** The binding scope freezes "historical-record conversion for |
| +records that cannot exist at first activation". The approved seed copy says |
| +"a proven new empty host needs no files seed" (runbook line 2397). So Plan 5 |
| +and the files seed drop out of the trial. |
| + |
| +**"Empty", checked by machine:** |
| +- E1. No in-scope record and no claim (`collect`). |
| - 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. |
| + completes (`rootmigration_census.go:146-215`). |
| +- E3. Leo's registry has no row. |
| - 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 |
| + certificate cache (b) and the box files (c), from the branch. There is no |
| + retained-unowned class. Otherwise the new host's first install would refuse, |
| + because the per-host census refuses unclassified files |
| + (`empty_census.go:233-241`) and excuses only claimed siblings |
| + (`rootinventory_sibling.go:22-85`). |
| + |
| +**Before the window** (claims off on leo; each item has its own go): |
| +- B1. While the console's claim runtime is off for the pause (A2), delete |
| + legacy `claimtrial` through the ordinary door. The claim-mode door cannot do |
| + it (`claim_retire.go:49`, `:63-71`). Leo's removal phase then retires the |
| + row. |
| +- B2. Move claimtrial's and wakecheck's leftovers aside on leo, with the |
| + delta's recipe applied to both hosts. Certificates stay. |
| +- B3. Roll release R. Check the gate. |
| +- B4. Console claim runtime on again, then `console-report`. This waits on |
| + question 6. |
| +- B5. Read-only checks: E1 and E3, P4, P6. Then excluded-box observation 1, |
| + and the admission inventory. |
| + |
| +**The window**, against the runbook's numbering. It needs one go for 8.1 to |
| +9.4. 9.5 and step 10 keep their own gos. |
| + |
| +| Step | What | Needs | |
| +|---|---|---| |
| +| 8.1 | barrier on, record, drain, mask (unchanged) | | |
| +| 8.1b | `backfill-collect`, empty-cohort mode (freeze starts) | roster, floor, gate, observation 1, admission inventory | |
| +| 8.2 | host.env, `install-control`, `journal-migrate --window`, mode checks, start instance-control; check E2 | collecting plan | |
| +| 8.2b | `backfill-adopt` with zero hosts | collecting plan, leo's migration report | |
| +| 8.3 | skipped: no eligible host | | |
| +| 8.4 | restore the six services and five triggers; the edge stays masked; no seed override or guard moves | | |
| +| 9.1 | restore the edge, start the claims-on agent and the triggers, **barrier kept**; step 9's checks | nothing from the plan | |
| +| 9.2 | wait for leo's fresh verified runtime report (section 2) | 9.1 | |
| +| 9.3 | excluded-box observation 2 | | |
| +| 9.4 | `backfill-done` (freeze ends) | adopting plan, 9.2, 9.3, admission inventory | |
| +| 9.5 | release the barrier after the serving check | 9.4 | |
| +| 10 | acceptance (below), then observation 3 | 9.5 | |
| + |
| +**No deadlock with zero hosts.** Each step needs only earlier steps: |
| +- `collect` needs nothing from the plan, and 8.2's census needs only the |
| + collecting plan. |
| +- `adopt` needs only leo's migration report. With zero hosts there are no |
| + owners to find. |
| +- The agent's start reads no plan state: the box's backfill pass acts only on |
| + `adopting` (`backfill_controller.go:55-66`), and with scoping it touches no |
| + record. |
| +- `done` needs 9.1 to 9.3, and acceptance needs `done`. |
| + |
| +**Acceptance (MEDIUM 7).** The legacy `claimtrial` app cannot get an |
| +admission (`backfill.go:166-175`; `enroll.go:94-99`). So acceptance runs on a |
| +new app, `claimtrial2`, enrolled by `createClaimAppRow` |
| +(`claim_app_enroll.go:16-43`): |
| +1. Create, ship wc4, promote, and `instances enable --pool cohort1`. |
| +2. `instances create`. It selects leo (`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 |
| + install takes the new-host branch (`empty_census.go:130-132`). Send the |
| + certificate hello only after the install is reported. |
| +3. **Launch.** A real launch. Record the observed `hits=` as baseline B0. |
| + Empty bootstrap publishes origin evidence only after the app's first |
| + transaction (`claimwakeassembly.go:677`, `:730`). |
| +4. **Confirmed stop.** The idle stop completes: "instance exited", plus root's |
| + stop evidence for that generation. |
| +5. **Cold generation 1.** A wake with a new `carlos-instance-gen@<host>:<n>`. |
| + The answer shows `hits=` at or above B0+1. That count is the durable |
| + sentinel: it survives only through the restore. Record it as B1. |
| +6. **Confirmed stop, then cold generation 2.** A generation number distinct |
| + from step 5's, and `hits=` at or above B1+1. |
| +7. **Evidence.** For each generation, the admitted origin and publication |
| + evidence name the claim's incarnation and that generation's own session. |
| + |
| +Exact `+1` steps are not asserted, because probe requests add rows (runbook |
| +lines 827 and 838). The isolation, reboot and signed-update checks of step 10 |
| +stay as written. The files seed's KEEP items (reboot race, retained parks) |
| +are explicitly not exercised. |
| + |
| +## 5. Smallest build |
| 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. |
| +- **Reused:** the two phases (extended); `Publish` and `VerifyAdopting`; |
| + `journal-migrate --window` and class (a); classes (b) and (c); the R3 umask |
| + fixes; the S5 absent-journal fix; the routes reader; the witness and the leo |
| + snapshot loader. |
| +- **Kept for unscoped plans only:** the legacy-row reporting. |
| +- **Thrown away:** the wakecheck retained-unowned class, 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. |
| +1. `hostops`: `mode`, `scope`, `outside`, the admission inventory and the |
| + `abandoned` phase, all `omitempty`; one in-scope predicate. |
| +2. `backfill-collect` in empty-cohort mode: the excluded-box inventory and its |
| + completeness check, the admission inventory, the release gate, and every |
| + refusal in section 1. |
| +3. `census`, `Publish` and `VerifyAdopting`: in-scope boxes only; nonempty |
| + `hosts` refused. |
| +4. Migration census: empty-cohort rules, gated on mode. Needs a census overlay |
| + ruling from platform-1e. |
| +5. The `backfill-done` and `backfill-abandon` phases. |
| +6. Freeze: scoped at the two record sites, `EnrollNewApp` refused while |
| + frozen, `abandoned` lifting it. |
| 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 |
| +8. **#312.** `console-report` writes the console process entry to its own |
| + object, which the agent never rewrites. `verifyCompute` and the floor |
| + check read it there, still matching the box report's boot ID and the |
| + process. Sophie's agent code is untouched (ruling 6). |
| +9. **Compatibility fixtures.** Existing signed unscoped plans, both |
| + `collecting` and `adopting`, verify byte for byte under the new type. A |
| + scoped plan fails verification under the old type, which is the reason for |
| + the gate. Unscoped plans behave unchanged, including legacy-row reporting. |
| +10. Tests: |
| + - every refusal, mutation-checked; |
| + - the ruling 6 pin test extended; |
| + - a root-runner witness on leo's emptied tree, from 8.1 through 9.4 with |
| + a real agent start; |
| + - a production-path witness of the create, launch, stop and two cold |
| + generations; |
| + - the #312 fix through the production create path, with the agent writing |
| + its report between refresh and create. |
| +11. Runbook delta (section 4) and copy review for the new strings. |
| + |
| +## 6. Deferred to production cohorts |
| + |
| +- **Populated cohorts.** Prerequisites: |
| + - Plan 5 install of adopted legacy hosts, and the legacy-row trust ruling; |
| + - complete valid ownership before `adopting` (the parked `Publish` allows |
| + zero owners, `232baff2:backfill_publish.go:72-95`); |
| + - park obligations scoped to in-scope boxes (`backfill.go:410-414`, |
| + `hostclaim/adopt.go:23-28`); |
| + - resumable partial adoption; |
| + - the files seed over adopted hosts. |
| +- Box-signed placement and retained-state exclusion, with IAM exclusivity |
| + (section 1). |
| +- Effect-runner and move-grant freeze scoped per app; plan lifecycle across |
| + cohorts. |
| +- Plan 15's writer, key rotation, and the rollback export. |
| +- Deployment-wide lifecycle censuses. Delete, purge and app lifecycle need |
| 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. |
| + `production_held_start.go:54-75`), so `claimtrial2` cannot be deleted while |
| + sophie is claims-off. |
| +- 4a, the named-unit console box. |
| -## 6. Ruling 6 |
| +## 7. Ruling 6 |
| Every change sits on a claim-mode path: |
| -- the signer phases exist only in `claims run`; |
| +- the signer phases are 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 |
| +- the freeze's callers are the claim doors 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. |
| +- the box's backfill pass is a no-op with claims off (`claimbackfill.go:17-19`); |
| +- the #312 fix is in `console-report` (a root claim tool) and the census |
| + reader. |
| + |
| +The excluded-box observations are operator reads, not box code. Claims-off |
| +and no-census boxes run main's code unchanged. An unscoped claim-mode plan |
| +behaves as today; the compatibility fixtures pin that, not just the call |
| +sites. |
| + |
| +## Pool-less creates on a claim-mode console |
| + |
| +This is real. A pool-less record selects roster pool `default` |
| +(`production_create.go:123-125`). Selection needs a box in exactly that pool |
| +(`:136-152`) and returns `ErrPending` when there is none (`:207-209`). Dev's |
| +roster has none, so a claim-mode dev console refuses every pool-less create. |
| +Sophie's legacy reconciler serving pool-less records does not qualify it. On |
| +Paul's ruling, dev's console claim mode is OFF for the pause. Turning it back |
| +on (B4) needs the roster to give pool-less records a home, or another |
| +decision. |
| ## 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? |
| +1. The empty-cohort route: |
| + - delete legacy `claimtrial` while the console is claims-off; |
| + - move its leftovers and wakecheck's aside; |
| + - skip the files seed; |
| + - accept on `claimtrial2`. |
| + |
| + The seed's KEEP items then go unexercised. |
| +2. Accept the excluded-box inventory as a signed operator assumption for the |
| + trial, with sophie and dennis held unchanged from observation 1 to the end |
| + of acceptance? Box-signed evidence and IAM exclusivity would be owed for |
| + production. |
| +3. One window go covering 8.1 to 9.4, with the barrier kept until `done`? |
| +4. If the admission inventory is not empty, may those apps' effects freeze for |
| + the window? |
| +5. `claimtrial2` cannot be deleted while sophie is claims-off. Is that |
| + acceptable for the trial? |
| +6. Before B4: how should pool-less records get a home on dev's roster (a |
| + `default`-pool box, sophie re-pooled, or another rule)? Or should the |
| + console stay claims-off and the trial wait? |