carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1# Census proposal: claim-step8-readiness
2
3Status: RULED (platform-1e, 2026-10-20): accepted as proposed. Overlay `internal/provision/testdata/plan14-root-reads-claim-step8-readiness.tsv`, loaded last (after `claim-guard-umask`), every row `rev3=census-ruling-claim-step8-readiness`. The 9B8 addendum is `internal/provision/claim_step8_readiness_census_test.go`. No accepted-legacy row is added, so the seal stands.
4
5## What changed
6
7Dev cohort-1 step 8 readiness (Paul's approval of 2026-10-19; controller rulings R1 to R5):
8
9- **R1 (S1).** `carlos system claims run` gains `backfill-collect` and `backfill-adopt` on the trusted signer. Collect writes the signed collecting plan at `control/hosts-backfill.json`, create-only, freezing every instance record the bucket lists (`hostops.Apps`, `hostops.Records`, the census's own listing). Adopt advances it to adopting under If-Match through the existing `hostops.Backfill.Publish`, which now takes each host's box and region from the one box whose signed census report names it, then reads the plan back through `Backfill.VerifyAdopting`, the adopters' own acceptance. The migration census reports an ordinary legacy route row as the box's route installation of its frozen record, so adoption can name this box.
10- **R2 (S2).** The migration census classifies leo's real tree: the window's `/dev/null` masks (only with `journal-migrate --window`, only for the twelve window units, only with each original held), the edge's certificate cache and the box's own files (in the classes every claim census shares), a reported route installation's instance directories, program file and live link, and a frozen record's retained leftovers with no row and no claim. A refusal names the path.
11- **R3.** journal-migrate's public floor directories and install-control's retry directory are made with `mkdirAllExact` (#317), so a umask 077 shell leaves them 0755.
12- **Found by the witness.** A box with no legacy journal directory (leo) failed the import: the copy named each missing file while the identity said `absent`. `MigrateLegacy` now treats both as nothing to import.
13
14Ruling 6: every new or widened entry is reached only from claim-mode code (`TestStep8CensusChangesReachOnlyClaimMode` pins the callers): journal-migrate refuses without `CARLOS_HOST_CLAIM`, the root migration runs only inside it, the shared census classes are read only by the claim assembly, root's claim inventory startup and the migration, and the retry directory is prepared only with a census or claim units on. The backfill phases run only from `claims run` on the signer.
15
16## Units (root-read census)
17
18| Unit | Change | Class | Executing identity |
19|---|---|---|---|
20| `internal/claimactivation/runner.go` `Runner.Run` | two phases and their one allowed key | authority F,G,J4 (as ruled for claim-step6-artifacts) | `carlos system claims run`, explicit operator CLI, uid 0 on the trusted signing host |
21| `internal/claimactivation/backfill.go` `backfillReady` | new | authority F,G,J4 | the same signer CLI. Reads the published floor; refusal only |
22| `internal/host/claimjournal_migration.go` `RunSystemJournalMigrate` | `--window`, checked before any write; declaration digest only | authority G,J4 (as ruled in p3) | `carlos system journal-migrate`, offline operator CLI, uid 0 |
23| `internal/host/rootmigration_census.go` `rootMigrationCollector.collect` | reports legacy route rows | authority G,J4 (as ruled in p3) | the same, uid 0 |
24| `internal/host/rootmigration_census.go` `legacyRouteRow` | new | authority G,J4 | the same, uid 0. Reads an agent-written registry row while the agent is stopped and masked. A match only nominates this box for its frozen record's adoption, which the adopter checks again against the signed census digests and the committed domain claim |
25| `internal/host/rootmigration_production_linux.go` `migrateRootClaimAuthority` | wiring: routes, the migration classes, the floor directories | authority G,J4,E3 (as ruled in p3) | offline root migration, uid 0 |
26| `internal/host/claimcensus_migration_linux.go` `claimMigrationSharedPaths` | new | authority G,J4 | offline root migration, uid 0. Refusal-only classification |
27| `internal/host/claimcensus_migration_linux.go` `verifyMigrationWindow`, `rootPrivateEntry` | new | authority G,J4 | `carlos system journal-migrate`, uid 0. Lstat and Readlink of the window and the window units; refusal only |
28| `internal/host/rootupdates_runtime.go` `prepareRootRetryUnitDirectory` | `mkdirAllExact` before the public open | authority E3 (as ruled in p3) | root claim and adoption controllers, and `install-control`, uid 0 |
29| `internal/hostbox/migration_census_linux.go` `CheckMigrationRoots` | a refusal names its root and path | authority G,J4 (as ruled in p3) | offline root migration, uid 0 |
30| `internal/hostbox/journalmigration_linux.go` `JournalService.MigrateLegacy` | an absent legacy directory imports nothing | authority C,J2,J4 (as ruled, root-input) | offline root migration, uid 0 |
31
32Envelopes re-pinned: `internal/claimactivation`, `internal/host`, `internal/hostbox`, `internal/hostops`. New helpers with no read site of their own (`claimBoxDataCensus`, `hostInstallationPaths`, `prepareRootFloorDirs`, `verifyJournalMigrateWindow`, `listBackfillHosts`, `readSignedPlan`, `backfillCollect`, `backfillAdopt`, `Backfill.VerifyAdopting`) are envelope only.
33
34## 9B8
35
36| Unit | Class | Why |
37|---|---|---|
38| `internal/hostops/backfill_publish.go` `Backfill.Publish` | nonrecord (re-reviewed) | its one Put, of the backfill manifest under its etag, is unchanged; it now copies box and region from the census before it |
39| `internal/claimactivation/backfill.go` `Runner.backfillCollect` | nonrecord (new) | its one Put is the create-only manifest through the runner's checkedStore, which admits only `control/hosts-backfill.json` for the phase; it reads records and writes none |
40| `internal/host/claimcensus_migration_linux.go` `hostInstallationPaths` | key-site (new) | the `instances` literal names a host's nested data directory for the census; no record key |
41
42## Rows carried over
43
44Every re-pinned row keeps its class and task exactly; only the bases add what changed.
45
46## backfill-done
47
48Status: RULED (platform-1e, 2026-10-20): accepted as proposed, in the same overlay and 9B8 addendum (no new file, no new seal). Claim trial Option B (Paul's D1 of 2026-10-20): leo stays empty, the dev console stays claims-off through the window, and a new signer phase goes straight from `collecting` to `done`. No adoption, no abandon or recover phase, no plan fields, no #312 code, no inventory code. `backfill-adopt` stays in the binary unchanged.
49
50- `carlos system claims run` gains `backfill-done` (`Runner.backfillDone`, mirroring `backfillAdopt`, dry run included). It refuses up front through `backfillReady` and the plan check, then calls the new `hostops.Backfill.Done` under the plan's etag.
51- `Backfill.Done` refuses, each condition separately: the etag is not `collectingETag`; the envelope does not verify; the phase is not `collecting`; the live record listing (`Apps`, `Records`) is not the plan's hosts by key, etag and acquisition; `control/hosts/` lists anything; any listed app has `admission.json`; any roster box with a census key lacks a report for the reservation that verifies as the census verifies it, captured no earlier than the plan, listing zero installations. Boxes with no census key are skipped and returned as unreported. With no keyed report at all it refuses. It then fills the reporting boxes' digests, writes `done` under If-Match, reads it back byte for byte, and requires `CheckCreateFreeze` to return nil.
52- `ProductionAuthority.census` is accepted-legacy and is not touched. Done uses `verifyCensusReport`, a line-for-line copy of census's per-box check; `TestDoneVerifiesReportsAsTheCensusDoes` holds the two together over 18 report variants.
53- A zero-host plan (the trial deletes every dev record before the window) is `"hosts":{}` and passes collect, the migration census, journal-migrate `--window` and done.
54
55Ruling 6: the phase is reached only from `claims run` on the trusted signer. `TestStep8CensusChangesReachOnlyClaimMode` now also scans the module's non-test sources and pins `Backfill.Done` (a three-argument `Done` call) to `Runner.backfillDone`, `backfillDone` to `Runner.Run`, and `verifyCensusReport` to `Backfill.Done`. No box, console or agent path calls them, and a claims-off or no-census box sees no change.
56
57| Unit | Change | Class | Executing identity |
58|---|---|---|---|
59| `internal/claimactivation/runner.go` `Runner.Run` | re-pinned: the `backfill-done` case and its one allowed key, `control/hosts-backfill.json` | authority F,G,J4 (unchanged) | `carlos system claims run`, explicit operator CLI, uid 0 on the trusted signing host |
60| `internal/hostops/backfill_publish.go` `Backfill.Done` | new | authority F,G,J4 | the same signer CLI, its only caller. Reads the plan, roster, record listing, `control/hosts/`, admissions and keyed census reports; one If-Match write of the plan as done; adopts and installs nothing |
61
62Envelopes re-pinned: `internal/claimactivation`, `internal/hostops`. New helpers with no read site of their own (`Runner.backfillDone`, `verifyCensusReport`) are envelope only. `internal/host` changes are tests only.
63
649B8: `internal/hostops/backfill_publish.go` `Backfill.Done`, nonrecord (new). Its one Put is the backfill manifest rewritten as done under the collecting etag, through the runner's checkedStore; it reads records, claims, admissions and census reports and writes none of them.
65
66## Fix round 1
67
68Status: RULED (platform-1e, 2026-10-20): accepted as proposed. Astra's review of `claim-empty-cohort-done` blocked the merge on two findings, and platform-1e ruled the fixes. The re-pins are in a new overlay, `internal/provision/testdata/plan14-root-reads-claim-empty-cohort-fix1.tsv`, loaded last and marked `rev3=PROPOSED-claim-empty-cohort-fix1`. The 9B8 re-pin is in `internal/provision/claim_empty_cohort_fix1_census_test.go`. No accepted-legacy row is added, so the seal stands.
69
70- **F1: `Backfill.Done` could finish with a record it never listed.** Done now lists the records a second time, immediately before its If-Match write, after every report has been fetched and verified. If that listing is not `p.Hosts`, it refuses with `ErrDoneRecords` and writes nothing. After the write and the readback it lists them a third time. A change there returns `ErrDoneRecordsChanged`, which wraps `ErrDoneRecords`, so `claims run` puts the existing records refusal in front of it. No new human-facing string. All three listings use the new `doneRecords` (`Apps`, then `Records` per app, the comparison collect uses). The If-Match covers only the plan, so this narrows the window but cannot close it. The trial closes it by stopping the dev console (`console.service`, owned by platform-d8) from 8.1b to 8.2b, which stops its record writes.
71- **F2: the umask repair could not be reached on a rerun.** `rootMigration.run` now calls a required `prepareFloors` hook under its lock and quiesce, before `guardReady`. That is before the first floor read. Production wires the hook to `prepareRootFloorDirs`. A missing directory is made with `mkdirAllExact`. An existing `guard/moves` or `guard/seeds` is repaired to 0755 only when it is a real directory owned by root:root, reached from `/` with `O_NOFOLLOW` at every component. A symlink, a file or another owner refuses and is left as it was. `ready` still calls it before opening the floors.
72
73Ruling 6: `prepareRootFloorDirs` is still called only from `migrateRootClaimAuthority`. `TestStep8CensusChangesReachOnlyClaimMode` also pins `prepareRootFloorDirsOwned` to `prepareRootFloorDirs` and `repairRootFloorDir` to `prepareRootFloorDirsOwned`. `Backfill.Done`'s callers are unchanged. A claims-off or no-census box sees no change.
74
75| Unit | Change | Class | Executing identity |
76|---|---|---|---|
77| `internal/hostops/backfill_publish.go` `Backfill.Done` | re-pinned: the second and third record listings | authority F,G,J4 (unchanged) | `carlos system claims run backfill-done`, explicit operator CLI, uid 0 on the trusted signing host |
78| `internal/host/rootmigration.go` `rootMigration.run` | re-pinned: the required `prepareFloors` hook, before `guardReady` | authority G,J4 (unchanged) | `carlos system journal-migrate`, offline uid 0 |
79| `internal/host/rootmigration_production_linux.go` `migrateRootClaimAuthority` | re-pinned: `prepareFloors` wired to `prepareRootFloorDirs` | authority G,J4,E3 (unchanged) | offline root migration, uid 0 |
80| `internal/host/rootmigration_production_linux.go` `repairRootFloorDir` | new: Lstat, then an `O_NOFOLLOW` walk from `/`, Fstat, and Fchmod to 0755 only for a root:root directory | authority G,J4 | `carlos system journal-migrate`, offline uid 0 |
81
82Envelopes re-pinned: `internal/host`, `internal/hostops`. The new `doneRecords` and `prepareRootFloorDirsOwned` have no read site of their own, so they are envelope only.
83
849B8: `internal/hostops/backfill_publish.go` `Backfill.Done`, still nonrecord (re-reviewed). The new listings only read. Its one Put, of the backfill manifest under the collecting etag, is unchanged.
85
86## Fix round 2
87
88Status: RULED (platform-1e, 2026-10-20): accepted as proposed. Astra's re-review of `claim-empty-cohort-done` found F1 resolved. It found one new MEDIUM in F2's repair: `repairRootFloorDir` made a missing floor directory with the pathname `mkdirAllExact`, which follows an ancestor symlink, before its `O_NOFOLLOW` walk, and that walk checked the owner only of the final directory. The controller ruled a descriptor walk shared with `consoleDir`. The re-pins are in a new overlay, `internal/provision/testdata/plan14-root-reads-claim-empty-cohort-fix2.tsv`, loaded last and marked `rev3=PROPOSED-claim-empty-cohort-fix2`. No accepted-legacy row is added, so the seal stands.
89
90- **The shared walk.** `walkNoFollowDir` (new, `internal/host/nofollowdir_linux.go`) is `consoleDir`'s walk moved out unchanged. It opens the caller's base, `/` in production, then each name beneath it with `O_NOFOLLOW|O_DIRECTORY`. The caller's check sees every directory before anything beneath it is opened or made. A missing name is made, only where the caller allows it, by `Mkdirat` then `Fchmodat` to 0755 beneath the checked parent, and the parent is fsynced. The name is then opened again with `O_NOFOLLOW`.
91- **`consoleDir`** keeps its own check (root-owned, not group or other writable, other-searchable; the last exactly 0755 root:root). It allows creation at every component when asked, as before, so its behaviour is the same. Its row loses the open and Openat sites to `walkNoFollowDir` and keeps its class.
92- **`repairRootFloorDir`** has no pathname `Lstat` or `mkdirAllExact` now. It walks from `/` with `walkNoFollowDir`, and every directory from `/` to `guard/` must be a real directory owned by root and not writable by group or others before anything is made. Only then is a missing `guard/moves` or `guard/seeds` made by `Mkdirat` beneath the checked `guard/`. It is opened with `O_NOFOLLOW`, must be a directory owned by root:root, and is Fchmodded to 0755. A symlink or a missing directory on the way, an untrusted ancestor, or a floor directory that is a symlink, a file or another owner's refuses with nothing made or changed. `mkdirAllExact` stays for guard-arm's own directories.
93
94Ruling 6: `prepareRootFloorDirs` is still called only from `migrateRootClaimAuthority`. `TestStep8CensusChangesReachOnlyClaimMode` also pins `walkNoFollowDir` to `consoleDir` and `repairRootFloorDir`. A claims-off or no-census box sees no change.
95
96| Unit | Change | Class | Executing identity |
97|---|---|---|---|
98| `internal/host/activation_artifacts_linux.go` `consoleDir` | re-pinned: its walk moved unchanged into `walkNoFollowDir`; its checks and its Fstat stay | authority G,J4 (unchanged) | `claims install-console`, explicit box installer, uid 0 |
99| `internal/host/nofollowdir_linux.go` `walkNoFollowDir` | new: `consoleDir`'s walk, shared | authority G,J4 (its callers' class) | uid 0 callers only: `consoleDir` and `repairRootFloorDir` |
100| `internal/host/rootmigration_production_linux.go` `repairRootFloorDir` | re-pinned: every ancestor checked through `walkNoFollowDir` before `Mkdirat` of the leaf, then `O_NOFOLLOW`, Fstat, Fchmod; no `Lstat`, no `mkdirAllExact` | authority G,J4 (unchanged) | `carlos system journal-migrate`, offline uid 0 |
101
102Envelope re-pinned: `internal/host`. `prepareRootFloorDirsOwned` gains the walk's base as a parameter and has no read site of its own.
103
1049B8: no record write changes.
105
106## Harness census
107
108Status: RULED (platform-d8, 2026-10-20): accepted as proposed (H1 to H4); Astra approved 8a313548..5efe8763 with no findings. platform-1e had left; platform-d8 ruled. platform-d8's deployment harness stopped 8.2 twice on 2026-10-20, each time on box state with no installation behind it. It would stop the same way on dev.
109
110- `opt` entry `pointer-digests/bab/claimtrial/edge.digests`: root's #253 ratchet (`release.RootRatchetDir`).
111- `units` entry `.claims-activation.lock`: install-control's lock (`activationLock`). Step 8.2 runs install-control before journal-migrate, so this lock is always present.
112
113The edge's ratchet under the data root (`release.EdgeRatchetDir`) was the stop before both. The trial ruled it a B2 move-aside (#324).
114
115The fix classifies all three as box state the census keeps. The re-pins are in a new overlay, `internal/provision/testdata/plan14-root-reads-claim-harness-census.tsv`, loaded last and marked `rev3=PROPOSED-claim-harness-census`. It adds no accepted-legacy row, so the seal stands.
116
117- **Ratchets.** Both directories are shared trees: directories and regular files only, so a symlink anywhere in either refuses.
118 - A mark is one file per channel (`<acct>/<app>/<channel>.digests`). Marks are never cleared and never adopted.
119 - Root and the agent each read both trees. Moving either tree aside would weaken #253 for every channel it holds, and moving both would reset it. Keeping them is the only safe answer for a populated box, and this closes #324's code half.
120- **Activation lock.** It is a shared regular file for that exact name in the unit directory root only. The same name in a drop-in directory still refuses.
121 - The lock stays where it is. Unlinking a flocked file would let a second installer lock a fresh inode beside the first, and moving it would let an older binary lock the old place.
122 - `activationLock` now names it through the constant `activationLockName`. The value is unchanged.
123
124Ruling 6: `claimGlobalSharedPaths` is reached only from the claim assembly, root's empty-installation census and the migration census. A claims-off or no-census box sees no change.
125
126| Unit | Change | Class | Executing identity |
127|---|---|---|---|
128| `internal/host/activation_install_linux.go` `activationLock` | re-pinned: the lock name is the constant | authority G,J4 (unchanged) | explicit operator CLI, uid 0 |
129| `internal/host/claimcensus.go` `claimUnitCensus` | re-pinned: adds `CensusFile(".claims-activation.lock")` | authority C,G,J4 (unchanged) | root installation admission controller, uid 0 |
130
131`claimOptCensus` and `claimBoxDataCensus` have no read site of their own (envelope only). Envelope re-pinned: `internal/host`.
132
1339B8: no record write changes.
134
135Tests: `TestMigrationCensusKeepsRatchetsAndActivationLock` reproduces each harness refusal verbatim when its class is removed (mutation-checked one class at a time). `TestMigrationCensusRefusesOutsideLeosClasses` gains four cases:
136
137- a symlink in root's ratchet;
138- a symlink in the edge's ratchet;
139- the lock replaced by a link;
140- the lock inside a drop-in directory.
141
142## Own child
143
144Status: RULED (platform-d8, 2026-10-20): accepted as proposed (O1 to O4). Astra approved 5efe8763..e2c006d2 with no findings. The harness's fourth 8.2 stop was `/proc/<pid>/task/<pid>: service: permission denied`. It comes from the retired-generation kernel census, which journal-migrate runs after the migration census. The census refused journal-migrate's own service-user registry child: uid 989, with journal-migrate as its parent. The root-task rule accepts a non-root task only inside a service whose ExecStart selects the running binary, and from an SSM or ssh root shell the child is in none. The quiesce scan had the same defect (G7, fixed at 465c3a08).
145
146The re-pins are in a new overlay, `internal/provision/testdata/plan14-root-reads-claim-own-child.tsv`, loaded last and marked `rev3=PROPOSED-claim-own-child`. It adds no accepted-legacy row, so the seal stands.
147
148- **The rule.** `generationKernel` gains `parent`. When it is set, a non-root, non-kernel task whose `/proc` stat ppid equals it passes the root-task rule.
149- **What still applies.** The child is not trusted:
150 - its mount and pid namespaces must equal the census's own;
151 - its exe, cwd, root, descriptors, mappings and mounts pass every reference check;
152 - the closing stat re-read must give the same start time.
153- **Who sets it.** Only journal-migrate does, through the context value `generationCensusParentKey`, holding its own pid. Every other caller of `productionGenerationKernelCensus` passes zero, so the rule is exactly as before. The registry child runs the same pinned binary, spawns nothing, and is waited for on cleanup (Astra's quiesce review).
154- **One parser.** `procStatParent` parses the ppid after the last `)` and is shared by `procParent` (the quiesce scan) and the census, so the two own-child rules cannot drift.
155
156Ruling 6: only `migrateRootClaimAuthority` sets the parent, and it runs only in claim migration.
157
158| Unit | Change | Class |
159|---|---|---|
160| `internal/host/rootgeneration_kernel_linux.go` `generationKernel.task` | the own-child pass for the root-task rule, untrusted | authority E2 (unchanged) |
161| `internal/host/rootgeneration_kernel_linux.go` `type generationKernel` | adds `parent` (declaration) | authority E2 (unchanged) |
162| `internal/host/rootgeneration_census_linux.go` `productionGenerationKernelCensus` | passes `generationCensusParent(ctx)` (declaration) | authority E2 (unchanged) |
163| `internal/host/rootmigration_production_linux.go` `migrateRootClaimAuthority` | sets the parent to its own pid | authority G,J4,E3 (unchanged) |
164| `internal/host/claimjournal_migration_linux.go` `procParent` | parse moved into `procStatParent` | authority C,G,J4 (unchanged) |
165
166Envelope re-pinned: `internal/host`. 9B8: no record write changes.
167
168Tests:
169
170- `TestPlan14GenerationCensusAcceptsOwnChildOnly` accepts a uid 989 child of the parent when its service lookup fails, which is the harness's case. It refuses with no parent and with another parent. With the right parent, it still refuses on any of these:
171 - an exe on the retired binary;
172 - a descriptor or mapping into it;
173 - a foreign mount namespace;
174 - a bound tree mount.
175
176 With the rule removed, the accept case fails with the harness's exact error.
177- `TestProcStatParent` covers comms containing `)` and newlines, and malformed lines.
178
179## Ack refresh
180
181Status: ACCEPTED (G10 fix, 2026-10-20). Ruled by platform-d8, accepted; the overlay is marked `rev3=census-ruling-claim-ack-refresh`. The harness's first claim-mode create on a freshly activated box never completed. The console's one in-request `Complete` ran before the box's ack, and its only retry is recovery at `started_at + 5 min`. The box's ack carried its first lease's 90 s expiry and was write-once, so every recovery saw an expired ack, got `ErrPending`, and the create stayed `creating`. Separately, the agent's regional worker pinned the roster-wide revision at startup, so the create's own actor hold stopped it with `ErrChanged` until the agent restarted.
182
183The re-pins are in a new overlay, `internal/provision/testdata/plan14-root-reads-claim-ack-refresh.tsv`, loaded last and now marked `rev3=census-ruling-claim-ack-refresh`. It adds no accepted-legacy row, so the seal stands.
184
185- **The hostclaim rule.** `AcknowledgeExecution` lets an existing ack be replaced only by one with the same op, enrollment, boot and controller, a lease ETag and a strictly later expiry. Byte-identical is still `ErrAckAlreadyApplied`. Anything else is still `ErrWrongState`, including an undecodable previous ack.
186- **The box refresh.** `acknowledgeOperation` hands an existing ack to `refreshAcknowledgement`. Once fewer than `ackRefreshBefore` (60 s, two-thirds of the lease TTL) remain, it writes a refreshed ack from the controller's current `Ready` state, under the same checks as the first ack.
187- **Same controller.** The root broker rotates the controller token on every renewal, so the slot that wrote the first ack identifies the controller, not the current token. The refreshed ack keeps the first ack's `ControllerID` and carries the current lease ETag and expiry. A restarted agent opens a new slot and leaves the old ack to expire.
188- **The regional check.** `claimRegionalEnrollment.VerifyRegional` compares the fresh actor with the startup actor field by field, ignoring only `Revision`. `productionClaimRoster.Actor` now also sets the row's `QuarantineRevision` on the actor, from the same roster read. That count only grows, so a box quarantined and readmitted since startup still never matches. `claimBoundIdentity` still pins the revision and is unchanged.
189
190Ruling 6: the refresh runs only in the claim participation loop (claims on, agent mode). `sameVerifiedActor` is called only from `VerifyRegional`, pinned in `TestStep8CensusChangesReachOnlyClaimMode`. Only the claim roster adapter sets `QuarantineRevision`; every other actor leaves it zero.
191
192| Unit | Change | Class |
193|---|---|---|
194| `internal/host/rootexecution_client.go` `productionClaimLocal.acknowledgeOperation` | hands an existing ack to the refresh (declaration) | authority C (unchanged) |
195| `internal/host/rootexecution_client.go` `productionClaimLocal.refreshAcknowledgement` | new: extends this controller's own ack (declaration) | authority C (new row, acknowledgeOperation's class) |
196| `internal/host/claimproduction.go` `productionClaimRoster.Actor` | the actor carries `QuarantineRevision`; same two reads | authority C,G,J4 (unchanged) |
197
198Envelopes re-pinned: `internal/host`, `internal/hostbox`, `internal/hostclaim`. 9B8: no record write changes; `TestPlan9B8RecordWriteCensus` passes unchanged.
199
200Tests:
201
202- `TestClaimAckRefreshCompletesCreateThroughRecovery` runs the production create through the real hostops controller, authority and control writer on a test clock. The console's in-request `Complete` gets `ErrPending`. The box acks 15 s in and keeps the ack usable at every 30 s tick, and recovery passes before five minutes leave the op and its registration alone. At `started_at + 5 min + 1 s` the create registration is expired but listed, both effects are complete and the hold is present. One recovery pass completes the create: the claim is serving with a stamp, and the sweep then settles the registration. With the first ack only, recovery gets `ErrPending` and the create stays stuck, as on the harness.
203- `TestClaimAckRefreshLeavesOtherControllersAlone` covers a refresh not yet due, a restarted controller, another boot, a lease that did not renew, a journalled ack naming another enrollment, op or controller, and a lease for another incarnation.
204- `TestClaimAckRefreshProductionClient` runs against the real root broker and RootClient. It waits for one real renewal, confirms the token rotated, and checks that the refreshed ack keeps the first controller ID.
205- `TestAcknowledgeExecutionExtendsOnlyTheSameLease` covers each hostclaim refusal.
206- `TestSameVerifiedActorIgnoresOnlyRevision`, `TestClaimRegionalEnrollmentSurvivesRosterRevision` and `TestClaimRegionalEnrollmentFieldChanges` cover the regional check, on the production roster adapter for the revision bump and a quarantine and readmission.
207
208## Roster binding
209
210Status: PROPOSED (roster binding build, tasks 1 to 7 at 7103ee07, 2026-10-20), awaiting a ruling. The design is `design-roster-revision-holds.md`, rev 3 plus section 7 (B1 to B4). Installed claim state was stamped with, and compared against, the roster's write counter. Every hold, activation, enrollment attempt and role change bumps that counter, so ordinary roster traffic refused claim work. Installed state now binds to `hostactors.BoxBinding`: a digest of the deployment's binding facts and this box's row identity and state. It moves on a quarantine, readmission, re-enrolment, floor publish, home-region change or census-key bind, and on nothing else.
211
212The re-pins are in a new overlay, `internal/provision/testdata/plan14-root-reads-roster-binding.tsv`, loaded last and marked `rev3=census-ruling-roster-binding`. RULED (platform-d8, 2026-10-20): accepted with conditions at baf278a4 (roster-binding-census-review2.md), all met at 52ac5c5a; Astra approved 52ac5c5a. It adds no accepted-legacy row, so the seal stands.
213
214- **No new read.** The binding is computed from the roster object that `productionClaimRoster.Actor` and `verifyClaimCleanupEnrollment` already read and verify. Root's startup binding comes from the existing startup read. So every changed unit keeps its class, task and identity; only its basis, sites and digest move.
215- **Root's two identities (R2).** Root's inventory actor is now a fresh `Identity.Verify` on every call. The startup actor pins only the census and supplies `startupBinding`.
216- **Receipts (N1, B2).** A stop receipt's `directory_revision` is the roster numeral persisted in the fence target (`FenceTarget.RosterRevision`), so the v0.28.4 console's bound still holds. The binding goes in a new `binding` field. A fence without a numeral is refused before any drain or kill.
217- **Obsolete state (N3, B1, B3).** At root startup, a pending inventory or rebind intent stamped under another binding than the startup binding is settled through the guard's `SettleObsoleteRootInventory` (fix round 1; the build first used `AbandonRootInventory`), never replayed. The installation stays quarantined and is marked settled: only startup's migration census inspects it. A journaled cold handover whose successor is stamped under another binding is settled through the new fenced `SettleObsoleteColdHandover`, never resumed. Every cold handover, new or replayed, first passes the fresh successor admission.
218- **Recovery (R3).** A predecessor inventory is reused only when it carries today's binding.
219- **Folded from Ack refresh.** That overlay's `productionClaimRoster.Actor` row (the actor carries `QuarantineRevision`) is superseded: the field is gone and the binding covers it. The row is re-pinned here. The Ack refresh overlay stays loaded where it is, and its other two rows are unchanged.
220- **New units** take the class of the callers that execute them. `var openRootMigrationState` is the only one with a read: the `rootstate.Open` of the migration directory, moved unchanged out of its two callers into a variable so tests can stand in for the root-owned directory. It takes both callers' tasks. The others are declarations with no read, pinned so a later change is reviewed.
221
222Ruling 6: a claims-off or no-census box never builds a claim assembly or claim owners. So it computes no binding, compares none, writes no fence and no receipt, and settles nothing. `TestStep8CensusChangesReachOnlyClaimMode` now scans the whole module and pins these callers:
223
224- `BoxBinding` is called only from `productionClaimRoster.Actor` and `verifyClaimCleanupEnrollment`. The design also named `recoverRootInventoryStartup`, but that function takes the startup actor's binding and does not compute one.
225- `productionClaimRoster` is built and `verifyClaimCleanupEnrollment` called only in `ProductionClaimOwnerProvider.OpenClaimOwners`. `OpenClaimOwners` is called only from `OpenProductionClaimAssembly` and `migrateClaimJournalsWith`, both claim mode.
226- `SameBinding` is called only at four claim-mode sites: `claimRegionalEnrollment.VerifyRegional`, `claimBoundIdentity`, `ClaimOwnerBindings.claimEvidenceCoordinator` and `servingParkRoot.prepareOrCommitServingReboot`. The design named six; the scan finds these four calls, and the reboot preparation also stores the `Bound()` actor at its prepare and commit sites, which the N2 witness covers.
227- The cold handover's `Admit` is set only in `ClaimRootHTTP.coldHandover`, and `Obsolete` only in `ClaimRootHTTP.RecoverPending`, never in a literal. `SettleObsoleteColdHandover` is called only from `ColdHandover.settleObsolete`.
228- `obsoleteIntent` is called only from `RecoverPending` and `recoverRootInventory`, and `settleObsoleteIntent` only from `recoverRootInventory`. `claimRootHTTP` and `rootStopCoordinator` are called only from `NewProductionRootClaimStop`.
229
230The `sameVerifiedActor` pin was dropped in phase A, with the function.
231
232Changed units, re-pinned at their existing class:
233
234| Unit | Change | Class | Executing identity |
235|---|---|---|---|
236| `internal/host/claimproduction.go` `productionClaimRoster.Actor` | the actor carries `BoxBinding` of its row from the same verified read (folded from Ack refresh) | authority C,G,J4 (unchanged) | whoever opens claim owners: the claim assembly (agent uid carlos, or root instance-control uid 0) or journal-migrate (uid 0) |
237| `internal/host/claimcleanup_identity.go` `verifyClaimCleanupEnrollment` | the cleanup actor carries its row's binding from the same startup read | authority C,G,J4 (unchanged) | as above |
238| `internal/host/claimassembly.go` `ClaimAssembly.Build` | the install and census pins are the binding | authority C,G (unchanged) | agent uid carlos; root instance-control uid 0 |
239| `internal/host/claimconstruction.go` `OpenProductionClaimAssembly` | the launch owner's fence target carries the binding and `RosterRevision` | authority C,E3,J4 (unchanged) | root controller and service UID |
240| `internal/host/claimfilesseed_select.go` `inspectFilesSeed` | the actor must carry a binding | authority C,J4 (unchanged) | the claim assembly's process |
241| `internal/host/claimhandover_recovery.go` `claimCrossBoxHandover.installRecoveryInventory` | predecessor reused only at today's binding (R3); successor stamped with it | authority C,G,J1 (unchanged) | the claim assembly's process |
242| `internal/host/claimrecovery_remote.go` `claimCrossBoxHandover.installRecoveryRemote` | the rebind request's successor carries the binding | authority C,J1 (unchanged) | agent uid carlos, through root's socket |
243| `internal/host/claimjournal_proof.go` `legacyJournalProof` | the match tuple digests the receipt's `binding` | authority C,G,J4 (unchanged) | journal-migrate and the offline root migration, uid 0 |
244| `internal/host/claimlaunch_root.go` `ClaimLaunchRoot.startRecorded` | fresh binding equals the launch target's stamp | authority E1,E2,E3 (unchanged) | root claim, migration or adoption controller uid 0 |
245| `internal/host/claimroleassembly_linux.go` `ClaimRoleAssembly.PrepareState` | local wake compares the binding | authority C,J1 (unchanged) | agent uid carlos; root validates |
246| `internal/host/claimservinghandover.go` `ClaimRootHTTP.validateServingHandover` | binding equality for prior and successor | authority C,J4 (unchanged) | root instance-control uid 0 |
247| `internal/host/claimservinglegacy.go` `validateServingLegacyStop` | the request carries `RosterRevision`; none is refused | authority C,J4 (unchanged) | root instance-control uid 0 |
248| `internal/host/claimservinglegacyroot_linux.go` `servingParkRoot.retainLegacyNoProcess` | binding equality | authority C,J4 (unchanged) | root instance-control uid 0 |
249| `internal/host/claimservingparkroot_linux.go` `servingParkRoot.withRootInstallation` | the actor must carry a binding | authority C,J4 (unchanged) | root instance-control uid 0 |
250| `internal/host/claimservingreboot_prepare_linux.go` `servingParkRoot.prepareOrCommitServingReboot` | persists `Bound()`, rereads with `SameBinding` (N2) | authority C,J4 (unchanged) | root instance-control uid 0 |
251| `internal/host/claimstop.go` `ClaimStopCoordinator.FenceAndStop` | numeral from the persisted fence, binding field; no numeral refused before any kill | authority C,J4 (unchanged) | root instance-control uid 0 |
252| `internal/host/claimstop.go` `ClaimStopCoordinator.witnessNoProcess` | numeral and binding from the witness target | authority C,G,J4 (unchanged) | root instance-control uid 0 |
253| `internal/host/claimstop_writers.go` `ClaimStopCoordinator.WritersStopped` | requires `roster_revision`, copied into the target | authority C,G,J4 (unchanged) | root instance-control uid 0 |
254| `internal/host/claimwakestop.go` `ClaimAssembly.collectCapturedStop` | binding fallback; the launch record's numeral, else the actor's counter | authority C,G,J4 (unchanged) | agent uid carlos |
255| `internal/host/rootexecution_authority.go` `ClaimRootHTTP.executionAuthority` | carries the fresh binding | authority C,E2,J4 (unchanged) | root instance-control uid 0 |
256| `internal/host/rootinventory.go` `ClaimRootHTTP.commitRootInventoryPending` | stamp equals the fresh `InventoryActor`'s binding | authority E2 (unchanged) | root instance-control uid 0 |
257| `internal/host/rootinventory.go` `ClaimRootHTTP.recoverRootInventory` | obsolete intents settled, never replayed (N3) | authority C,E1 (unchanged) | root instance-control or root participation/update, uid 0 |
258| `internal/host/rootrecovery_rebind.go` `ClaimRootHTTP.rebindRecoveryInventory` | successor and predecessor at the fresh binding (R3) | authority C (unchanged) | root instance-control uid 0 |
259| `internal/host/rootmigration_evidence.go` `readMigrationStop` | matches on `binding`; carries the numeral into `RosterRevision` | authority G,J4 (unchanged) | journal-migrate, offline operator, uid 0 |
260| `internal/hostbox/coldhandover.go` `ColdHandover.Run` | runs `Admit` first, for new and replayed handovers (N5, B1) | authority C,J2,J4 (unchanged) | root claim, migration or adoption controller uid 0 |
261| `internal/hostbox/coldhandover.go` `ColdHandover.Recover` | skips settled journals; settles obsolete ones (B3) | authority C (unchanged) | root instance-control uid 0 |
262| `internal/hostbox/coldhandover.go` `type ColdHandover` | adds `Admit` and `Obsolete` (declaration) | authority C,J4 (unchanged) | root instance-control uid 0 |
263| `internal/host/claimassembly.go` `NewProductionRootClaimStop` | assembly moved into the two helpers below (declaration) | authority C,E2 (unchanged) | root instance-control uid 0 |
264| `internal/host/claimhandover_transport.go` `ClaimRootHTTP.serveColdHandover` | uses `coldHandover` (declaration) | authority C,J4 (unchanged) | root instance-control uid 0 |
265| `internal/host/claimlaunch_authority.go` `ClaimLaunchRoot.authorize` | fresh binding equals the target's stamp (declaration) | authority D,E2 (unchanged) | root instance-control uid 0 |
266| `internal/host/claimretire_transport.go` `ClaimRootHTTP.RecoverPending` | fresh admission and obsolete settlement for cold handovers (declaration) | authority C,J4 (unchanged) | root instance-control uid 0 |
267| `internal/host/claimretire_transport.go` `type ClaimRootHTTP` | adds `startupBinding` (declaration) | authority E1 (unchanged) | root instance-control uid 0 |
268| `internal/host/claimservinglegacyroot_linux.go` `servingParkRoot.validateLegacyRootEvidence` | binding equality (declaration) | authority C,J4 (unchanged) | root instance-control uid 0 |
269| `internal/host/claimservingpark_transport.go` `ClaimRootHTTP.serveServingPark` | the actor must carry a binding (declaration) | authority C,J4 (unchanged) | root instance-control uid 0 |
270| `internal/host/claimservingparkroot_linux.go` `servingRootStops` | binding equality (declaration) | authority C,J4 (unchanged) | root instance-control uid 0 |
271| `internal/host/claimservingparksource_linux.go` `servingParkStateDirectory` | the inventory's stamp equals the binding (declaration) | authority C,J4 (unchanged) | root instance-control uid 0 |
272| `internal/host/claimservingreboot_prepare_linux.go` `servingParkLedger.observeRebootPreparation` | the bound actor must carry a binding (declaration) | authority C,J4 (unchanged) | root instance-control uid 0 |
273| `internal/host/claimservingreboot_prepare_linux.go` `type servingParkRebootAuthority` | comment only (declaration) | authority C,J4 (unchanged) | root instance-control uid 0 |
274| `internal/host/claimstop_recovery.go` `ClaimStopCoordinator.RecoverStopped` | numeral required before any kill; receipt numeral and binding (declaration) | authority C,J4 (unchanged) | root instance-control uid 0 |
275| `internal/host/rootinventory_startup.go` `recoverRootInventoryStartup` | its read moved into `openRootMigrationState`; fresh `InventoryActor`, `startupBinding` (now a declaration) | authority C,G (unchanged) | root control startup and system recovery uid 0 |
276| `internal/host/rootmigration_startup.go` `verifyRootMigrationStartup` | its read moved into `openRootMigrationState` (now a declaration) | authority G,F,J4 (unchanged) | root instance-control startup and recovery uid 0 |
277
278New units, at the class of their callers:
279
280| Unit | What it is | Class | Executing identity |
281|---|---|---|---|
282| `internal/host/rootmigration_startup.go` `var openRootMigrationState` | the migration directory's read-only `rootstate.Open`, moved from its two callers | authority C,F,G,J4 (both callers' tasks) | root instance-control startup and recovery uid 0 |
283| `internal/host/claimassembly.go` `ClaimAssembly.rootStopCoordinator` | root's stop coordinator, moved (declaration) | authority C,E2 (`NewProductionRootClaimStop`'s) | root instance-control uid 0 |
284| `internal/host/claimassembly.go` `ClaimAssembly.claimRootHTTP` | root's handler: fresh `InventoryActor`, `startupBinding` (declaration) | authority C,E2 (`NewProductionRootClaimStop`'s) | root instance-control uid 0 |
285| `internal/host/claimhandover_transport.go` `ClaimRootHTTP.coldHandover` | the operation cold handover with `Admit` (declaration) | authority C,J4 (`serveColdHandover`'s and `RecoverPending`'s) | root instance-control uid 0 |
286| `internal/host/claimhandover_transport.go` `ClaimRootHTTP.admitColdSuccessor` | fresh successor admission (declaration) | authority C,J4 (as above) | root instance-control uid 0 |
287| `internal/hostbox/coldhandover.go` `ColdHandover.settleObsolete` | settles an obsolete journaled handover (declaration) | authority C (`Recover`'s) | root instance-control uid 0 |
288| `internal/hostguard/coldhandover.go` `Guard.SettleObsoleteColdHandover` | the fenced guard settlement (declaration) | authority C (`settleObsolete`'s) | root instance-control uid 0 |
289| `internal/host/rootinventory.go` `ClaimRootHTTP.obsoleteIntent` | compares a stamp with `startupBinding` (declaration) | authority C,E1,J4 (`recoverRootInventory`'s and `RecoverPending`'s) | root instance-control uid 0 |
290| `internal/host/rootinventory.go` `ClaimRootHTTP.settleObsoleteIntent` | settles an obsolete intent through `SettleObsoleteRootInventory` (declaration; fix round 1, replacing `AbandonRootInventory`) | authority C,E1 (`recoverRootInventory`'s) | root instance-control uid 0 |
291| `internal/host/claimstop.go` `stopReceiptNumeral` | the receipt numeral from the fence target (declaration) | authority C,G,J4 (its three stop callers', and its fourth caller `verifyRetirementReceipt`, claimretire_transport.go:200; class unchanged) | root instance-control uid 0 |
292| `internal/host/claimstop.go` `receiptRosterRevision` | parses a receipt numeral (declaration) | authority G,J4 (`readMigrationStop`'s) | journal-migrate uid 0; the agent's transport check also uses it |
293| `internal/host/claimservinghandover.go` `servingPriorBinding` | binding equality, replacing `servingPriorRevision` (declaration) | authority C,J4 (the removed row's) | root instance-control uid 0 |
294
295Removed: `internal/host/claimservinghandover.go` `servingPriorRevision` (`@remove`; renamed `servingPriorBinding`). `TestPlan14RootInputInventoryCoverage` reads only the overlays up to activation-tools, where seed's row for it still stands, so it carries a PROPOSED exemption naming the rename. The census owner may prefer another way to retire that row.
296
297Envelopes re-pinned: `internal/host`, `internal/hostactors`, `internal/hostbox`, `internal/hostguard`.
298
2999B8: the design expected no change. Six key-site units changed source, each because it compares or produces an installation stamp: `installRecoveryRemote`, `servingParkStateDirectory`, `executionAuthority`, `commitRootInventoryPending`, `recoverRootInventory` and `rebindRecoveryInventory`. Their kinds and class are unchanged, and none writes an instance or sidecar record. They are re-pinned in `plan14ClaimRosterBindingWriteCensusInventory`, on top of the merged inventory, and the test now uses it. B3's `SettleObsoleteColdHandover` is not a record write in 9B8's sense. It rewrites the guard's own host-local record (`recordDir.write`) and, through its callback, the cold handover journal; neither is an instance or sidecar record in the deployment store. The scanner does not see it, as it does not see the siblings that make the same guard write (`BeginColdHandover`, `CompleteColdHandover`, `AbandonColdHandover`). Plan 14 pins it.
300
301Tests: the build's witnesses are listed in the phase reports (L0 to L7, N1 to N5). For the census: `TestPlan14RootReadCensus`, `TestPlan14RootInputInventoryCoverage`, `TestPlan9B8RecordWriteCensus` and `TestStep8CensusChangesReachOnlyClaimMode` pass. A mutation that adds a `SameBinding` call outside the four sites fails the ruling 6 test.
302
303### Roster binding, fix round 1
304
305Status: RULED with the rest of Roster binding (`rev3=census-ruling-roster-binding`). Four code-review findings were fixed; the same overlay is updated in place.
306
3071. **Settled installations stay inspectable.** Startup settlement now uses the new `Guard.SettleObsoleteRootInventory`, which leaves the record quarantined and marked settled. Only the migration census reads a settled record (`RootAuthority.readView` with `census`, view marked `Settled`); every execution read still refuses it. An interrupted commit that already landed is recorded as that commit would have recorded it. `verifyRootMigrationStartup` skips a settled first install that never got an inventory, and its floor check goes through the new `var rootMigrationStartupFloors`.
3082. **Target stamp checked.** `commitRootInventoryPending` refuses a non-empty target stamp that differs from the validated inventory stamp, before any intent. `obsoleteIntent` takes one stamp, validated evidence only.
3093. **Numeral bound.** `FenceAndStop` and `witnessNoProcess` call the new `ClaimStopCoordinator.boundRosterRevision`: 0 < n <= `VerifiedRosterRevision`, which `rootStopCoordinator` binds to the new `claimCleanupEnrollment.rosterRevision` (root's own signed-roster read, quarantined or not; the fresh identity when there is no cleanup enrollment).
3104. **B1 witness.** Test only.
311
312Changed units, re-pinned at their latest class: `recoverRootInventory`, `settleObsoleteIntent`, `obsoleteIntent`, `commitRootInventoryPending`, `verifyRootMigrationStartup`, `FenceAndStop`, `witnessNoProcess`, `rootStopCoordinator`, `type ClaimStopCoordinator`, and in hostguard `Guard.locked`, `Guard.AbandonRootInventory`, `Guard.CommitRootInventoryArtifacts`, `Guard.RebindRootInventory`, `RootAuthority.read`, `RootAuthority.write`, `RootAuthority.viewLocked`, `RootAuthority.MigrationInstallations` (each sets, clears or reads the settled mark; `MigrationInstallations` now lists settled records instead of refusing, which journal-migrate's collector refuses again in fix round 2).
313
314New units, at their callers' class:
315
316| Unit | What it is | Class |
317|---|---|---|
318| `internal/hostguard/root.go` `Guard.SettleObsoleteRootInventory` | the settlement write (host lock, exact intent) | authority C,E1 |
319| `internal/hostguard/rootview.go` `RootAuthority.readView` | viewLocked's read plus the census exception | authority C,G,J4 |
320| `internal/host/rootinventory.go` `ClaimRootHTTP.inventoryIntentApplied`, `rebindIntentUnapplied` | whether the interrupted commit landed | authority C,E1 |
321| `internal/host/rootmigration_startup.go` `var rootMigrationStartupFloors` | the floor check's test seam | authority G,F,J4 |
322| `internal/host/claimstop.go` `ClaimStopCoordinator.boundRosterRevision` | the numeral bound | authority C,G,J4 |
323| `internal/host/claimcleanup_identity.go` `claimCleanupEnrollment.rosterRevision` | root's fresh signed-roster read (one `Read`) | authority C,G,J4, root-input kept C,G |
324
3259B8: `commitRootInventoryPending` and `recoverRootInventory` re-pinned again; kinds and class unchanged. Ruling 6 now also pins the callers of `openRootMigrationState` (its two), `rootMigrationStartupFloors`, `SettleObsoleteRootInventory` and `boundRosterRevision`.
326
327### Roster binding, fix round 2
328
329Status: PROPOSED with the rest of Roster binding. The superseded-claim abandonment (`Guard.AbandonRootInventory`, reached from `recoverRootInventory`) now ends in the same terminal state as an obsolete-intent settlement. It keeps its exact-intent and fresh-claim checks, reconciles a landed commit through the same `applied` callback, and leaves the record quarantined and settled. Before, it left a quarantine the startup census refused on every restart. Both entry points now share the new `Guard.resolveRootInventory` (authority C,E1, a declaration with no read of its own). Re-pinned: `recoverRootInventory` (also in 9B8; kinds and class unchanged), `AbandonRootInventory`, `SettleObsoleteRootInventory`.
330
331Census review 2 conditions (folded into fix round 2):
332
333- **Condition 1.** `rootMigrationCollector.collect` (journal-migrate, G,J4) consumes `MigrationInstallations` too, and would have admitted a settled installation the claim still names as a live local item. It now refuses any `Settled` view with `ErrFenced` (`refuseSettledInstallations`, new, G,J4), as `MigrationInstallations` refused every quarantined record before. Startup's `verifyRootMigrationStartup` still inspects them. Re-pinned: `collect` at G,J4.
334- **Condition 2.** The overlay header and the two entries above now name `SettleObsoleteRootInventory`.
335- **Recommended (a).** `rootStopCoordinator` no longer falls back to `Identity.Verify`: without a cleanup enrollment the bound stays nil and every stop refuses. Its row's "no read" now holds. The lifecycle rootrunner fixture, whose owners open no cleanup enrollment, sets the bound itself.
336- **Recommended (b).** Ruling 6 also fails on any production reassignment of `openRootMigrationState` or `rootMigrationStartupFloors`.
337- **Condition 3** (the `inputExemptions` tag) is left for the ruling.
338
339## Router cache
340
341Status: RULED (platform-d8, 2026-10-20): census-ruling-claim-router-cache, accepted as proposed. Astra approved 27303f54.
342
343At 8.2 the harness's G10 re-run (2026-10-20, 23:25Z) stopped with `migration census: data entry "system/router/v0.28.15/carlos" is not classified`.
344
345**Cause.** A normal `carlos system update` leaves the agent's router cache, `<data>/system/router/<version>/carlos` (`systemCachePath`), in place until the agent's next hourly observe forgets it. A migration window can start inside that hour.
346
347**Fix.** `claimBoxDataCensus` now keeps the agent's whole system update directory (`systemDataDir`, `system/router`) as a shared tree: directories and regular files only, and a symlink refuses. The existing file rule for `requested.json` stays.
348- **Root reads.** Root already walks the cache beneath the data dir (`openBeneath` over `systemCacheParts`) and checks it against the release's signed sums before use.
349- **Not an installation artifact.** The cache is box state, and nothing the census adopts or imports.
350
351**Census.** `claimBoxDataCensus` has no read site of its own, so this is envelope only: overlay `plan14-root-reads-claim-router-cache.tsv` re-pins `internal/host`. The seal stands, and 9B8 has no change.
352
353**Ruling 6.** Only claim-mode census callers reach `claimBoxDataCensus`.
354
355**Tests.**
356- `TestMigrationCensusKeepsRouterCache` accepts a cache file and refuses a symlinked one.
357- With the class removed, it reproduces the harness's exact error.
358
359## Fresh install
360
361Status: PROPOSED (rev3=PROPOSED-claim-fresh-install). Fix round 2 follows Astra's review of 4dd4c436 (MERGE: block, one P2).
362
363The harness's first claim-mode create on leo (G10, 2026-10-21) reached serving at 08:42:03Z, but leo never installed it. Each pass, the agent logged `provision: claim install claimtrial2.dev.harness.test: hostbox: selected authority changed`. Root's inventory stayed at revision 1 with no route entry, and no registry row was written.
364
365**Cause.** The unit-shape refusal refused with `ErrChanged` whenever the host had no registry row. `productionClaimUnitShapePolicy` adds that refusal to every production claim policy. A fresh install has no row until `registerRemote`'s second `work.With` writes it, and the policy runs on every `work.With` before that. So every fresh claim install refused before its route entry reached root. The non-remote `Reconciler.Install` path refused the same way. The binding witnesses used an allow-all policy and never ran the refusal.
366
367**Fix (round 1, 4dd4c436).** With no row, the refusal checked an empty route for the host, as `rootClaimRestartShape` does. Astra found that this admitted all empty-identity work on a host whose row was gone, not just the install. In particular, a timer delivering to its cached route could register a producer and run `MarkTimerFired` after the route was removed.
368
369**Fix (round 2).** Only the installation's own work may go on without a row.
370- **The mark.** `Reconciler.installWithWork` marks the work it selects as installation work (`hostbox.Selection.Installation`). The mark lives in an unexported field that only `hostbox` can set, on a copy of the work `SelectAt` returned. This is the path to `registerRemote` and to the local `ownedRow` write.
371- **Why not a context key.** A context key would miss the policy calls that matter. The install closures capture the caller's context, not `installWithWork`'s: `registerRemote`'s `work.With` calls use `installRemote`'s `ctx`, and `Reconciler.Install`'s use `Install`'s. The mark rides on the selected work, so it reaches every `With` on that work and nothing else.
372- **The decision.** `claimUnitShapeCheck` is the refusal's decision, split out of `claimUnitShapePolicy`, which now passes it `s.Installation()`. Without the mark, a missing row refuses with `ErrChanged` as before 4dd4c436. With it, the check reads an empty route for the host.
373- **Still refused.** The shape and binding reads are unchanged. A configured shape or a saved unit binding refuses: through `checkClaimUnitShape` in instance-control and adopt modes, and root's `boundInputs` in agent mode. A row for another host refuses. `registerRemote` still refuses to recreate a row its inventory records as materialized.
374
375**Census.** Overlay `plan14-root-reads-claim-fresh-install.tsv` is loaded last and updated in place.
376- `claimUnitShapePolicy` keeps no read of its own. It is removed (`@remove`), and its reads are pinned on `claimUnitShapeCheck` at the same class and task (authority, E2).
377- `Reconciler.installWithWork` is re-pinned at its existing class (authority, C). Its reads are unchanged.
378- The `internal/host` and `internal/hostbox` envelopes are re-pinned.
379- No accepted-legacy row, so the seal stands. 9B8 has no change.
380
381**Ruling 6.** No new caller. The refusal is reached only from the production claim policy, as before. Only `installWithWork` sets the mark.
382
383**Production callers and the checks that refuse a host whose row is absent for another reason** (retired, deleted or mid-retirement). This is the census owner's owed confirmation, taken from Astra's caller table with lines at this commit. The timer row is updated to round 2.
384
385| Caller | Where | What refuses a host whose row is absent for another reason |
386|---|---|---|
387| Registration | `ProductionClaimOwnerProvider.OpenClaimOwners` adds the refusal (host/claimproduction.go:217); `OpenProductionClaimAssembly` composes it (host/claimconstruction.go:414) | n/a |
388| Main consumer | `ClaimAssembly.Build` hands the policy to the shared `WorkSource` (host/claimassembly.go:165) | per operation, below |
389| Fresh or retried installation (the only marked work) | `Reconciler.installWithWork` | The claim gate: serving, no operation, `Exec` is this box, not lost (hostbox/reconcile.go:196); a deleting, purging or releasing claim refuses. The record ETag (hostbox/reconcile_adapter.go:75). The guard's retired-stamp refusal (hostguard/root.go:174). `registerRemote` refuses an absent row its inventory records as materialized (hostbox/inventory_registration.go:187). The shape and binding refusal still refuses a configured shape or saved binding |
390| Ordinary serving wake, config and release effects | `WorkSource` work with a nonempty identity | `claimInputPolicy` refuses a missing row (host/claimproduction.go:270). The unit-shape refusal refuses it too (unmarked). Cold wake also rereads the route |
391| Move and relocation work | `WorkSource` work | Operation, phase, executor and input checks. Held actions require the operation's hold. The unit-shape refusal refuses a missing row (unmarked) |
392| Timer mutation | `MutateTimer` | Requires a current, owned route (hostbox/timer_registration.go:73). The unit-shape refusal refuses a missing row (unmarked) |
393| Timer delivery and bookkeeping | `ClaimTimerDelivery` (host/claimtimer.go:40), `Runner.record`'s `MarkTimerFired` (schedule/runner.go:640) | **Round 2:** the unit-shape refusal refuses a missing row, because timer work is never marked. `TestClaimTimerRefusedAfterRouteRemoval` witnesses this |
394| Root restart requests | `rootRestartRequests` | The unit-shape refusal refuses a missing row (unmarked). It also requires a retained installation, canonical record and verified running-unit identity (host/rootrestart_requests.go:123) |
395| Root held-release work | reuses the assembly policy (host/movesheldrelease_production.go:72) | The independent authority recheck requires a current row (host/movesheldrelease_root.go:481) |
396| Admin token mint | `claimAdminTokens` (host/claimproduction.go:182) | Uses a separate base policy and never calls this refusal |
397| Stop, delete, retirement | Fence, stop-evidence and exact-inventory paths | Never obtain authority through this policy |
398
399**Tests.**
400- `TestClaimFreshInstallUnderProductionPolicy` runs an L2-style fresh remote install through root's real `commitRootInventory`, under the production policy (`hostbox.Policy` with the hold policy, `.WithRefusals(productionClaimUnitShapePolicy(...))`). It fails on 27303f54, and when the nil-row admission or `installWithWork`'s mark is removed.
401- `TestClaimTimerRefusedAfterRouteRemoval` selects a timer for an installed serving host and keeps its cached route, then removes the route. The claim, installation, timer row and producer all stay valid. It asserts:
402 - no producer registration;
403 - no `MarkTimerFired`;
404 - an unchanged timer row.
405
406 A control run, with the route present, delivers and records. The test fails on 4dd4c436 and when the mark check is dropped.
407- `TestClaimUnitShapePolicyFreshInstall` covers instance-control mode:
408 - ordinary work through the production policy refuses every missing row;
409 - the installation's own work passes with no shape and no binding, or with no binding store;
410 - the installation's work refuses a configured shape (`ErrUnsupported`) and a saved binding, with or without the shape;
411 - a row for another host refuses, and matching rows pass, for both kinds of work.
412- `TestClaimUnitShapePolicyFreshInstallAgentReader` covers agent mode. Ordinary work never reaches root's reader without a row. The installation's work sends it `Route{Host}`, and root's `boundInputs` passes or refuses as above.
413- `TestInstallationWorkMarksOnlyTheInstallationsSelection` (hostbox): the mark is set only on `installWithWork`'s copy of the selected work.
414- The binding witnesses (the `claim_binding_*` L2/L3 worlds and the assembly-wired agent) run the production policy instead of allow-all, and pass.
415