carlos / platform Public

design-cohort-activation rev 3: second review, surface-shrinking rulings

Full retained-state observation with refusal of unclassified state; EC2
inventory across every region, refusing stopped box-role instances;
per-consumer compatibility evidence; empty admission inventory only;
unscoped behaviour kept (legacy rows and retained-unowned) with behavioural
fixtures; #312 fixed by agent carry-forward inside the existing report;
abandon keeps the cohort freeze until a separate recover; 9.5 is the
edge/guard readiness check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 483c5c9a18089063190e0bedd0f4c8bce3de9b47 parent dcabf37
1 file changed, +321 −330
  • docs/superpowers/specs/2026-10-19-cohort-activation-design.md +321 −330

CI passed — 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	299.465s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.936s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.344s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.718s
ok  	github.com/carlosframework/platform/internal/activator	249.966s
ok  	github.com/carlosframework/platform/internal/agentsock	0.028s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.028s
ok  	github.com/carlosframework/platform/internal/appconfig	2.504s
ok  	github.com/carlosframework/platform/internal/apperr	0.104s
ok  	github.com/carlosframework/platform/internal/applog	0.150s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.005s
ok  	github.com/carlosframework/platform/internal/boxid	0.586s
?   	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.009s
ok  	github.com/carlosframework/platform/internal/caps	0.018s
ok  	github.com/carlosframework/platform/internal/certwarm	0.004s
ok  	github.com/carlosframework/platform/internal/cgroup	0.711s
ok  	github.com/carlosframework/platform/internal/changelog	0.327s
ok  	github.com/carlosframework/platform/internal/claimactivation	0.846s
ok  	github.com/carlosframework/platform/internal/clamd	0.061s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	402.390s
?   	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.811s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.013s
ok  	github.com/carlosframework/platform/internal/dbverify	0.005s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.004s
ok  	github.com/carlosframework/platform/internal/docsite	0.007s
ok  	github.com/carlosframework/platform/internal/domains	2.417s
ok  	github.com/carlosframework/platform/internal/doorkey	0.008s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	44.887s
?   	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.144s
ok  	github.com/carlosframework/platform/internal/gate	1.666s
ok  	github.com/carlosframework/platform/internal/genselect	0.016s
ok  	github.com/carlosframework/platform/internal/geo	0.011s
ok  	github.com/carlosframework/platform/internal/geoip	0.036s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.176s
ok  	github.com/carlosframework/platform/internal/host	179.317s
ok  	github.com/carlosframework/platform/internal/hostactors	5.883s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.004s
ok  	github.com/carlosframework/platform/internal/hostbox	2.665s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.592s
ok  	github.com/carlosframework/platform/internal/hostenv	0.004s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.006s
ok  	github.com/carlosframework/platform/internal/hostexec	4.781s
ok  	github.com/carlosframework/platform/internal/hostfence	0.028s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.361s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.013s
ok  	github.com/carlosframework/platform/internal/hostguard	1.052s
ok  	github.com/carlosframework/platform/internal/hostops	1.645s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.003s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.002s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.460s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.138s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.002s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	5.213s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.712s
ok  	github.com/carlosframework/platform/internal/instancefs	0.006s
ok  	github.com/carlosframework/platform/internal/instancekey	0.002s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.007s
ok  	github.com/carlosframework/platform/internal/layout	1.576s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.002s
ok  	github.com/carlosframework/platform/internal/lease	9.787s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.005s
ok  	github.com/carlosframework/platform/internal/ledger	0.019s
ok  	github.com/carlosframework/platform/internal/mail	0.169s
ok  	github.com/carlosframework/platform/internal/maintenance	0.078s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.486s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.072s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	20.916s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.767s
ok  	github.com/carlosframework/platform/internal/objstore	0.176s
ok  	github.com/carlosframework/platform/internal/ops	0.166s
ok  	github.com/carlosframework/platform/internal/park	0.196s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	16.944s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.011s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.390s
ok  	github.com/carlosframework/platform/internal/relay	7.226s
ok  	github.com/carlosframework/platform/internal/release	23.565s
ok  	github.com/carlosframework/platform/internal/rollout	3.642s
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.062s
ok  	github.com/carlosframework/platform/internal/rootstate	0.027s
ok  	github.com/carlosframework/platform/internal/runs	0.575s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.476s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	8.475s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.805s
ok  	github.com/carlosframework/platform/internal/status	0.060s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.156s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.005s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
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.001s
ok  	github.com/carlosframework/platform/internal/vet	1.540s
=== 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 23:09:12 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E011023719841E, 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.00s)
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
index 520912340..88ee704e6 100644
--- a/docs/superpowers/specs/2026-10-19-cohort-activation-design.md
+++ b/docs/superpowers/specs/2026-10-19-cohort-activation-design.md
@@ -1,425 +1,416 @@
# Cohort activation: one claims-on box inside a populated deployment
-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
+Status: draft rev 3 for platform-1e and Paul, 2026-10-20. Revised after
+Astra's second review and the coordinator's rulings. Where a finding allowed a
+choice, this revision takes the option that shrinks the surface. 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, adopt, `done` and the freeze to that
-cohort. The first implementation supports **empty cohorts only**.
+`cohort1`) while sophie (roster pool `console`, claims off) and dennis
+(outside the roster) keep running. 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. 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`).
+- A2. Dev's console claim runtime is OFF for the pause, on Paul's ruling (see
+ "Pool-less creates").
+- A3. A claims-on box acts only on hosts it holds a claim for
+ (`internal/hostbox/work.go:69-71`).
- 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.
+ `internal/provision/reconcile.go:401-414`, `:2155-2160`), and keeps rows
+ outside its current scope (`reconcile.go:1518-1520`). So only the running
+ process and the retained state say what a box serves.
- 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
-**Plan shape.** The backfill plan (`control/hosts-backfill.json`) gains three
-optional fields:
+**Plan shape.** The backfill plan (`control/hosts-backfill.json`) gains
+optional fields, all `omitempty`:
- `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"`.
+A plan without `mode` behaves exactly as today. If the parked branch lands,
+that includes its legacy-row reporting
+(`232baff2:internal/host/rootmigration_census.go:236-260`) and its
+retained-unowned classification
+(`232baff2:internal/host/claimcensus_migration_linux.go:174-201`). 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`);
+**One predicate** decides membership:
+- a **box** is in scope when roster namespace, region and pool all match, as
+ the create census requires (`census.go:99-101`);
- 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.
+The cohort pool must be named, never `""`.
+
In empty-cohort mode:
-| Consumer | Today | Empty-cohort mode |
-|---|---|---|
-| `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.
+| Consumer | Behaviour |
+|---|---|
+| `backfill-collect` (branch, `claimactivation/backfill.go:136-177`) | refuses any in-scope record, any claim under `control/hosts/`, any `admission.json`, a failed gate (section 3) or an inventory refusal (below); writes `hosts: {}` |
+| Migration census (`rootmigration_census.go:65-95`) | no in-scope record; out-of-scope records are left out of `s.Records` (`rootmigration.go:82-92`); any registry row and any retained-unowned path refuses |
+| `census`/`Publish`/`VerifyAdopting` (`backfill.go:217-264`) | in-scope boxes' reports only; nonempty `hosts` refuses |
+| `ReconcileClaims`, on the console and on leo (`backfill_controller.go:89-113`, `internal/host/claimbackfill.go:16-31`) | skips out-of-scope records |
+| Freeze | section 2 |
+
+**Excluded-box inventory.** The single-writer property for a cohort host is
+that only claims-on boxes holding its claim serve it. A3 covers claims-on
+boxes. Every other box with deployment credentials must be shown to run and
+retain nothing in the cohort.
+
+*Which instances.*
+1. List the account's enabled EC2 regions, then query every one of them for
+ instances 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`.
+2. A region that cannot be queried refuses.
+3. A box-role instance that is stopped, stopping or pending refuses.
+ Instances must be running and observed, or terminated.
+4. Every identity under `status/boxes/` and in the roster is reconciled
+ against that result by instance ID. A status object whose instance is
+ terminated is recorded as terminated. One whose instance is found nowhere
+ refuses.
+5. Every running instance must be a cohort box or have an `outside` entry.
+ For dev those are leo, sophie and dennis.
+
+*What is observed* on each running excluded box. Reads are read-only, over
+SSM, signer-hashed into its entry, each with `observed_at`:
+- **Identity:** instance ID, account, region, role ARN, boot ID, and its
+ `control/boxes` digest if any.
+- **Effective config:** the edge agent's MainPID, start time and environment
+ digest from `/proc/<pid>/environ`, with `CARLOS_POOLS`, `CARLOS_REGION`,
+ `CARLOS_HOST_CLAIM`, `CARLOS_FLEET` and `CARLOS_DEPLOYMENT_BUCKET` named.
+- **Processes and cgroups:** every process running as `carlos` and the cgroup
+ tree of every `carlos-*` unit. That includes the `carlos-exec@` children of
+ claims-off exec instances (`internal/activator/backend_exec.go:2218-2242`)
+ and `carlos-instance*` units.
+- **Instance trees:** both `/data/.instances` and `/data/instances`.
+- **Program history:** `/data/programs`.
+- **Live links:** `/opt/carlos/live` and `/opt/carlos/previous`.
+- **Custom units:** every `/etc/systemd/system` unit and drop-in that the
+ release does not ship.
+- **Pending work:** stage, restart and update request markers, restart marks,
+ and move journals.
+- **Registry:** every route row.
+
+**Classification.** Every observed item must be either box state the release
+ships, or belong to a current record whose pool is not a cohort pool.
+Anything else refuses activation: a cohort item, a leftover of a deleted
+record, an unknown unit or a stray process. There are no classification rules
+for orphaned leftovers. Cleaning one up is a separate step with its own go.
+
+*Stability.* The excluded boxes are held from observation 1 until acceptance
+ends: no host.env edit, no agent restart, no reboot, no update. platform-d8
+is told, as with H2. Observation 2 is at 9.3 and observation 3 after
+acceptance. Each must match observation 1 on boot ID, agent PID and start
+time, environment digest and classification.
+
+*Trial assumptions, stated plainly.* Two things stay operator assumptions:
+that the box roles are the only credential holders that run tenants, and that
+nothing changed between observations. Production needs:
+- box-signed placement and retained-state evidence per activation
+ reservation;
+- IAM exclusivity, so only enrolled boxes hold deployment credentials;
+- a console refusing any claims-off box whose signed scope or retained state
+ touches a cohort pool.
+
+That last item is census code on claims-off boxes, which needs a ruling 6
+amendment.
+
+*Sophie* serves pool `""`, and `poolServed` keeps that apart from `cohort1`.
+Its records are never frozen or adopted, and its census report is never
+required. The create census still verifies its console compute binding
+(`census.go:171-174`, `:385-421`; section 5, #312).
**Rejected.**
-- 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).
+- A census key on sophie: the named-unit preflight refuses it
+ (`claimactivation_preflight.go:25-42`).
+- Removing sophie from the roster: the compute binding needs a roster box
+ (`census.go:395-399`).
+- host.env or roster pools as evidence (A4).
+- Classifying orphaned leftovers instead of refusing them.
## 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`):
+**`backfill-done`** is a signer phase, If-Match on the adopting plan. It
+requires:
+- `hosts` still empty, and no in-scope record or cohort claim;
+- `VerifyAdopting` passing on the in-scope reports;
+- no `admission.json` anywhere;
+- excluded-box observation 2 matching observation 1;
+- a fresh verified runtime report from every in-scope box (`VerifyReport`,
+ `census.go:63-83`):
+ - `HostClaimEnabled`, which the running agent produces
+ (`internal/host/claimreport.go:38`) and the migration report lacks
+ (`backfill.go:37-50`);
+ - `generated_at` after the claims-on agent started;
+ - the current boot.
+
+**The freeze.** It applies while an empty-cohort plan is `collecting` or
+`adopting`:
- 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.
+- the effect runner and the move grants stay deployment-wide
+ (`effect_runner.go:59`);
+- `EnrollNewApp` refuses.
+
+The **admission inventory must be empty**: `collect` and `done` both refuse
+any `admission.json`. So no admitted app can be mutated during the window,
+and no admission digest can drift. The frozen-admission option is dropped.
+
+A frozen write gets the claim door's 409 and busy message
+(`claim_response.go:23-24`, `:29-33`). The freeze lasts from 8.1b to 9.4,
+about 50 minutes, inside the deadline.
+
+**Abandonment and recovery.** There are two signer transitions, both
+If-Match:
+- **`backfill-abandon`**, from `collecting` or `adopting`, to `abandoned`. It
+ releases only the deployment-wide parts: app effects, move grants and
+ enrollment. **The cohort create and record freeze stays.**
+- **`backfill-recover`**, from `abandoned`, to `recovered`. It lifts the
+ cohort freeze. It requires:
+ - leo stopped or masked;
+ - leo's claims-enabled report expired, past `VerifyReport`'s 15 minutes, or
+ superseded by a report without `HostClaimEnabled`;
+ - no cohort claim;
+ - a fresh excluded-box observation that passes classification.
+
+A later `collect` may replace a `done` or `recovered` plan by If-Match, with
+a new id. Leo itself stays stop-and-hold under the runbook.
**Rejected.**
-- 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
+- `abandoned` lifting everything. Leo's report stays valid for up to 15
+ minutes, so a cohort create could start during stop-and-hold.
+- A non-empty frozen admission set. A refused mutation still moves the
+ admission epoch (`app_mutation_runner.go:95-108`,
+ `control_writer.go:662-663`).
+
+## 3. Compatibility gate
+
+An older reader rejects a scoped plan permanently:
+- root-envelope verification disallows unknown fields and re-marshals the
+ payload (`internal/release/rootenvelope.go:66-92`);
+- `CheckCreateFreeze` then fails before it reads the phase
+ (`create_freeze.go:21-23`).
+
+So every plan consumer must run release R before any scoped plan exists. R
+understands the new fields and phases and carries the #312 fix. The new
+fields are `omitempty`, so unscoped plans keep their old canonical bytes.
+Claims-off boxes never read the plan (`claimbackfill.go:17-19`).
+
+| Consumer | Evidence | When |
+|---|---|---|
+| Sophie's console | A fresh console report (`status/consoles/<id>.json`) passing `VerifyReport`, release at or above R | At `collect` |
+| Leo's router (its agent backfill pass) | A fresh box report captured before 8.1's masking, release at or above R, digest bound into the plan. Valid for the offline interval: the masks hold `carlos-system-update` and the agent, so the router cannot change. Re-checked at 9.2 | Before 8.1, and at 9.2 |
+| Leo's root migration executable | The sha256 of `/opt/carlos/carlos` and `carlos.prev` equals the signed release identity's router sha256, as guard-arm checks (runbook blocker 6) | In 8.2, before `journal-migrate` |
+| The signer | The sha256 of its executing binary (`/proc/self/exe`) is in R's signed checksum list. It is a CLI, so binary identity is enough | Before every phase |
+
+## 4. S4: an empty cohort box, and the window
**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.
+and the files seed drop out.
**"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 (`rootmigration_census.go:146-215`).
+- E1. No in-scope record, no claim, no admission (`collect`).
+- E2. Leo's signed migration report lists zero installations
+ (`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), 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.
+- E4. Leo's roots hold only the window masks (a), the certificate cache (b)
+ and the box files (c). The per-host census refuses anything else at the new
+ host's first install (`empty_census.go:233-241`;
+ `rootinventory_sibling.go:22-85`).
+
+**Before the window** (leo claims-off; each item has its own go):
+- B1. While the console is claims-off (A2), delete legacy `claimtrial` through
+ the ordinary door. The claim-mode door cannot do it
+ (`claim_retire.go:49`, `:63-71`).
+- B2. Move claimtrial's and wakecheck's leftovers aside on leo. Clean up any
+ unclassified state found on the excluded boxes.
+- B3. Roll R. Then check the console row of the gate table.
- 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.
+ question 5.
+- B5. Read-only checks: E1, E3, P4, P6, the EC2 inventory and observation 1.
-**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.
+**The window.** One go covers 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.1 | barrier on; capture leo's pre-mask report; record, drain, mask | |
+| 8.1b | `backfill-collect`, empty-cohort mode (freeze starts) | gate, B5 |
+| 8.2 | host.env, `install-control`, executable check, `journal-migrate --window`, mode checks, instance-control; check E2 | collecting plan |
+| 8.2b | `backfill-adopt`, zero hosts | 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 |
+| 8.4 | restore the six services and five triggers; the edge stays masked | |
+| 9.1 | restore the edge, start the claims-on agent and triggers, barrier kept | nothing from the plan |
+| 9.2 | wait for leo's fresh verified runtime report | 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.
+| 9.4 | `backfill-done` (freeze ends) | 9.2, 9.3 |
+| 9.5 | the edge and guard readiness checks of step 9, then release the barrier. No app response is checked: no tenant exists yet | 9.4 |
+| 10 | acceptance, then observation 3 | 9.5 |
+
+There is no deadlock with zero hosts:
+- `adopt` needs only leo's migration report.
- 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
+ `adopting` (`backfill_controller.go:55-66`) and skips every out-of-scope
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 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.
+- `done` needs 9.1 to 9.3.
+
+**Acceptance** runs on a new app, `claimtrial2`. The legacy `claimtrial` app
+cannot get an admission (`backfill.go:166-175`; `enroll.go:94-99`), while a new
+app enrolls through `createClaimAppRow` (`claim_app_enroll.go:16-43`).
+1. Create, ship wc4, promote, then `instances enable --pool cohort1`.
+2. `instances create` selects leo (`production_create.go:136-152`). It starts
+ a create with one participant and no park obligations
+ (`hostclaim/create_selection.go:8-23`). The first install takes the
+ new-host branch (`empty_census.go:130-132`). Send the certificate hello only
+ after the install is reported.
+3. A real launch. Record `hits=` as baseline B0. Empty bootstrap publishes
+ origin evidence only after the first transaction
+ (`claimwakeassembly.go:677`, `:730`).
+4. A confirmed stop: "instance exited", plus root's stop evidence.
+5. Cold generation 1: a new `carlos-instance-gen@<host>:<n>`, with `hits=` at
+ or above B0+1. That count is the durable sentinel. Record it as B1.
+6. A confirmed stop, then cold generation 2: a distinct generation number,
+ with `hits=` at or above B1+1.
+7. For each generation, the admitted origin and publication evidence name the
+ claim's incarnation and that generation's session.
+
+The isolation, reboot and signed-update checks of step 10 stay as written.
+The seed's KEEP items are not exercised.
## 5. Smallest build
From `claim-step8-readiness`:
- **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.
+ `--window` and class (a); classes (b) and (c); the R3 umask fixes; the S5
+ absent-journal fix; the routes reader; the witness and the snapshot loader.
+- **Kept for unscoped plans only:** legacy-row reporting and the
+ retained-unowned class.
+- **Thrown away:** only the witness variant with sophie's stand-in report.
Tasks:
-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.
+1. `hostops`: `mode`, `scope`, `outside`, and the `abandoned` and
+ `recovered` phases (`omitempty`); one in-scope predicate.
+2. `backfill-collect` in empty-cohort mode: the inventory checks of section 1,
+ the empty admission check, and the gate (section 3).
+3. In-scope reports only in `census`, `Publish` and `VerifyAdopting`; nonempty
+ `hosts` refuses.
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.
+5. Three phases: `backfill-done`, `backfill-abandon` and `backfill-recover`.
+6. Freeze, in empty-cohort mode only:
+ - scoped at the two record sites;
+ - `EnrollNewApp` refused;
+ - `abandoned` releases app effects only.
7. `ReconcileClaims` skips out-of-scope records.
-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.
+8. The signer's self-check of its executing binary.
+9. **#312, inside the existing authority.** When the agent writes its box
+ report (`statusTick`, `agent.go:3762-3785`; `status.WriteBox`,
+ `internal/status/snapshot.go:1111`), it reads its current report and
+ carries forward each `console_processes` entry
+ (`internal/status/console_process.go:5-12`) that is still current:
+ - The entry's process ID names the current boot. Its format is
+ `boot:pidns:pid:start` (`activation_console_report_linux.go:26-40`).
+ - `/proc/<pid>/stat` shows the same start time.
+
+ Both are visible to the agent: the console and the agent both run as
+ `carlos`, and the edge unit's `ProtectProc=invisible` hides only other
+ users' processes (`internal/host/units/carlos-edge.service:28`, `:50`). No
+ root-only observation is needed. Verification already rejects a stale
+ entry, because it must equal the console's fresh self-reported process ID
+ (`census.go:409-416`; `internal/console/hostreports.go:66`).
+
+ `console-report` stays the only producer of new entries. The writer and its
+ trust are unchanged.
+10. Fixtures and tests:
+ - **Signed bytes:** existing signed unscoped `collecting` and `adopting`
+ plans verify byte for byte. A scoped plan fails under the old type.
+ - **Unscoped behaviour unchanged:** legacy-row reporting, the
+ retained-unowned class, and app enrollment during an unscoped
+ `collecting` or `adopting` plan.
+ - **Refusals:** every refusal, mutation-checked.
+ - **Abandonment:** a cohort create racing straight after `abandoned`
+ refuses.
+ - **#312:** the agent's report written between `console-report` and the
+ create, through the production create path. A dead or restarted console
+ process is not carried forward.
+ - **Root runner:** a witness from 8.1 through 9.4 with a real agent start.
+ - **Production path:** create, launch, stop and two cold generations.
+ - **Ruling 6:** the pin test extended.
+11. Runbook delta, 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`);
+ - Plan 5 install of adopted hosts, and the legacy-row trust ruling;
+ - complete ownership before `adopting` (`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.
+- Box-signed placement and retained-state exclusion, and IAM exclusivity.
+- A freeze scoped per app; non-empty admission inventories.
- 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 `claimtrial2` cannot be deleted while
+- Deployment-wide lifecycle censuses (`production_delete.go:23-42`,
+ `production_held_start.go:54-75`). `claimtrial2` cannot be deleted while
sophie is claims-off.
- 4a, the named-unit console box.
## 7. Ruling 6
-Every change sits on a claim-mode path:
+The claim work sits on claim-mode paths:
- the signer phases are in `claims run`;
- `journal-migrate` refuses unless `hostwrite.Enabled`;
-- 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`);
-- 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.
+- the freeze runs in claim doors and in the conditional record store, which
+ only the console's claim runtime builds (`internal/console/claim_runtime.go:42`);
+- the box backfill pass is a no-op with claims off (`claimbackfill.go:17-19`).
+
+Unscoped plans keep today's behaviour, and behavioural fixtures pin it.
+
+**One narrow exception: the #312 carry-forward runs in every agent.** A box
+whose report has no console entry writes exactly what main writes. Only a
+console box, after `console-report`, keeps that entry across ticks. It needs
+the ruling's explicit acceptance.
+
+The excluded-box observations are operator reads.
## 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.
+(`production_create.go:123-125`), and selection returns `ErrPending` without a
+box in that pool (`:136-152`, `:207-209`). Dev's roster has none, so a
+claim-mode dev console refuses every pool-less create. 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` while the console is claims-off;
- - move its leftovers and wakecheck's aside;
+ - move its leftovers and wakecheck's aside, plus any unclassified state on
+ the excluded boxes;
- 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
+2. Accept the excluded-box inventory's completeness and stability as trial
+ assumptions, with sophie and dennis held from observation 1 to the end of
+ acceptance?
+3. One window go covering 8.1 to 9.4?
+4. Accept the #312 carry-forward as a narrow ruling 6 exception?
+5. Before B4: how do pool-less records get a home on dev's roster (a
+ `default`-pool box, sophie re-pooled, or another rule)? Or does the
console stay claims-off and the trial wait?
+6. `claimtrial2` cannot be deleted while sophie is claims-off. Is that
+ acceptable for the trial?