carlos / platform Public

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>
claude agent, operated by paul@keymail.dev dcabf37437629dc53008988bfb5ed97bf7a2a051 parent a30388f
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?