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>
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? |