| index 9ce13445c..b9972fd0d 100644 |
| --- a/docs/superpowers/plans/2026-10-17-region-ha-2b-joining.md |
| +++ b/docs/superpowers/plans/2026-10-17-region-ha-2b-joining.md |
| @@ -4,11 +4,11 @@ |
| |
| **Goal:** Build spec 2 §5's box side and console side of joining, dormant behind `CARLOS_REGION_HA` and claim mode: the agent chooses hosting or joining at every start, a joining box publishes its identity record and waits behind a 503 health answer, the console confirms the box's group membership itself and then invites and enrolls it, root arms the box's guard, a fresh box installs its channel's release into both protected slots, and `host.env` is assembled from SSM parameters. |
| |
| -**Architecture:** The mode is chosen once, in `RunAgent`, before anything else starts (`hostingGate`). Joining is its own small run (`internal/host/joining.go`): the identity record loop, a 503-only health listener, the guard-ready report, and a marker that asks root (`carlos system join`, behind a path unit) to arm the guard with the invitation's enrollment. Hosting is main's claim agent, which reports hosting to the health answer only while its claim worker runs. The console is the only membership confirmer (H28): its enrolment pass, `hostactors.AutoEnroller` driven by `internal/console/enrollpass.go`, asks the cloud through `hostactors.MembershipReader`, its own read-only client, and `EnrollAutomatic` and `CheckConfirmed` make that read themselves and build the confirmation inside `hostactors` where no caller can supply one. The write each read allows runs on a store bound to the read's deadline (`writeBound`, the single enforcement point), so no store call of it is issued 60 seconds or more after the read's anchor, the describe's start, and each call it does issue is cut off 15 seconds after it began, so a write that commits late is at most 60 + 15 seconds behind its read. An outcome the bound cannot confirm is unknown, and the next pass settles it the ordinary way, by reading the roster. They stand on main's `BeginEnrollmentAttempt`, `Invite`, `Replace` and `membership.go`, unchanged. A box its region's group launched is referenced by its group reference, `g-` and 20 hex digits of its account and instance's digest (`GroupBoxReference`): it publishes under it, `statusLoop` included (and publishes nothing while the reference cannot be read), and the console derives it again from the instance it proved, never trusting the key the box chose. The status reader drops reports no box has rewritten in a day before its key cap, so replaced boxes' history never hides the running ones. Each AWS part is an adapter named for AWS: the SSM parameter store (`internal/hostenv/awsssm.go`) and the Auto Scaling and EC2 describe calls (`internal/hostactors/awsmembership`, opened by the console in `internal/console/awsmembership.go`). |
| +**Architecture:** The mode is chosen once, in `RunAgent`, before anything else starts (`hostingGate`). Joining is its own small run (`internal/host/joining.go`): the identity record loop, a 503-only health listener, the guard-ready report, and a marker that asks root (`carlos system join`, behind a path unit) to arm the guard with the invitation's enrollment. Hosting is main's claim agent, which reports hosting to the health answer only while its claim worker runs. The console is the only membership confirmer (H28): its enrolment pass, `hostactors.AutoEnroller` driven by `internal/console/enrollpass.go`, asks the cloud through `hostactors.MembershipReader`, its own read-only client, and `EnrollAutomatic` and `CheckConfirmed` make that read themselves and build the confirmation inside `hostactors` where no caller can supply one. The write each read allows runs on a store bound to the read's deadline (`writeBound`, the single enforcement point), so no store call of it is issued 60 seconds or more after the read's anchor, the describe's start, and the console waits at most 15 seconds for any call it issues. An outcome the console did not see is unknown, and a later pass settles it the ordinary way, by reading the roster. A cancelled write can still commit later, on the roster revision it was built against. They stand on main's `BeginEnrollmentAttempt`, `Invite`, `Replace` and `membership.go`, unchanged. A box its region's group launched is referenced by its group reference, `g-` and 20 hex digits of its account and instance's digest (`GroupBoxReference`): it publishes under it, `statusLoop` included (and publishes nothing while the reference cannot be read), and the console derives it again from the instance it proved, never trusting the key the box chose. The status reader drops reports no box has rewritten in a day before its key cap, so replaced boxes' history never hides the running ones. Each AWS part is an adapter named for AWS: the SSM parameter store (`internal/hostenv/awsssm.go`) and the Auto Scaling and EC2 describe calls (`internal/hostactors/awsmembership`, opened by the console in `internal/console/awsmembership.go`). |
| |
| **Tech Stack:** Go 1.26, module `github.com/carlosframework/platform`; `internal/hostenv` (new), `internal/host`, `internal/hostactors`, `internal/console`, the `carlos` CLI (repo root); AWS SDK v2 SSM and Auto Scaling (new, v1.74.0, the versions built against the repo's `aws-sdk-go-v2 v1.44.0`), EC2 (already a dependency). |
| |
| -**Spec:** `docs/superpowers/specs/2026-10-15-region-ha-two-box-regions-design.md`, **rev 19**: §5 (boot and enrolment, host.env's fixed path and checked write, the write deadline and its per-call timeout, unknown outcomes settled by the roster, group references, the status listing and the activation gates), §12 (spec 2's hand-off to spec 3), §4 (the SSM layout and IAM, for 2c's seams), §25 (the provider boundary), rulings H1 to H28 (H28: the console is the only membership confirmer), and platform-1e's census ruling region-ha-2b (RULED by the Plan 14 controller 2026-10-16, with its conditions). Plan 2a (`2026-10-16-region-ha-2a-core.md`, built and merged as 6834659c) for format and the seams it left. |
| +**Spec:** `docs/superpowers/specs/2026-10-15-region-ha-two-box-regions-design.md`, **rev 20**: §5 (boot and enrolment, host.env's fixed path and checked write, the write deadline and its per-call timeout, unknown outcomes settled by the roster, group references, the status listing and the activation gates), §12 (spec 2's hand-off to spec 3), §4 (the SSM layout and IAM, for 2c's seams), §25 (the provider boundary), rulings H1 to H28 (H28: the console is the only membership confirmer), and platform-1e's census ruling region-ha-2b (RULED by the Plan 14 controller 2026-10-16, with its conditions). Plan 2a (`2026-10-16-region-ha-2a-core.md`, built and merged as 6834659c) for format and the seams it left. |
| |
| **Review round 1.** Astra's review of the first draft (`CODEX-REVIEW-REGION-HA-2B-1.md`) found eight things; this revision answers all of them, with the claim owner's conditions on the first (C1 to C4): |
| |
| @@ -49,10 +49,19 @@ |
| |
| | Finding or ruling | What changed | |
| |---|---| |
| -| #1 (P2): a success that returns after the deadline escapes the bound | U2: every store call of a bound write runs under `deadlineStore.call`, which gives it a hard 15-second timeout (`storeCallTimeout`) on top of the 60-second issue bound. A store that ignores its context is cut off by the wrapper itself, and a call that answers, even successfully, only after the deadline is `ErrWriteDeadline`, unknown. A write that commits late is at most 60 + 15 seconds behind its read (Tasks 4, 5) | |
| +| #1 (P2): a success that returns after the deadline escapes the bound | U2: every store call of a bound write runs under `deadlineStore.call`, which gives it a hard 15-second timeout (`storeCallTimeout`) on top of the 60-second issue bound. A store that ignores its context is cut off by the wrapper itself, and a call that answers, even successfully, only after the deadline is `ErrWriteDeadline`, unknown. Round 5 corrected what this row claimed: the 15 seconds bound how long the console waits, not when a write commits (Tasks 4, 5) | |
| | #2 (P2): the read-back accepts a stale describe; #3 (P2): a failed page discards the escalation | U1: the read-back, its in-memory table, its fresh describe and its page are removed, and with them RHB-C65 and RHB-C66. An unknown outcome is settled on the next pass by the roster read every pass makes: a box the roster lists is enrolled and done; one it does not list goes on with the same attempt, if it is still valid, and no backoff growth. A detached instance that keeps running is spec 3's: its regional worker reconciles roster membership against the group's (Activation notes, Open question 9) (Task 5) | |
| | #4 (P2): host.env's directory check leaves the write path open | U3: `host-env write` has no `--out`; the path is fixed at `/etc/carlos/host.env`. `writeHostEnv` (Linux) opens `/etc` and `/etc/carlos` by descriptor, `O_NOFOLLOW\|O_DIRECTORY`, and refuses either unless `fstat` shows a directory root owns that no group or other can write; it writes a temporary file 0600 inside the held directory (`openat`), fsyncs it, renames it within the directory and fsyncs the directory. Off Linux, `write` refuses. RHB-C67 is reworded and RHB-C68 added; no pending row named `--out` (Task 2) | |
| |
| +**Review round 5.** Astra's fifth review (`CODEX-REVIEW-REGION-HA-2B-5.md`) closed round 4's four findings and found one more; platform-1e accepted revision 4 with three items (binding, V1 to V4): |
| + |
| +| Finding or ruling | What changed | |
| +|---|---| |
| +| New (P2): the timeout does not establish a 75-second commit ceiling | V1: the claim is withdrawn everywhere. The guarantee, stated as it is: every store call of the invite and the enrolling write is issued before the deadline (60 s from the start of the membership describe); the console waits at most 15 s for any call; a write whose result the console did not see is unknown and is settled on a later pass by reading the roster. A cancelled write can still commit later; such an enrolment rests on a membership read that was true when the write was issued, and is the same exposure as an instance detached after enrolment, which spec 3's worker reconciles. No mechanism is added. Task 5's `TestAWriteTheConsoleGaveUpOnCanCommitLater` documents the exposure: a delayed `Put` commits after the next pass found the box absent and refused it on a fresh describe (Tasks 4, 5) | |
| +| platform-1e: the per-call context reaches the SDK | V2: Task 4's `TestTheBoundReachesTheSDKRequest` builds the console's stores as `claim_runtime.go` does, over `release.NewS3Store` with only the network replaced, and records each HTTP request's context deadline: 15 seconds from the call, or the earlier write deadline. A wrapper that drops the context fails it (Task 4) | |
| +| platform-1e: the two host.env rows | V3: `writeHostEnv` and `checkedDir` re-marked `census-ruling-region-ha-2b` (Task 2) | |
| +| platform-1e: late commits land only where they were aimed | V4: every write in the invitation and enrolment paths is conditional, shown from main's code in "Global Constraints: Freshness bound" | |
| + |
| ## Scope |
| |
| Spec 2 is four plans. This is **2b**. What 2a left it, and what it leaves the others: |
| @@ -82,7 +91,7 @@ Spec 2 is four plans. This is **2b**. What 2a left it, and what it leaves the ot |
| - **Membership (H16, H28; C1, C2).** The console is the only membership confirmer. No box writes or contributes membership evidence, and no box role can describe the group or create under `control/box-enrollment/`. |
| - `EnrollAutomatic` and `CheckConfirmed` take a `MembershipReader` (the console's AWS client) and call `Member` themselves. The confirmation is built from that answer alone, by the unexported `confirmMembership`, and never leaves `hostactors`; no exported function takes a confirmation or a `Membership`. Tests use a fake reader. |
| - A member is in the region's group, `Pending` or `InService`, `pending` or `running`, owned by the region's account, in its AWS region, with a request id from each of the two calls. |
| - - **Freshness bound: 60 seconds, enforced at the write (S1, T3, U2).** A fresh describe runs immediately before each write, the invitation and the enrolment. **The write deadline is anchored at the describe's start, which is never later than its completion, and a write 60 s or more after that anchor is refused. Each store call issued before it gets a hard timeout of 15 s (`storeCallTimeout`). So a write is issued no later than 60 s after the membership read, and one that commits late is at most 60 + 15 = 75 s behind it.** `writeBound` is the single enforcement point. It gives the sealed writers (`Invite`, and `Replace` with the `checkMembership` it makes) a copy of the directory whose store refuses every call at or after the deadline, by the read's clock or the context's, and a context that ends at the deadline. What it guarantees: no store call, the roster `Put` and the invitation's `Put` included, is issued at or after the deadline; a call in flight then is cancelled; a call the store has not answered 15 s after it was issued is cut off by the wrapper itself (`deadlineStore.call` returns on its own and cancels the call's context, which the AWS SDK enforces at the HTTP layer); and a call that answers only after the deadline, even with success, is `ErrWriteDeadline`, unknown. The store is wrapped, not trusted: `release.S3Store` passes its context to the AWS SDK, which honours it, but `release.DirStore` ignores its context. The pass's S3 store is the console's claim data boundary, whose client the console shares; rather than a second client and a second boundary, the wrapper gives each call its own 15-second context, the same hard limit at the HTTP layer, and does not wait past it whatever the store does. A missed deadline keeps the attempt and its wait. A describe that itself took 60 seconds or more is `ErrMembershipStale`, checked once in `checkConfirmed` after the describe, and counts against the attempt. Nothing else checks freshness: no callback in `enroll` and no check before `Invite`, which would only repeat the bound; between the describe and `Invite` sits nothing that waits. |
| + - **Freshness bound: 60 seconds, enforced at the write (S1, T3, U2).** A fresh describe runs immediately before each write, the invitation and the enrolment. **The write deadline is anchored at the describe's start, which is never later than its completion, and a write 60 s or more after that anchor is refused. Each store call issued before it gets a hard timeout of 15 s (`storeCallTimeout`).** What that guarantees (V1): every store call of the invite and the enrolling write is issued before the deadline (60 s from the start of the membership describe); the console waits at most 15 s for any call; a write whose result the console did not see is unknown and is settled on a later pass by reading the roster. A cancelled write can still commit later; such an enrolment rests on a membership read that was true when the write was issued, and is the same exposure as an instance detached after enrolment, which spec 3's worker reconciles. `writeBound` is the single enforcement point. It gives the sealed writers (`Invite`, and `Replace` with the `checkMembership` it makes) a copy of the directory whose store refuses every call at or after the deadline, by the read's clock or the context's, and a context that ends at the deadline. What it guarantees: no store call, the roster `Put` and the invitation's `Put` included, is issued at or after the deadline; a call in flight then is cancelled; a call the store has not answered 15 s after it was issued is cut off by the wrapper itself (`deadlineStore.call` returns on its own and cancels the call's context, which the AWS SDK enforces at the HTTP layer); and a call that answers only after the deadline, even with success, is `ErrWriteDeadline`, unknown. The store is wrapped, not trusted: `release.S3Store` passes its context to the AWS SDK, which honours it, but `release.DirStore` ignores its context. The pass's S3 store is the console's claim data boundary, whose client the console shares; rather than a second client and a second boundary, the wrapper gives each call its own 15-second context, the same hard limit at the HTTP layer, and does not wait past it whatever the store does. Every write in the invitation and enrolment paths is conditional (V4; lines are main's, `6834659c`): the roster write is `Directory.Replace`'s compare-and-swap, `d.Store.Put(ctx, Key, wire, before.ETag)` (`internal/hostactors/roster.go:281`), which refuses an observation with no ETag (`roster.go:216`), and every roster write the pass makes goes through it (`enroll.go:80`, the enrolment `EnrollAutomatic` makes through `enroll`, and `enrollment_confirmation.go:158`, `BeginEnrollmentAttempt`'s); the invitation is created only if absent, `d.Store.Put(ctx, key, raw, "")` (`internal/hostactors/invitation.go:78`). `release.Store`'s contract makes `""` create-only (`If-None-Match: *`) and an ETag replace-only (`If-Match`) (`internal/release/store.go:40`), as `S3Store.Put` sends them (`store.go:795` to `:797`) and `DirStore.Put` checks them (`store.go:430` to `:440`). The pass's store keeps that: the observed mutation boundary re-reads the object and refuses a Put whose ETag no longer matches before forwarding the same condition (`internal/hostops/mutation_boundary.go:147` to `:162`), and the conditional record store passes it through for every key that is not an instance or sidecar record (`internal/hostops/record_writes.go:86` to `:88`), over the console's raw store (`internal/console/claim_runtime.go:42`, `:46`, `:60`). So a write that commits late lands only on the exact roster revision it was built against, or creates an invitation that did not exist; it never overwrites a newer revision or another enrolment. A missed deadline keeps the attempt and its wait. A describe that itself took 60 seconds or more is `ErrMembershipStale`, checked once in `checkConfirmed` after the describe, and counts against the attempt. Nothing else checks freshness: no callback in `enroll` and no check before `Invite`, which would only repeat the bound; between the describe and `Invite` sits nothing that waits. |
| - **Unknown outcomes are settled by the roster (U1).** An outcome the bound cannot confirm (`ErrWriteDeadline`) is resolved the ordinary way on the next pass, which reads the signed roster as every pass does: a box the roster lists is enrolled, and the pass has nothing more to do for it; one it does not list goes on with the same attempt while it is still valid, and its wait does not grow. There is no read-back, no memory of unknown outcomes and no page. An instance that left its group but still runs after its enrolment landed keeps that enrolment until an operator removes it or spec 3's regional worker reconciles it ("Activation gates", note); a terminated one is kept from hosting and from capacity by main's termination fence (`internal/hostactors/fence_authority.go:14`, honoured at `internal/hostops/executor.go:204`), and a stopped one by the stale-edges rule (`internal/boxchoice/boxchoice.go:131`) and C3's restart hold (Task 6's `relaunched`). |
| - **Audit (S5).** Main's roster wire has no field for request ids, on the attempt or on the box entry, so none is added: the console logs every membership read, with the box's reference, its attempt id, its instance id, both request ids (or "none" for a call that failed before the cloud answered) and the read's time, and what came of it: invited, enrolled, refused or unknown (`AutoEnroller.Logf`). |
| - **Unknown is not invalid (#7).** A read that fails, or is not an answer (EC2 does not list the instance, a missing request id), is `ErrMembershipUnknown`: the pass asks again next time with the same attempt, which it neither supersedes nor counts. Only the cloud's positive answer that the instance is not a current member (`ErrNotMember`), or a read that went stale before its write (`ErrMembershipStale`), counts against the attempt: the pass keeps it, asking nothing more, for `Patience`, doubling with each refusal up to 32 times, then begins another. A record whose instance is gone costs one roster write and one describe an hour. |
| @@ -107,15 +116,15 @@ Spec 2 is four plans. This is **2b**. What 2a left it, and what it leaves the ot |
| |
| ## Census items for platform-1e (census ruling region-ha-2b) |
| |
| -**RULED by the Plan 14 controller 2026-10-16 as census-ruling-region-ha-2b**, approved on conditions, all built here (round 3). The overlay's first line says so. One legacy-sealed unit changes, as ruled: `statusLoop`. Every other legacy-sealed unit 2b touches is called, never edited: `runAgent`, `edgesFileLoop`, `ExecutorChooser.eligible`, `stageCandidate`, `Directory.Invite`, `Directory.BeginEnrollmentAttempt`, `Directory.checkMembership`, `Directory.Replace`, `readIdentityRecord`, `status.CollectBox`, `status.BuildFleet` and `RunSystemGuardArm`. The gate sits in `RunAgent`, which is not a census unit, so `runAgent` keeps its digest. Rows that changed in round 4 are marked, with their old values; the new values, and the two new rows marked PROPOSED, are platform-1e's to confirm. The seal does not move in round 4. |
| +**RULED by the Plan 14 controller 2026-10-16 as census-ruling-region-ha-2b**, approved on conditions, all built here (round 3). The overlay's first line says so. One legacy-sealed unit changes, as ruled: `statusLoop`. Every other legacy-sealed unit 2b touches is called, never edited: `runAgent`, `edgesFileLoop`, `ExecutorChooser.eligible`, `stageCandidate`, `Directory.Invite`, `Directory.BeginEnrollmentAttempt`, `Directory.checkMembership`, `Directory.Replace`, `readIdentityRecord`, `status.CollectBox`, `status.BuildFleet` and `RunSystemGuardArm`. The gate sits in `RunAgent`, which is not a census unit, so `runAgent` keeps its digest. Rows that changed in round 5 are marked, with their old values; the new values are platform-1e's to confirm. The seal does not move in round 5. |
| |
| **Census rows.** |
| |
| | Task | Census | Unit | Class (ruled) | Basis, change and the ruling's conditions | Seal | |
| |---|---|---|---|---|---| |
| | 2 | root-read | `internal/hostenv/cli.go:runHostEnv` | authority, A,J4 | New; **changed in round 4** (kinds `file:ReadFile@1500`, digest `df447017...`; were `file:Lstat@1495,file:ReadFile@1930`, `b1cdca1b...`). `write` runs as root, takes only allowlisted names with safe values, and hands the file to `writeHostEnv` at its fixed path, so its `Lstat` of the directory goes; `seed`'s read site is its `--from` file. Conditions: no box write to the parameters (2c seam 3), the allowlist and atomic, checked write (built, U3), authority-selecting values cross-checked against the roster at the agent's use (Task 6) | stands | |
| -| 2 | root-read | `internal/hostenv/write_linux.go:writeHostEnv` | **PROPOSED** authority, A,J4 (`PROPOSED-region-ha-2b-r4`) | **New in round 4** (kinds `file:Open@68,file:Openat@509`, digest `1245fc0f...`). Writes `/etc/carlos/host.env` only through `/etc` and `/etc/carlos` held by descriptor and checked by `checkedDir`: a temporary file created 0600 (`O_EXCL\|O_NOFOLLOW`) inside the held directory, fsynced, renamed over `host.env` within it, the directory fsynced. Its read sites open the root and the temporary file. Proposed under `runHostEnv`'s ruling, as the checked parent its condition 2 asks for | stands | |
| -| 2 | root-read | `internal/hostenv/write_linux.go:checkedDir` | **PROPOSED** authority, A,J4 (`PROPOSED-region-ha-2b-r4`) | **New in round 4** (kinds `file:Fstat@576,file:Openat@168,file:Openat@372`, digest `012257ac...`). Opens one directory under one already held, `O_NOFOLLOW\|O_DIRECTORY`, creating it 0755 first only when asked and missing, and refuses it unless `fstat` shows a directory owned by root (this process's user, which `write` requires to be root) that no group or other can write | stands | |
| +| 2 | root-read | `internal/hostenv/write_linux.go:writeHostEnv` | authority, A,J4 (`census-ruling-region-ha-2b`; **re-marked in round 5**, was `PROPOSED-region-ha-2b-r4`) | New in round 4 (kinds `file:Open@68,file:Openat@509`, digest `1245fc0f...`). Writes `/etc/carlos/host.env` only through `/etc` and `/etc/carlos` held by descriptor and checked by `checkedDir`: a temporary file created 0600 (`O_EXCL\|O_NOFOLLOW`) inside the held directory, fsynced, renamed over `host.env` within it, the directory fsynced. Its read sites open the root and the temporary file. Ruled with `runHostEnv`, as the checked parent its condition 2 asks for | stands | |
| +| 2 | root-read | `internal/hostenv/write_linux.go:checkedDir` | authority, A,J4 (`census-ruling-region-ha-2b`; **re-marked in round 5**, was `PROPOSED-region-ha-2b-r4`) | New in round 4 (kinds `file:Fstat@576,file:Openat@168,file:Openat@372`, digest `012257ac...`). Opens one directory under one already held, `O_NOFOLLOW\|O_DIRECTORY`, creating it 0755 first only when asked and missing, and refuses it unless `fstat` shows a directory owned by root (this process's user, which `write` requires to be root) that no group or other can write | stands | |
| | 3 | root-read | `internal/host/installchannel.go:channelInstall.run` | authority, F,J4 | New. Its one read site opens root's apply lock (`systemApplier.lockPath`); the install goes through `verifyPointer`, `hostfloor.CheckImage` and `stageCandidate`, unchanged; cache directories root-created, symlinks refused, as ruled | stands | |
| | 4 | record-write (B8) | `internal/hostactors/membershipread.go:deadlineStore.Put` | nonrecord (`def:Put,sel:Put`) | New; **digest changed in round 4** (`848f2b52...`, was `e0f34b1d...`): it runs under `deadlineStore.call`, the 15-second hard timeout, and a success that arrives after the deadline is unknown. It forwards `Put` and refuses it at or after the deadline; it names no record key | n/a | |
| | 5 | root-read | `internal/hostactors/autoenroll.go:AutoEnroller.inputs` | accepted-legacy, J4 root-input | New, unchanged in rounds 3 and 4 (U1 removed the read-back, not this unit). The signed roster and the host floor, in the console | moves | |
| @@ -204,14 +213,14 @@ nice -n 19 go test ./internal/console/ -run 'TestClaimAuthoritySourceCensus' -co |
| 14. **Ownership is reconciled on a retry (S2).** Task 3's `TestInstallChannelReconcilesOwnershipOnARetry` and `TestInstallChannelRefusesASymlinkedParent`. |
| 15. **A group box never publishes status under a key it doesn't own (T1).** Task 6's `TestAGroupBoxPublishesOnlyUnderItsReference`, through `statusLoop`'s own tick and `WriteBox`: while IMDS fails, another box's report under the colliding label is untouched and nothing is written, with the switches on or off; the reference is asked again each tick, and on recovery the box publishes under it. `TestABoxThatIsNotAGroupBoxKeepsItsLabelAndKey` compares every other box's keys with `statusTick`'s. |
| 16. **Replaced boxes' history never hides the running ones (T2).** Task 6's `TestStaleReportsDoNotHideLiveBoxes`: 250 day-old reports and two live boxes: both live boxes found, and the reading complete. |
| -17. **No store call outlives its bound, and an unknown outcome is settled by the roster (U1, U2).** Task 5's `TestAStoreThatIgnoresItsContextIsCutOffAtTheHardTimeout`: a store that ignores its context and never answers is cut off at the hard timeout, the outcome unknown, the attempt and its wait unchanged. `TestAnUnknownEnrolmentIsSettledByTheRoster`: an enrolling write that did not land goes on with the same attempt and enrols next pass; one that landed, whether cut off in flight or answering success only after the deadline, is unknown, and the next pass, reading the roster, has nothing left to do: no step and no describe. |
| +17. **No store call is issued past its bound, the console waits no longer than its timeout, and an unknown outcome is settled by the roster (U1, U2, V1, V2).** Task 4's `TestTheBoundReachesTheSDKRequest`: through the console's production stores, each SDK request carries 15 seconds from its call, or the earlier write deadline. Task 5's `TestAWriteTheConsoleGaveUpOnCanCommitLater`, the accepted exposure: a `Put` the console gave up on commits after the next pass found the box absent and refused it on a fresh describe without writing, and the pass after that has nothing to do. Task 5's `TestAStoreThatIgnoresItsContextIsCutOffAtTheHardTimeout`: a store that ignores its context and never answers is cut off at the hard timeout, the outcome unknown, the attempt and its wait unchanged. `TestAnUnknownEnrolmentIsSettledByTheRoster`: an enrolling write that did not land goes on with the same attempt and enrols next pass; one that landed, whether cut off in flight or answering success only after the deadline, is unknown, and the next pass, reading the roster, has nothing left to do: no step and no describe. |
| 18. **host.env takes nothing that could change a box's authority unchecked (runHostEnv conditions, U3).** Task 2's `TestWriteTakesOnlyAllowedNamesAndSafeValues`, `TestHostEnvCommand` (no flag moves the path) and `TestWriteGoesOnlyThroughDirectoriesRootAloneCanWrite` (a group-writable `etc/carlos`, a world-writable `etc`, an `etc/carlos` another user owns, a symlinked `etc/carlos` and a symlinked `etc` each refused with nothing written; a normal write lands 0600, replaces the old file by rename, never in place, and leaves no temporary file); Task 6's `TestSelectModeHoldsAStoreValueTheRosterDisagreesWith`; Task 7's `TestRootJoinTakesNothingFromItsTrigger`. |
| |
| ## Dry run |
| |
| -Every code block below was written into this branch on main `6834659c` (plan 2a merged), built for linux, darwin and windows, vetted for linux and darwin, and its tests run as each task's steps say, with `nice -n 19` and a `-run` filter; the whole `internal/host` suite was never run. The plan was then replayed from a clean base, task by task in order, by applying its own code blocks: every intermediate state built on all three platforms, vetted, passed its task's tests and all three censuses, and the census values here are those runs'. The copy gate was checked without approving anything: with every row pending, `TestRegionHA2bCopyRecordIsComplete`, `TestRegionHA2bCopyHasNoEmDash` and `TestRegionHA2bBuiltCopyIsApplied` pass, and `TestRegionHA2bBuiltCopyIsApproved` fails naming exactly the built ids (21 after Task 2, 66 after Task 9). With a scratch record that marks every row approved, the applied check passes for all 66 rows, and fails for a row whose text was edited. |
| +Every code block below was written into this branch on main `6834659c` (plan 2a merged), built and vetted for linux, darwin and windows, and its tests run as each task's steps say, with `nice -n 19` and a `-run` filter; the whole `internal/host` suite was never run. The plan was then replayed from a clean base, task by task in order, by applying its own code blocks: every intermediate state built on all three platforms, vetted, passed its task's tests and all three censuses, and the census values here are those runs'. The copy gate was checked without approving anything: with every row pending, `TestRegionHA2bCopyRecordIsComplete`, `TestRegionHA2bCopyHasNoEmDash` and `TestRegionHA2bBuiltCopyIsApplied` pass, and `TestRegionHA2bBuiltCopyIsApproved` fails naming exactly the built ids (21 after Task 2, 66 after Task 9). With a scratch record that marks every row approved, the applied check passes for all 66 rows, and fails for a row whose text was edited. |
| |
| -Rounds 1 to 4 were dry-run the same way, each from a clean base for every task. Round 4's mutations were all run through `go test -overlay` from `$TMPDIR`, never editing source in place, and every one of the 51 was caught by the test named. Kept from rounds 1 to 3 and run again on round 4's code (37): the group check and `EnrollAutomatic`'s own read; a failed read counted against the attempt or reported as a non-member; no backoff; enrolment without the guard-ready report; seed overwriting shared values or not reading one back; install-channel not handing its directories over, handing over only the ones it created, or following a symlinked parent; the relaunch hold, or `relaunched` ignoring `pendingTime`; joining marking the box hosting; the bound store never refusing; `Invite` or `enroll` run unbound; a missed deadline counted against the attempt; a group box keeping its label, or every box taken for one; `PrivateBoxReport`, the pass or `checkConfirmed` trusting a chosen key; the reference without the account; `Fetch` without the marker; a log line without the attempt id; the report read under another key; a group box's status tick falling back to its label, or resolving its reference once; the status listing ignoring write times, or keeping history; the bound with no deadline context; a read of exactly 60 seconds taken as fresh, and a store call exactly at the deadline issued; no allowlist; no cross-check against the roster; and root's join reading its trigger. Round 3's three read-back mutations went with the read-back, and its directory-check mutation with `--out`. New in round 4 (14): no hard per-call timeout (`TestAStoreThatIgnoresItsContextIsCutOffAtTheHardTimeout`, which then hangs and times out); a success after the deadline accepted, an unanswered call reported as success, and a call cut off taken at the store's word (`TestAnUnknownEnrolmentIsSettledByTheRoster`, `TestAPutInFlightAtTheDeadlineIsUnknown`); an unknown counted against the attempt, or the attempt forgotten so a fresh one begins with its wait reset (`TestAnUnknownEnrolmentIsSettledByTheRoster`); and for host.env, symlinks followed, `etc` opened unchecked, a group- or world-writable directory accepted, a directory another user owns accepted, the file rewritten in place, the temporary file left behind, `--out` accepted again, and a write that bypasses the held directory (`TestWriteGoesOnlyThroughDirectoriesRootAloneCanWrite`, `TestHostEnvCommand`). The fsyncs are not observable in a test and were not mutated. The claim-authority census reads source with `go/parser`, which an overlay does not reach, so its four mutations overlaid the census test instead, to parse a mutated `enrollpass.go`: with the pass's start removed, its construction duplicated, its invocation removed, or its start duplicated, the census went red each time, and unmutated it stayed green. With a scratch record marking every row approved, the copy gate passed, and went red with RHB-C67's text edited. `internal/hostenv`, `internal/hostactors/...` and `internal/status` passed in full on the final tree, the three timeout and unknown-outcome tests also under `-race`, and the console's enrolment and authority-census tests on their own. The code was then removed, leaving the plan, its copy record and the spec. |
| +Rounds 1 to 5 were dry-run the same way, each from a clean base for every task. Round 4 ran 51 mutations through `go test -overlay` from `$TMPDIR`, never editing source in place, and every one was caught by the test named: 37 kept from rounds 1 to 3 and run again on round 4's code (the group check and `EnrollAutomatic`'s own read; a failed read counted against the attempt or reported as a non-member; no backoff; enrolment without the guard-ready report; seed overwriting shared values or not reading one back; install-channel not handing its directories over, handing over only the ones it created, or following a symlinked parent; the relaunch hold, or `relaunched` ignoring `pendingTime`; joining marking the box hosting; the bound store never refusing; `Invite` or `enroll` run unbound; a missed deadline counted against the attempt; a group box keeping its label, or every box taken for one; `PrivateBoxReport`, the pass or `checkConfirmed` trusting a chosen key; the reference without the account; `Fetch` without the marker; a log line without the attempt id; the report read under another key; a group box's status tick falling back to its label, or resolving its reference once; the status listing ignoring write times, or keeping history; the bound with no deadline context; a read of exactly 60 seconds taken as fresh, and a store call exactly at the deadline issued; no allowlist; no cross-check against the roster; and root's join reading its trigger), and 14 for U1 to U3 (no hard per-call timeout; a success after the deadline accepted; an unanswered call reported as success; a cut-off call taken at the store's word; an unknown counted against the attempt, or the attempt forgotten; and for host.env, symlinks followed, `etc` opened unchecked, a group- or world-writable directory accepted, a directory another user owns accepted, the file rewritten in place, the temporary file left behind, `--out` accepted again, and a write that bypasses the held directory). Round 5 changed only a comment in production code, so those stand; it added 9 mutations, all caught. For V1, `TestAWriteTheConsoleGaveUpOnCanCommitLater` catches a cut-off write reported as success and an absent box enrolled without a fresh describe. For V2, `TestTheBoundReachesTheSDKRequest` catches the bound passing its own context instead of the per-call one, the bound dropping the context, `S3Store.Get` or `S3Store.Put` dropping it, the mutation boundary's `Get` or its `Put` pre-read dropping it, and the conditional record store's `Get` dropping it. The fsyncs are not observable in a test and were not mutated. The claim-authority census reads source with `go/parser`, which an overlay does not reach, so in round 4 its four mutations overlaid the census test instead, to parse a mutated `enrollpass.go`: with the pass's start removed, its construction duplicated, its invocation removed, or its start duplicated, the census went red each time, and unmutated it stayed green; round 5 does not touch that file. With a scratch record marking every row approved, the copy gate passed, and went red with RHB-C67's text edited. `internal/hostenv` and `internal/hostactors/...` passed in full on the final tree, the new tests also under `-race`, and the console's enrolment and authority-census tests on their own. The code was then removed, leaving the plan, its copy record and the spec. |
| |
| --- |
| |
| @@ -1912,18 +1921,18 @@ Expected: PASS once the rows are approved. While any built row is pending, `Test |
| |
| - [ ] **Step 8: Census** |
| |
| -Create the overlay and load it last. `runHostEnv` is a new root-read unit (its `seed` path reads the `--from` file), ruled A,J4 by census ruling region-ha-2b. `writeHostEnv` and `checkedDir` are new in round 4 (their read sites open the root, `etc`, `etc/carlos` and the temporary file); they are proposed A,J4 under `runHostEnv`'s ruling, marked `PROPOSED-region-ha-2b-r4` until platform-1e rules. No legacy row is added, so the seal stands. |
| +Create the overlay and load it last. `runHostEnv` is a new root-read unit (its `seed` path reads the `--from` file), ruled A,J4 by census ruling region-ha-2b. `writeHostEnv` and `checkedDir` are new in round 4 (their read sites open the root, `etc`, `etc/carlos` and the temporary file); platform-1e ruled them A,J4 with `runHostEnv` (`census-ruling-region-ha-2b`, round 5). No legacy row is added, so the seal stands. |
| |
| `internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv`: |
| |
| ````text |
| -# Plan 14 census overlay region-ha-2b (region HA spec 2, plan 2b: docs/superpowers/plans/2026-10-17-region-ha-2b-joining.md), loaded last. RULED by the Plan 14 controller 2026-10-16 as census-ruling-region-ha-2b; the two rows marked PROPOSED-region-ha-2b-r4 (Task 2, review round 4) await its ruling. |
| -# Task 2: host.env from the deployment's parameters (internal/hostenv: hostenv.go, the layout, assembly and rendering, with an allowlist of names, shared values written create-only and the group-box marker added on write; cli.go, carlos system host-env write and seed; write_linux.go, the write through directories held by descriptor, and write_other.go, its refusal off Linux; awsssm.go, the SSM adapter) and the SSM SDK in go.mod. Three new root-read units. runHostEnv: seed's --from file. Ruled authority A,J4 on conditions (no box write to the parameters; allowlisted names, safe values, atomic write, checked parent; authority-selecting values cross-checked against the roster at the agent's use). writeHostEnv and checkedDir (Linux): write's fixed path, /etc/carlos/host.env, reached only through /etc and /etc/carlos opened O_NOFOLLOW|O_DIRECTORY and checked by fstat (root's, writable by no group or other), the file written 0600 in that directory, synced, renamed and the directory synced; proposed A,J4 under runHostEnv's ruling, as the checked parent its condition asks for. The legacy seal stands. |
| +# Plan 14 census overlay region-ha-2b (region HA spec 2, plan 2b: docs/superpowers/plans/2026-10-17-region-ha-2b-joining.md), loaded last. RULED by the Plan 14 controller 2026-10-16 as census-ruling-region-ha-2b. |
| +# Task 2: host.env from the deployment's parameters (internal/hostenv: hostenv.go, the layout, assembly and rendering, with an allowlist of names, shared values written create-only and the group-box marker added on write; cli.go, carlos system host-env write and seed; write_linux.go, the write through directories held by descriptor, and write_other.go, its refusal off Linux; awsssm.go, the SSM adapter) and the SSM SDK in go.mod. Three new root-read units. runHostEnv: seed's --from file. Ruled authority A,J4 on conditions (no box write to the parameters; allowlisted names, safe values, atomic write, checked parent; authority-selecting values cross-checked against the roster at the agent's use). writeHostEnv and checkedDir (Linux): write's fixed path, /etc/carlos/host.env, reached only through /etc and /etc/carlos opened O_NOFOLLOW|O_DIRECTORY and checked by fstat (root's, writable by no group or other), the file written 0600 in that directory, synced, renamed and the directory synced; ruled A,J4 under census-ruling-region-ha-2b, as the checked parent runHostEnv's condition asks for. The legacy seal stands. |
| @coverage . be7c96bb2ff00b5b018b436f976f0d33bbcc8765c17e357baa6cd0290f59f42d |
| @coverage internal/hostenv a3f3274a86cdf8194afb871301c1ba3289dfbdc9b703dd547178273df151cdaa |
| internal/hostenv/cli.go runHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env: write runs as root at a box's start-up and writes /etc/carlos/host.env, 0600, whole (temporary file and rename), from allowlisted parameters with safe values only, refusing a missing required value; its one read site is seed's --from file, an operator's host.env whose values go only to the parameter store; write's path is fixed and reached only through writeHostEnv file:ReadFile@1500 df4470176b229b311927ff0ac4f268d354c3a2357cac4300ac0992d2ebb90899 |
| -internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| -internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| +internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| +internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| ```` |
| |
| `internal/provision/plan14_root_reads_test.go`: |
| @@ -2662,14 +2671,14 @@ Expected: PASS once the rows are approved. While any built row is pending, `Test |
| --- a/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv |
| @@ -1,7 +1,10 @@ |
| - # Plan 14 census overlay region-ha-2b (region HA spec 2, plan 2b: docs/superpowers/plans/2026-10-17-region-ha-2b-joining.md), loaded last. RULED by the Plan 14 controller 2026-10-16 as census-ruling-region-ha-2b; the two rows marked PROPOSED-region-ha-2b-r4 (Task 2, review round 4) await its ruling. |
| - # Task 2: host.env from the deployment's parameters (internal/hostenv: hostenv.go, the layout, assembly and rendering, with an allowlist of names, shared values written create-only and the group-box marker added on write; cli.go, carlos system host-env write and seed; write_linux.go, the write through directories held by descriptor, and write_other.go, its refusal off Linux; awsssm.go, the SSM adapter) and the SSM SDK in go.mod. Three new root-read units. runHostEnv: seed's --from file. Ruled authority A,J4 on conditions (no box write to the parameters; allowlisted names, safe values, atomic write, checked parent; authority-selecting values cross-checked against the roster at the agent's use). writeHostEnv and checkedDir (Linux): write's fixed path, /etc/carlos/host.env, reached only through /etc and /etc/carlos opened O_NOFOLLOW|O_DIRECTORY and checked by fstat (root's, writable by no group or other), the file written 0600 in that directory, synced, renamed and the directory synced; proposed A,J4 under runHostEnv's ruling, as the checked parent its condition asks for. The legacy seal stands. |
| + # Plan 14 census overlay region-ha-2b (region HA spec 2, plan 2b: docs/superpowers/plans/2026-10-17-region-ha-2b-joining.md), loaded last. RULED by the Plan 14 controller 2026-10-16 as census-ruling-region-ha-2b. |
| + # Task 2: host.env from the deployment's parameters (internal/hostenv: hostenv.go, the layout, assembly and rendering, with an allowlist of names, shared values written create-only and the group-box marker added on write; cli.go, carlos system host-env write and seed; write_linux.go, the write through directories held by descriptor, and write_other.go, its refusal off Linux; awsssm.go, the SSM adapter) and the SSM SDK in go.mod. Three new root-read units. runHostEnv: seed's --from file. Ruled authority A,J4 on conditions (no box write to the parameters; allowlisted names, safe values, atomic write, checked parent; authority-selecting values cross-checked against the roster at the agent's use). writeHostEnv and checkedDir (Linux): write's fixed path, /etc/carlos/host.env, reached only through /etc and /etc/carlos opened O_NOFOLLOW|O_DIRECTORY and checked by fstat (root's, writable by no group or other), the file written 0600 in that directory, synced, renamed and the directory synced; ruled A,J4 under census-ruling-region-ha-2b, as the checked parent runHostEnv's condition asks for. The legacy seal stands. |
| +# Task 3: carlos system install-channel (internal/host installchannel.go, installchannel_linux.go): a fresh box installs its signed channel release into both protected slots through verifyPointer, hostfloor.CheckImage and stageCandidate, all unchanged, and on every run hands each directory on the agent's cache path (system, system/router and the version's) to the service user through bootstrap's own descriptor-anchored open, refusing a symlinked parent. One new root-read unit, channelInstall.run, whose one read site opens root's apply lock (systemApplier's lockPath). Ruled authority F,J4. stageCandidate, accepted-legacy, is called and not changed. The legacy seal stands. |
| @coverage . be7c96bb2ff00b5b018b436f976f0d33bbcc8765c17e357baa6cd0290f59f42d |
| @coverage internal/hostenv a3f3274a86cdf8194afb871301c1ba3289dfbdc9b703dd547178273df151cdaa |
| internal/hostenv/cli.go runHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env: write runs as root at a box's start-up and writes /etc/carlos/host.env, 0600, whole (temporary file and rename), from allowlisted parameters with safe values only, refusing a missing required value; its one read site is seed's --from file, an operator's host.env whose values go only to the parameter store; write's path is fixed and reached only through writeHostEnv file:ReadFile@1500 df4470176b229b311927ff0ac4f268d354c3a2357cac4300ac0992d2ebb90899 |
| - internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| - internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| + internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| + internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| +@coverage internal/host 64ee4f561f2a28d584a6f7c82e068e57db4822f6cbb130116284bca583d2d390 |
| +internal/host/installchannel.go channelInstall.run authority F,J4 rev3=census-ruling-region-ha-2b;basis=carlos system install-channel (root, at a fresh box's start-up): its one read site opens root's apply lock; the install goes through verifyPointer, hostfloor.CheckImage and stageCandidate into both protected slots, and refuses a box whose guard is armed or that already runs another release file:OpenFile@196 f0cdb6dd28e83da282c3b0d7bc2012dcd80db181a8cecbf1a2db6bcc41d1d5e3 |
| ```` |
| @@ -2689,7 +2698,7 @@ git commit -m "region HA 2b: carlos system install-channel" |
| ### Task 4: Group membership, read by the console |
| |
| **Files:** |
| -- Create: `internal/hostactors/membershipread.go`, `internal/hostactors/groupref.go`, `internal/hostactors/membershipread_test.go` |
| +- Create: `internal/hostactors/membershipread.go`, `internal/hostactors/groupref.go`, `internal/hostactors/membershipread_test.go`, `internal/hostactors/export_test.go`, `internal/hostactors/boundsdk_test.go` |
| - Modify: `internal/hostactors/enrollment_confirmation.go` (`EnrollAutomatic` and `CheckConfirmed` read membership themselves; the stored confirmation is removed), `internal/hostactors/private.go` (a group box's report under its derived reference) |
| - Modify: `internal/hostactors/attempt_regression_test.go`, `internal/hostactors/roles_test.go` (main's tests, onto a fake reader) |
| - Create: `internal/hostactors/awsmembership/awsmembership.go`, `awsmembership_test.go` |
| @@ -2708,13 +2717,14 @@ Every other admission check stands, unchanged: the attempt in the roster matches |
| |
| The read (C2) is a member only when the instance is in the region's group, `Pending` or `InService`, `pending` or `running`, owned by the region's account and in its AWS region, with a request id from each call. Its anchor, its time, is taken before the call, so never later than the describe's completion. `checkConfirmed` refuses a read that was already 60 seconds old or more when the describe returned (`ErrMembershipStale`, which counts against the attempt); that is the only freshness check besides the bound. A failed call, or an answer that is not one (EC2 does not list the instance, a request id is missing), is `ErrMembershipUnknown`, never `ErrNotMember` (#7). The request ids come back as `MembershipEvidence` whenever the cloud answered, refusals included, for the console's log: main's roster has no field for them, and none is added. |
| |
| -**The write bound: the single enforcement point (S1, T3).** The write deadline is the read's anchor plus 60 seconds, and a write at or after it is refused: the write is issued no later than 60 s after the membership read. Each call issued has a hard timeout of 15 s (U2), so a write that commits late is at most 60 + 15 s behind its read. A write whose result the bound cannot confirm is unknown, and the next pass settles it from the roster. `Invite`, `checkMembership` and `Replace` are sealed and stay unchanged, and so does `Directory.enroll` (round 3 removed the callback round 1 gave it); instead `writeBound` gives them a copy of the `Directory` whose store is a `deadlineStore` and a context that ends at the deadline. What the bound guarantees: |
| +**The write bound: the single enforcement point (S1, T3).** The write deadline is the read's anchor plus 60 seconds, and a write at or after it is refused: the write is issued no later than 60 s after the membership read. Each call issued has a hard timeout of 15 s (U2). What the bound guarantees, and what it does not (V1): every store call of the invite and the enrolling write is issued before the deadline (60 s from the start of the membership describe); the console waits at most 15 s for any call; a write whose result the console did not see is unknown and is settled on a later pass by reading the roster. A cancelled write can still commit later; such an enrolment rests on a membership read that was true when the write was issued, and is the same exposure as an instance detached after enrolment, which spec 3's worker reconciles. `Invite`, `checkMembership` and `Replace` are sealed and stay unchanged, and so does `Directory.enroll` (round 3 removed the callback round 1 gave it); instead `writeBound` gives them a copy of the `Directory` whose store is a `deadlineStore` and a context that ends at the deadline. What the bound guarantees: |
| - no store call (`Get`, `Put`, `List`, `ListDirs`, `Delete`), the roster `Put` included, is issued at or after the deadline, by the read's clock or the context's: `deadlineStore` refuses it before it reaches the store underneath; |
| - every call runs under `deadlineStore.call`, with its own context that ends 15 seconds after it was issued (`storeCallTimeout`) or at the deadline, whichever is first. A call still in flight at the deadline, or still unanswered at its timeout, is cut off: `call` returns `ErrWriteDeadline` itself, without waiting for a store that ignores its context, and cancels the call's context, which the AWS SDK enforces at the HTTP layer; |
| - a call that answers only after the deadline, even with success, is `ErrWriteDeadline` too, so a write whose commit can't be confirmed before the deadline is unknown; |
| -- an unknown write keeps the attempt and its wait, and the next pass reads the roster as every pass does (Task 5, U1). |
| +- an unknown write keeps the attempt and its wait, and the next pass reads the roster as every pass does (Task 5, U1); |
| +- it bounds the console's waiting, not the bucket's commit: a call cut off may still be applied later. Every write it carries is conditional ("Global Constraints: Freshness bound", V4), so such a write lands only on the roster revision it was built against. |
| |
| -The store is wrapped because it can't be trusted to do this: `release.DirStore` ignores its context (`release.S3Store` passes it to the AWS SDK, which does honour it). The pass's S3 store is the console's claim data boundary (`internal/console/claim_runtime.go:42`, `NewConditionalRecordStore`, then `NewObservedMutationBoundary`), whose S3 client the console shares; U2 preferred a dedicated client for the pass, but a second client means a second boundary, with its own census-selected construction and its own mutation observer. The wrapper gives the same hard limit without one: each call's own 15-second context reaches the SDK's HTTP request, and `call` stops waiting at that point whatever the store does. `EnrollAutomatic` runs main's `enroll` on the bound directory. The tests drive a describe of exactly 60 seconds, a clock that reaches the deadline between the check and the roster write, and (Task 5) a store read that answers at the deadline inside the sealed functions, a `Put` still in flight there, a store that ignores its context and never answers, and a `Put` that answers success after the deadline. |
| +The store is wrapped because it can't be trusted to do this: `release.DirStore` ignores its context (`release.S3Store` passes it to the AWS SDK, which does honour it). The pass's S3 store is the console's claim data boundary (`internal/console/claim_runtime.go:42`, `NewConditionalRecordStore`, then `NewObservedMutationBoundary`), whose S3 client the console shares; U2 preferred a dedicated client for the pass, but a second client means a second boundary, with its own census-selected construction and its own mutation observer. The wrapper gives the same hard limit without one: each call's own 15-second context reaches the SDK's HTTP request, and `call` stops waiting at that point whatever the store does. `TestTheBoundReachesTheSDKRequest` (V2) holds that: it builds the console's stores as `claim_runtime.go:33` to `:50` does, the production mutation check included, over `release.NewS3Store` with only the network replaced by a recorder, and asserts every request the SDK sends (a `Get` through the boundary, a `Put`'s pre-read through it, and the S3 store's own `Put`) carries 15 seconds from its call, or the write deadline when that is earlier. `export_test.go` gives the external test `writeBound`; the test is external because `hostops` imports `hostactors`. `EnrollAutomatic` runs main's `enroll` on the bound directory. The tests drive a describe of exactly 60 seconds, a clock that reaches the deadline between the check and the roster write, and (Task 5) a store read that answers at the deadline inside the sealed functions, a `Put` still in flight there, a store that ignores its context and never answers, and a `Put` that answers success after the deadline. |
| |
| **Group references (S3).** `GroupBoxReference` is platform-1e's function: `g-` and the first 20 hex digits of `sha256("carlos-box-ref-v1|" + account + "|" + instance)`. `checkConfirmed` refuses a request whose reference is not the attempt instance's group reference, before the cloud is asked, and `PrivateBoxReport` reads a group box's report under the reference derived from the instance its entry pins, so `checkMembership`'s `VerifyReport`, the census and activation never read a group box's report under a key the box chose. A report another box writes there names its own instance and enrollment, and `VerifyReport` refuses it. Non-group boxes are read exactly as before. `deadlineStore.Put` is a new site in the record-write census (nonrecord). |
| |
| @@ -3022,6 +3032,190 @@ func TestEnrollAutomaticRefusesAReferenceTheBoxChose(t *testing.T) { |
| } |
| ```` |
| |
| +The bound through the console's production stores, down to the SDK request (V2): |
| + |
| +`internal/hostactors/export_test.go`: |
| + |
| +````go |
| +package hostactors |
| + |
| +import ( |
| + "context" |
| + "time" |
| +) |
| + |
| +// BindForTest is writeBound for a membership read anchored at observedAt, |
| +// for the external test that drives the console's production store. |
| +func BindForTest(ctx context.Context, d Directory, observedAt time.Time, now func() time.Time) (Directory, context.Context, context.CancelFunc) { |
| + return writeBound(ctx, d, membershipConfirmation{observedAt: observedAt}, now) |
| +} |
| +```` |
| + |
| +`internal/hostactors/boundsdk_test.go`: |
| + |
| +````go |
| +package hostactors_test |
| + |
| +import ( |
| + "context" |
| + "crypto/ed25519" |
| + "io" |
| + "net/http" |
| + "strings" |
| + "sync" |
| + "testing" |
| + "time" |
| + |
| + awss3 "github.com/aws/aws-sdk-go-v2/service/s3" |
| + |
| + "github.com/carlosframework/platform/internal/hostactors" |
| + "github.com/carlosframework/platform/internal/hostops" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// sdkRequest is one HTTP request the AWS SDK sent: its method, its path, |
| +// and how long its context had left when it was sent (zero: no deadline). |
| +type sdkRequest struct { |
| + method, path string |
| + left time.Duration |
| +} |
| + |
| +// recordingHTTP stands in for the network under the SDK. It records each |
| +// request's context deadline and answers as an empty bucket: a GET is |
| +// NoSuchKey, a PUT succeeds. |
| +type recordingHTTP struct { |
| + mu sync.Mutex |
| + seen []sdkRequest |
| +} |
| + |
| +func (h *recordingHTTP) Do(r *http.Request) (*http.Response, error) { |
| + var left time.Duration |
| + if dl, ok := r.Context().Deadline(); ok { |
| + left = time.Until(dl) |
| + } |
| + h.mu.Lock() |
| + h.seen = append(h.seen, sdkRequest{r.Method, r.URL.Path, left}) |
| + h.mu.Unlock() |
| + if r.Body != nil { |
| + _, _ = io.Copy(io.Discard, r.Body) |
| + } |
| + resp := &http.Response{Header: http.Header{}, Request: r, Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1} |
| + switch r.Method { |
| + case http.MethodPut: |
| + resp.StatusCode = http.StatusOK |
| + resp.Header.Set("ETag", `"e1"`) |
| + resp.Body = io.NopCloser(strings.NewReader("")) |
| + default: |
| + resp.StatusCode = http.StatusNotFound |
| + resp.Header.Set("Content-Type", "application/xml") |
| + resp.Body = io.NopCloser(strings.NewReader(`<?xml version="1.0" encoding="UTF-8"?><Error><Code>NoSuchKey</Code><Message>none</Message></Error>`)) |
| + } |
| + return resp, nil |
| +} |
| + |
| +func (h *recordingHTTP) take() []sdkRequest { |
| + h.mu.Lock() |
| + defer h.mu.Unlock() |
| + out := h.seen |
| + h.seen = nil |
| + return out |
| +} |
| + |
| +// productionStores builds the console's stores as claim_runtime.go does: |
| +// the release S3 store (its own construction, s3compat and all, with only |
| +// the network replaced), the conditional record store over it, and the |
| +// observed mutation boundary with the production mutation check over |
| +// that: the store the enrolment pass's Directory carries. |
| +func productionStores(t *testing.T) (raw *release.S3Store, data release.Store, h *recordingHTTP) { |
| + t.Helper() |
| + t.Setenv("AWS_ACCESS_KEY_ID", "k") |
| + t.Setenv("AWS_SECRET_ACCESS_KEY", "s") |
| + t.Setenv("AWS_SESSION_TOKEN", "") |
| + t.Setenv("AWS_REGION", "eu-west-1") |
| + t.Setenv("AWS_EC2_METADATA_DISABLED", "true") |
| + t.Setenv("AWS_CONFIG_FILE", "/nonexistent") |
| + t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "/nonexistent") |
| + t.Setenv("CARLOS_S3_ENDPOINT", "http://127.0.0.1:9") |
| + raw, err := release.NewS3Store(context.Background(), "deployment") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + h = &recordingHTTP{} |
| + raw.Client = awss3.New(raw.Client.Options(), func(o *awss3.Options) { o.HTTPClient = h; o.RetryMaxAttempts = 1 }) |
| + pub, _, err := ed25519.GenerateKey(nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + control, err := hostops.NewControlWriter(raw, pub, time.Now) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + check, err := hostops.NewProductionMutationCheck(raw, control) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + records, err := hostops.NewConditionalRecordStore(raw, pub) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + data, err = hostops.NewObservedMutationBoundary(true, records, check, nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return raw, data, h |
| +} |
| + |
| +// V2 (platform-1e): the per-call context of a bound write reaches the AWS |
| +// SDK's HTTP request through the console's production stores, so the |
| +// 15-second limit is enforced where the request is sent. Each request |
| +// carries a deadline of 15 seconds from its call, or the write deadline |
| +// when that comes first. A wrapper anywhere on the path that drops the |
| +// context, or a bound that does not give each call its own, fails here. |
| +func TestTheBoundReachesTheSDKRequest(t *testing.T) { |
| + raw, data, h := productionStores(t) |
| + for name, c := range map[string]struct { |
| + readAge time.Duration |
| + low, high time.Duration |
| + }{ |
| + "the per-call limit": {0, 14 * time.Second, 15 * time.Second}, |
| + "the earlier write deadline": {50 * time.Second, 9 * time.Second, 10 * time.Second}, |
| + } { |
| + t.Run(name, func(t *testing.T) { |
| + check := func(what string, got []sdkRequest, method string) { |
| + t.Helper() |
| + saw := false |
| + for _, r := range got { |
| + saw = saw || r.method == method |
| + if r.left <= c.low || r.left > c.high { |
| + t.Errorf("%s: %s %s had %s left, want (%s, %s]", what, r.method, r.path, r.left, c.low, c.high) |
| + } |
| + } |
| + if !saw { |
| + t.Fatalf("%s: no %s reached the SDK: %+v", what, method, got) |
| + } |
| + } |
| + // Through the pass's store: the Directory over the boundary. |
| + d := hostactors.Directory{Store: data, Bucket: "deployment"} |
| + bd, bctx, cancel := hostactors.BindForTest(context.Background(), d, time.Now().Add(-c.readAge), time.Now) |
| + defer cancel() |
| + _, _, _ = bd.Store.Get(bctx, hostactors.Key) |
| + check("Get through the boundary", h.take(), http.MethodGet) |
| + _, _ = bd.Store.Put(bctx, hostactors.Key, []byte("{}"), `"e0"`) |
| + check("Put through the boundary", h.take(), http.MethodGet) |
| + // The S3 store's own Put, the request a roster or invitation |
| + // write ends in. |
| + rd := hostactors.Directory{Store: raw, Bucket: "deployment"} |
| + rbd, rctx, rcancel := hostactors.BindForTest(context.Background(), rd, time.Now().Add(-c.readAge), time.Now) |
| + defer rcancel() |
| + if _, err := rbd.Store.Put(rctx, "control/host-actor-invitations/g-0123456789abcdef0123.json", []byte("{}"), ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + check("the S3 store's Put", h.take(), http.MethodPut) |
| + }) |
| + } |
| +} |
| +```` |
| + |
| Main's two tests that wrote a confirmation object read through a fake reader instead: |
| |
| `internal/hostactors/attempt_regression_test.go`: |
| @@ -3463,9 +3657,9 @@ func writeBound(ctx context.Context, d Directory, c membershipConfirmation, now |
| // storeCallTimeout is the hard limit on one store call of a bound write |
| // (U2): a call the store has not answered within it is unknown, and its |
| // context is cancelled, so an S3 request is abandoned at the HTTP layer. |
| -// With the 60-second issue bound, a write that commits late is at most |
| -// 60 + 15 seconds behind its membership read. A var so tests can shorten |
| -// it. |
| +// It bounds how long the console waits, not when a write commits: a |
| +// call cut off may still commit later, and is settled from the roster on |
| +// a later pass. A var so tests can shorten it. |
| var storeCallTimeout = 15 * time.Second |
| |
| // deadlineStore refuses every call once check says the deadline has been |
| @@ -3966,19 +4160,19 @@ No root-read unit is added or changed: the read is the cloud's, through the read |
| --- a/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv |
| @@ -1,10 +1,13 @@ |
| - # Plan 14 census overlay region-ha-2b (region HA spec 2, plan 2b: docs/superpowers/plans/2026-10-17-region-ha-2b-joining.md), loaded last. RULED by the Plan 14 controller 2026-10-16 as census-ruling-region-ha-2b; the two rows marked PROPOSED-region-ha-2b-r4 (Task 2, review round 4) await its ruling. |
| - # Task 2: host.env from the deployment's parameters (internal/hostenv: hostenv.go, the layout, assembly and rendering, with an allowlist of names, shared values written create-only and the group-box marker added on write; cli.go, carlos system host-env write and seed; write_linux.go, the write through directories held by descriptor, and write_other.go, its refusal off Linux; awsssm.go, the SSM adapter) and the SSM SDK in go.mod. Three new root-read units. runHostEnv: seed's --from file. Ruled authority A,J4 on conditions (no box write to the parameters; allowlisted names, safe values, atomic write, checked parent; authority-selecting values cross-checked against the roster at the agent's use). writeHostEnv and checkedDir (Linux): write's fixed path, /etc/carlos/host.env, reached only through /etc and /etc/carlos opened O_NOFOLLOW|O_DIRECTORY and checked by fstat (root's, writable by no group or other), the file written 0600 in that directory, synced, renamed and the directory synced; proposed A,J4 under runHostEnv's ruling, as the checked parent its condition asks for. The legacy seal stands. |
| + # Plan 14 census overlay region-ha-2b (region HA spec 2, plan 2b: docs/superpowers/plans/2026-10-17-region-ha-2b-joining.md), loaded last. RULED by the Plan 14 controller 2026-10-16 as census-ruling-region-ha-2b. |
| + # Task 2: host.env from the deployment's parameters (internal/hostenv: hostenv.go, the layout, assembly and rendering, with an allowlist of names, shared values written create-only and the group-box marker added on write; cli.go, carlos system host-env write and seed; write_linux.go, the write through directories held by descriptor, and write_other.go, its refusal off Linux; awsssm.go, the SSM adapter) and the SSM SDK in go.mod. Three new root-read units. runHostEnv: seed's --from file. Ruled authority A,J4 on conditions (no box write to the parameters; allowlisted names, safe values, atomic write, checked parent; authority-selecting values cross-checked against the roster at the agent's use). writeHostEnv and checkedDir (Linux): write's fixed path, /etc/carlos/host.env, reached only through /etc and /etc/carlos opened O_NOFOLLOW|O_DIRECTORY and checked by fstat (root's, writable by no group or other), the file written 0600 in that directory, synced, renamed and the directory synced; ruled A,J4 under census-ruling-region-ha-2b, as the checked parent runHostEnv's condition asks for. The legacy seal stands. |
| # Task 3: carlos system install-channel (internal/host installchannel.go, installchannel_linux.go): a fresh box installs its signed channel release into both protected slots through verifyPointer, hostfloor.CheckImage and stageCandidate, all unchanged, and on every run hands each directory on the agent's cache path (system, system/router and the version's) to the service user through bootstrap's own descriptor-anchored open, refusing a symlinked parent. One new root-read unit, channelInstall.run, whose one read site opens root's apply lock (systemApplier's lockPath). Ruled authority F,J4. stageCandidate, accepted-legacy, is called and not changed. The legacy seal stands. |
| -@coverage . be7c96bb2ff00b5b018b436f976f0d33bbcc8765c17e357baa6cd0290f59f42d |
| +# Task 4: group membership, read by the console (internal/hostactors membershipread.go: MembershipReader, the unexported confirmation and the write bound, the single enforcement point of the 60-second deadline, each store call cut off at 15 seconds; enrollment_confirmation.go: EnrollAutomatic and CheckConfirmed make the read themselves and write on the bound store, and the box-written confirmation object is removed; groupref.go: GroupBoxReference; private.go: a group box's report read under the reference the console derives; awsmembership, the Auto Scaling and EC2 describe adapter) and the Auto Scaling SDK in go.mod. No root-read unit is added or changed: the read is the cloud's, through the reader. Envelope-only, and the seal stands. The record-write census gains deadlineStore.Put (nonrecord, ruled). |
| +@coverage . 3678cfe22582d35fe0d8e93d3fed898cdffa6fb2ce2d81e0252c8a8da77993b3 |
| @coverage internal/hostenv a3f3274a86cdf8194afb871301c1ba3289dfbdc9b703dd547178273df151cdaa |
| internal/hostenv/cli.go runHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env: write runs as root at a box's start-up and writes /etc/carlos/host.env, 0600, whole (temporary file and rename), from allowlisted parameters with safe values only, refusing a missing required value; its one read site is seed's --from file, an operator's host.env whose values go only to the parameter store; write's path is fixed and reached only through writeHostEnv file:ReadFile@1500 df4470176b229b311927ff0ac4f268d354c3a2357cac4300ac0992d2ebb90899 |
| - internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| - internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| + internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| + internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| @coverage internal/host 64ee4f561f2a28d584a6f7c82e068e57db4822f6cbb130116284bca583d2d390 |
| internal/host/installchannel.go channelInstall.run authority F,J4 rev3=census-ruling-region-ha-2b;basis=carlos system install-channel (root, at a fresh box's start-up): its one read site opens root's apply lock; the install goes through verifyPointer, hostfloor.CheckImage and stageCandidate into both protected slots, and refuses a box whose guard is armed or that already runs another release file:OpenFile@196 f0cdb6dd28e83da282c3b0d7bc2012dcd80db181a8cecbf1a2db6bcc41d1d5e3 |
| -+@coverage internal/hostactors 4bb1e4b527662d1fe59845dab28fad14215f73f1e69c4701b92da02d824c8de3 |
| ++@coverage internal/hostactors f6dbcea46bed878257c5b34f8caeef83e71cc995cae7b3a4ae59339ed0740c68 |
| +@coverage internal/hostactors/awsmembership a3defad7179245492a3c1dcfce5e20f0f85db2e240b474c654acf211da16df98 |
| ```` |
| |
| @@ -4010,7 +4204,7 @@ git commit -m "region HA 2b: group membership, read by the console" |
| |
| For each box whose identity record is published and which the roster does not list, the pass takes the next step its state allows (§5 step 5, A2-1, H28): with no usable attempt, begin one; with one, read the instance's membership itself (`CheckConfirmed`) and invite on the directory bound to that read's deadline (`writeBound`), so `Invite`'s own read, its signature's `Put` and any read-back of its own are refused at or after the deadline, a call in flight then is cancelled, and one still unanswered 15 seconds after it began is cut off; once invited, wait for the box's guard-ready report to pass `VerifyReport` (the check `membership.go:40` makes at admission), and then enroll through `EnrollAutomatic`, which reads membership again itself and writes on its own bound directory. The same attempt serves the invitation and the enrolment: the read, not the attempt, carries the freshness, and the attempt's `Consumed` refuses a replay. Round 3 removed the check the pass made just before `Invite`: the bound store is the single enforcement point, and nothing that waits sits between the describe and the `Invite` call. |
| |
| -A read that fails is unknown: the step names it, and the next pass asks again with the same attempt, whose wait does not grow (#7). So is a write the bound could not confirm (`ErrWriteDeadline`, S1, U2): nothing was issued at or after the deadline, but a call cut off in flight, or one that answered only after the deadline, may have committed. An invitation that landed is simply found and the box goes on to its guard-ready report: an invitation grants nothing. |
| +A read that fails is unknown: the step names it, and the next pass asks again with the same attempt, whose wait does not grow (#7). So is a write the bound could not confirm (`ErrWriteDeadline`, S1, U2): nothing was issued at or after the deadline, but a call cut off in flight, or one that answered only after the deadline, may have committed, or may still commit later (V1). `TestAWriteTheConsoleGaveUpOnCanCommitLater` documents that exposure: the enrolling `Put` blocks, the pass returns unknown, the instance is detached, the next pass finds the box absent and its fresh describe refuses without writing, then the delayed `Put` commits on the unchanged roster revision, and the pass after that sees the box enrolled and does nothing more. Such an enrolment rests on a membership read that was true when its write was issued: the exposure of an instance detached after enrolment, which spec 3's worker reconciles. An invitation that landed is simply found and the box goes on to its guard-ready report: an invitation grants nothing. |
| |
| **Unknown enrolments are settled by the roster (U1).** An enrolling write whose outcome is unknown is not remembered and not read back. The next pass reads the signed roster, as every pass does, and that settles it: a box the roster lists is enrolled, and the pass has nothing more to do for it (no step, no describe); one it does not list goes on with the same attempt while that attempt is still valid, and its wait does not grow, so the next read and write are made under the same rules as any other. A box whose instance left its group, but still runs, after its enrolment landed keeps that enrolment until an operator removes it; spec 3's regional worker reconciles roster membership against Auto Scaling group membership, including detached-but-running instances ("Activation gates", note). A terminated instance is kept from hosting and from capacity by main's termination fence, and a stopped one by the stale-edges rule and Task 6's restart hold. Only `ErrNotMember` or `ErrMembershipStale` refuses the attempt: the pass keeps it, asking the cloud nothing more, for `Patience`, then twice that, up to 32 times, before it begins another, so the record of a box the group has already replaced costs one roster write and one describe an hour. An invitation resets the wait. The memory of attempts is the console's own and starts afresh at a restart. Every membership read is logged (`Logf`, S5), with the box's reference, its attempt id, its instance id, both request ids ("none" when the call failed before the cloud answered), the read's time and what came of it: the audit main's roster has no field for. Between the final check and the `Invite` call there is nothing that waits: no I/O, channel, lock or store call, only the construction of the bound directory. |
| |
| @@ -4409,7 +4603,9 @@ func TestABoxCannotStandInForAnother(t *testing.T) { |
| // waits for its context as an S3 request cut off by the deadline does |
| // ("cancelled"), ignores its context and never answers ("ignores"), or |
| // answers success only after the console's clock has passed the deadline |
| -// ("late"). |
| +// ("late"). In mode "delayed" it commits nothing until hang is closed, |
| +// whatever its context says, then commits and reports on done: a write |
| +// the console gave up on that the bucket applies later. |
| type inflightStore struct { |
| release.Store |
| key string |
| @@ -4417,11 +4613,18 @@ type inflightStore struct { |
| mode string |
| late func() |
| hang chan struct{} |
| + done chan error |
| } |
| |
| func (s *inflightStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { |
| if s.armed && key == s.key { |
| s.armed = false |
| + if s.mode == "delayed" { |
| + <-s.hang |
| + etag, err := s.Store.Put(ctx, key, body, ifMatch) |
| + s.done <- err |
| + return etag, err |
| + } |
| etag := "" |
| if s.commit { |
| var err error |
| @@ -4447,7 +4650,7 @@ func (s *inflightStore) Put(ctx context.Context, key string, body []byte, ifMatc |
| // membership read takes 59.95 seconds, so its write has 50 milliseconds, |
| // and a late answer comes a second after that. |
| func (w *enrollWorld) inflightAfterRead(key string, commit bool, mode string) *inflightStore { |
| - is := &inflightStore{Store: w.e.Directory.Store, key: key, commit: commit, mode: mode, hang: make(chan struct{}), |
| + is := &inflightStore{Store: w.e.Directory.Store, key: key, commit: commit, mode: mode, hang: make(chan struct{}), done: make(chan error, 1), |
| late: func() { w.clock = w.clock.Add(time.Second) }} |
| w.e.Directory.Store = is |
| w.cloud.during = func() { w.clock = w.clock.Add(59950 * time.Millisecond); is.armed = true } |
| @@ -4555,6 +4758,55 @@ func TestAnUnknownEnrolmentIsSettledByTheRoster(t *testing.T) { |
| } |
| } |
| |
| +// V1, the accepted exposure: the console waits for no write past its |
| +// bound, but a write it gave up on can still commit. Here the enrolling |
| +// Put is issued before the deadline and blocks; the pass returns unknown; |
| +// the instance is detached from its group (still running); the next pass |
| +// finds the box absent from the roster, and its fresh describe refuses |
| +// without writing anything; then the bucket applies the delayed Put, on |
| +// the roster revision it was built against, which nothing has moved. The |
| +// pass after that finds the box enrolled and does nothing more. Such an |
| +// enrolment rests on a membership read that was true when its write was |
| +// issued: the same exposure as an instance detached after enrolment, |
| +// which spec 3's regional worker reconciles. |
| +func TestAWriteTheConsoleGaveUpOnCanCommitLater(t *testing.T) { |
| + w := newEnrollWorld(t, "") |
| + w.pass(t) |
| + if got := w.pass(t); got != "invited" { |
| + t.Fatal(got) |
| + } |
| + inv, _ := w.invitation(t) |
| + w.guardReady(t, inv.Enrollment, true) |
| + before, _ := w.d.Read(context.Background()) |
| + is := w.inflightAfterRead(Key, false, "delayed") |
| + if got := w.pass(t); !strings.Contains(got, "in flight at the deadline") { |
| + t.Fatalf("unknown: %q", got) |
| + } |
| + // Detached: the group no longer lists the instance, which still runs. |
| + w.cloud.during = nil |
| + w.cloud.members["i-new"] = member("") |
| + calls := len(w.cloud.calls) |
| + if got := w.pass(t); !strings.Contains(got, "not confirmed") || len(w.cloud.calls) != calls+1 { |
| + t.Fatalf("absent, then refused: %q, calls %v", got, w.cloud.calls) |
| + } |
| + if o, _ := w.d.Read(context.Background()); o.ETag != before.ETag { |
| + t.Fatalf("the refusing pass wrote the roster: %s, was %s", o.ETag, before.ETag) |
| + } |
| + close(is.hang) |
| + if err := <-is.done; err != nil { |
| + t.Fatalf("the delayed write did not commit: %v", err) |
| + } |
| + calls = len(w.cloud.calls) |
| + steps, err := w.e.EnrollPass(context.Background()) |
| + if err != nil || len(steps) != 0 || len(w.cloud.calls) != calls { |
| + t.Fatalf("enrolled: %+v %v, calls %v", steps, err, w.cloud.calls) |
| + } |
| + o, _ := w.d.Read(context.Background()) |
| + if b := o.Roster.Boxes[newRef]; b.State != "active" || o.Roster.Revision != before.Roster.Revision+1 { |
| + t.Fatalf("roster %+v, revision %d", b, o.Roster.Revision) |
| + } |
| +} |
| + |
| // A refused attempt is kept, with no more reads, for its wait, then begun |
| // again; each refusal doubles the wait, up to 32 times Patience. |
| func TestARefusedAttemptIsBegunAgainWithADoublingWait(t *testing.T) { |
| @@ -4700,7 +4952,7 @@ func TestEnrollPassLogsFailuresSparingly(t *testing.T) { |
| |
| - [ ] **Step 2: Run them to see them fail** |
| |
| -Run: `nice -n 19 go test ./internal/hostactors/ ./internal/console/ -run 'TestAutomaticEnrollment|TestAnInstanceOutside|TestAMembershipReadThatFails|TestAnInvitationNeeds|TestASlowRead|TestAPutInFlight|TestAStoreThatIgnores|TestAnUnknownEnrolment|TestABoxCannotStandIn|TestARefusedAttempt|TestEnrollPass' -count=1` |
| +Run: `nice -n 19 go test ./internal/hostactors/ ./internal/console/ -run 'TestAutomaticEnrollment|TestAnInstanceOutside|TestAMembershipReadThatFails|TestAnInvitationNeeds|TestASlowRead|TestAPutInFlight|TestAStoreThatIgnores|TestAnUnknownEnrolment|TestAWriteTheConsoleGaveUp|TestABoxCannotStandIn|TestARefusedAttempt|TestEnrollPass' -count=1` |
| Expected: FAIL, neither package builds (`undefined: AutoEnroller`, `undefined: newAutoEnroller`). |
| |
| - [ ] **Step 3: Write the pass and start it** |
| @@ -5230,7 +5482,7 @@ func consoleMembershipReader(ctx context.Context) (hostactors.MembershipReader, |
| |
| - [ ] **Step 4: Run the tests** |
| |
| -Run: `nice -n 19 go test ./internal/hostactors/ ./internal/console/ -run 'TestAutomaticEnrollment|TestAnInstanceOutside|TestAMembershipReadThatFails|TestAnInvitationNeeds|TestASlowRead|TestAPutInFlight|TestAStoreThatIgnores|TestAnUnknownEnrolment|TestABoxCannotStandIn|TestARefusedAttempt|TestEnrollPass|TestEnrollAutomatic' -count=1` |
| +Run: `nice -n 19 go test ./internal/hostactors/ ./internal/console/ -run 'TestAutomaticEnrollment|TestAnInstanceOutside|TestAMembershipReadThatFails|TestAnInvitationNeeds|TestASlowRead|TestAPutInFlight|TestAStoreThatIgnores|TestAnUnknownEnrolment|TestAWriteTheConsoleGaveUp|TestABoxCannotStandIn|TestARefusedAttempt|TestEnrollPass|TestEnrollAutomatic' -count=1` |
| Expected: PASS. |
| |
| - [ ] **Step 5: The provider boundary** |
| @@ -5312,7 +5564,7 @@ Expected: PASS. With the selected names added and the sites not yet appended, it |
| --- a/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv |
| @@ -2,6 +2,7 @@ |
| - # Task 2: host.env from the deployment's parameters (internal/hostenv: hostenv.go, the layout, assembly and rendering, with an allowlist of names, shared values written create-only and the group-box marker added on write; cli.go, carlos system host-env write and seed; write_linux.go, the write through directories held by descriptor, and write_other.go, its refusal off Linux; awsssm.go, the SSM adapter) and the SSM SDK in go.mod. Three new root-read units. runHostEnv: seed's --from file. Ruled authority A,J4 on conditions (no box write to the parameters; allowlisted names, safe values, atomic write, checked parent; authority-selecting values cross-checked against the roster at the agent's use). writeHostEnv and checkedDir (Linux): write's fixed path, /etc/carlos/host.env, reached only through /etc and /etc/carlos opened O_NOFOLLOW|O_DIRECTORY and checked by fstat (root's, writable by no group or other), the file written 0600 in that directory, synced, renamed and the directory synced; proposed A,J4 under runHostEnv's ruling, as the checked parent its condition asks for. The legacy seal stands. |
| + # Task 2: host.env from the deployment's parameters (internal/hostenv: hostenv.go, the layout, assembly and rendering, with an allowlist of names, shared values written create-only and the group-box marker added on write; cli.go, carlos system host-env write and seed; write_linux.go, the write through directories held by descriptor, and write_other.go, its refusal off Linux; awsssm.go, the SSM adapter) and the SSM SDK in go.mod. Three new root-read units. runHostEnv: seed's --from file. Ruled authority A,J4 on conditions (no box write to the parameters; allowlisted names, safe values, atomic write, checked parent; authority-selecting values cross-checked against the roster at the agent's use). writeHostEnv and checkedDir (Linux): write's fixed path, /etc/carlos/host.env, reached only through /etc and /etc/carlos opened O_NOFOLLOW|O_DIRECTORY and checked by fstat (root's, writable by no group or other), the file written 0600 in that directory, synced, renamed and the directory synced; ruled A,J4 under census-ruling-region-ha-2b, as the checked parent runHostEnv's condition asks for. The legacy seal stands. |
| # Task 3: carlos system install-channel (internal/host installchannel.go, installchannel_linux.go): a fresh box installs its signed channel release into both protected slots through verifyPointer, hostfloor.CheckImage and stageCandidate, all unchanged, and on every run hands each directory on the agent's cache path (system, system/router and the version's) to the service user through bootstrap's own descriptor-anchored open, refusing a symlinked parent. One new root-read unit, channelInstall.run, whose one read site opens root's apply lock (systemApplier's lockPath). Ruled authority F,J4. stageCandidate, accepted-legacy, is called and not changed. The legacy seal stands. |
| # Task 4: group membership, read by the console (internal/hostactors membershipread.go: MembershipReader, the unexported confirmation and the write bound, the single enforcement point of the 60-second deadline, each store call cut off at 15 seconds; enrollment_confirmation.go: EnrollAutomatic and CheckConfirmed make the read themselves and write on the bound store, and the box-written confirmation object is removed; groupref.go: GroupBoxReference; private.go: a group box's report read under the reference the console derives; awsmembership, the Auto Scaling and EC2 describe adapter) and the Auto Scaling SDK in go.mod. No root-read unit is added or changed: the read is the cloud's, through the reader. Envelope-only, and the seal stands. The record-write census gains deadlineStore.Put (nonrecord, ruled). |
| +# Task 5: the console's automatic enrollment pass (internal/hostactors autoenroll.go: AutoEnroller: an enrolling write the deadline left unknown is settled by the next pass's roster read; internal/console enrollpass.go, started in StartClaimRecoveryPass with region HA on, and awsmembership.go, its membership reader). One new root-read unit, AutoEnroller.inputs, whose two read sites are the signed roster (Directory.Read) and the host floor (hostfloor.Read), in the console. Ruled accepted-legacy (J4 root-input). Invite, BeginEnrollmentAttempt and checkMembership, accepted-legacy, are called and not changed. The seal moves. |
| @@ -5320,11 +5572,11 @@ Expected: PASS. With the selected names added and the sites not yet appended, it |
| @coverage internal/hostenv a3f3274a86cdf8194afb871301c1ba3289dfbdc9b703dd547178273df151cdaa |
| internal/hostenv/cli.go runHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env: write runs as root at a box's start-up and writes /etc/carlos/host.env, 0600, whole (temporary file and rename), from allowlisted parameters with safe values only, refusing a missing required value; its one read site is seed's --from file, an operator's host.env whose values go only to the parameter store; write's path is fixed and reached only through writeHostEnv file:ReadFile@1500 df4470176b229b311927ff0ac4f268d354c3a2357cac4300ac0992d2ebb90899 |
| @@ -9,5 +10,8 @@ |
| - internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| + internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| @coverage internal/host 64ee4f561f2a28d584a6f7c82e068e57db4822f6cbb130116284bca583d2d390 |
| internal/host/installchannel.go channelInstall.run authority F,J4 rev3=census-ruling-region-ha-2b;basis=carlos system install-channel (root, at a fresh box's start-up): its one read site opens root's apply lock; the install goes through verifyPointer, hostfloor.CheckImage and stageCandidate into both protected slots, and refuses a box whose guard is armed or that already runs another release file:OpenFile@196 f0cdb6dd28e83da282c3b0d7bc2012dcd80db181a8cecbf1a2db6bcc41d1d5e3 |
| --@coverage internal/hostactors 4bb1e4b527662d1fe59845dab28fad14215f73f1e69c4701b92da02d824c8de3 |
| -+@coverage internal/hostactors bf971d3b6a02b468d80fc959cc519b05c4976a34823b595cfae228028f5a3296 |
| +-@coverage internal/hostactors f6dbcea46bed878257c5b34f8caeef83e71cc995cae7b3a4ae59339ed0740c68 |
| ++@coverage internal/hostactors a1088ca1b4e6bac902a9d2419c101181860381b7749596f2c3a9392894cee91d |
| @coverage internal/hostactors/awsmembership a3defad7179245492a3c1dcfce5e20f0f85db2e240b474c654acf211da16df98 |
| +@coverage internal/console 80fa5bba8bd23325ababaedc2b8ce810b25161c93a81c6d97dd7033bcc5df103 |
| +@legacy 2f4ca2a51661d8aae1e0f38a41efdcc9df9f1dd75411ca6742d6ee3ae71f1756 internal/hostactors/autoenroll.go AutoEnroller.inputs legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input;census-ruling-region-ha-2b file:Read@102,file:Read@305 c0ae921ee9d8df9de403d488d9708c565d659a7bf880199313f50bdc7c8f8127 |
| @@ -6551,12 +6803,12 @@ Expected: PASS. |
| @coverage . 3678cfe22582d35fe0d8e93d3fed898cdffa6fb2ce2d81e0252c8a8da77993b3 |
| @coverage internal/hostenv a3f3274a86cdf8194afb871301c1ba3289dfbdc9b703dd547178273df151cdaa |
| internal/hostenv/cli.go runHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env: write runs as root at a box's start-up and writes /etc/carlos/host.env, 0600, whole (temporary file and rename), from allowlisted parameters with safe values only, refusing a missing required value; its one read site is seed's --from file, an operator's host.env whose values go only to the parameter store; write's path is fixed and reached only through writeHostEnv file:ReadFile@1500 df4470176b229b311927ff0ac4f268d354c3a2357cac4300ac0992d2ebb90899 |
| - internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| - internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| + internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| + internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| -@coverage internal/host 64ee4f561f2a28d584a6f7c82e068e57db4822f6cbb130116284bca583d2d390 |
| +@coverage internal/host 7f17f1c86771c44641869ff808c4befe2698bbb6ed81c6acbcf1e8cfde570729 |
| internal/host/installchannel.go channelInstall.run authority F,J4 rev3=census-ruling-region-ha-2b;basis=carlos system install-channel (root, at a fresh box's start-up): its one read site opens root's apply lock; the install goes through verifyPointer, hostfloor.CheckImage and stageCandidate into both protected slots, and refuses a box whose guard is armed or that already runs another release file:OpenFile@196 f0cdb6dd28e83da282c3b0d7bc2012dcd80db181a8cecbf1a2db6bcc41d1d5e3 |
| - @coverage internal/hostactors bf971d3b6a02b468d80fc959cc519b05c4976a34823b595cfae228028f5a3296 |
| + @coverage internal/hostactors a1088ca1b4e6bac902a9d2419c101181860381b7749596f2c3a9392894cee91d |
| @coverage internal/hostactors/awsmembership a3defad7179245492a3c1dcfce5e20f0f85db2e240b474c654acf211da16df98 |
| @coverage internal/console 80fa5bba8bd23325ababaedc2b8ce810b25161c93a81c6d97dd7033bcc5df103 |
| @legacy 2f4ca2a51661d8aae1e0f38a41efdcc9df9f1dd75411ca6742d6ee3ae71f1756 internal/hostactors/autoenroll.go AutoEnroller.inputs legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input;census-ruling-region-ha-2b file:Read@102,file:Read@305 c0ae921ee9d8df9de403d488d9708c565d659a7bf880199313f50bdc7c8f8127 |
| @@ -7166,12 +7418,12 @@ The two unit files are new root inputs, which census ruling region-ha-2b is aske |
| @coverage . 3678cfe22582d35fe0d8e93d3fed898cdffa6fb2ce2d81e0252c8a8da77993b3 |
| @coverage internal/hostenv a3f3274a86cdf8194afb871301c1ba3289dfbdc9b703dd547178273df151cdaa |
| internal/hostenv/cli.go runHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env: write runs as root at a box's start-up and writes /etc/carlos/host.env, 0600, whole (temporary file and rename), from allowlisted parameters with safe values only, refusing a missing required value; its one read site is seed's --from file, an operator's host.env whose values go only to the parameter store; write's path is fixed and reached only through writeHostEnv file:ReadFile@1500 df4470176b229b311927ff0ac4f268d354c3a2357cac4300ac0992d2ebb90899 |
| - internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| - internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| + internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| + internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| -@coverage internal/host 7f17f1c86771c44641869ff808c4befe2698bbb6ed81c6acbcf1e8cfde570729 |
| +@coverage internal/host 7650e67baecb9b440c10993539befcb35992e55c48c87c90fbdff687818599f0 |
| internal/host/installchannel.go channelInstall.run authority F,J4 rev3=census-ruling-region-ha-2b;basis=carlos system install-channel (root, at a fresh box's start-up): its one read site opens root's apply lock; the install goes through verifyPointer, hostfloor.CheckImage and stageCandidate into both protected slots, and refuses a box whose guard is armed or that already runs another release file:OpenFile@196 f0cdb6dd28e83da282c3b0d7bc2012dcd80db181a8cecbf1a2db6bcc41d1d5e3 |
| - @coverage internal/hostactors bf971d3b6a02b468d80fc959cc519b05c4976a34823b595cfae228028f5a3296 |
| + @coverage internal/hostactors a1088ca1b4e6bac902a9d2419c101181860381b7749596f2c3a9392894cee91d |
| @coverage internal/hostactors/awsmembership a3defad7179245492a3c1dcfce5e20f0f85db2e240b474c654acf211da16df98 |
| @@ -19,3 +20,6 @@ |
| @coverage internal/status a1eee66fc906d6a7275017fd30c346b64aaafe4f88a501538248e99e9d95c33d |
| @@ -7828,12 +8080,12 @@ No root-read unit changed or was added: `runAgent` is untouched, `RunAgent` and |
| @coverage . 3678cfe22582d35fe0d8e93d3fed898cdffa6fb2ce2d81e0252c8a8da77993b3 |
| @coverage internal/hostenv a3f3274a86cdf8194afb871301c1ba3289dfbdc9b703dd547178273df151cdaa |
| internal/hostenv/cli.go runHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env: write runs as root at a box's start-up and writes /etc/carlos/host.env, 0600, whole (temporary file and rename), from allowlisted parameters with safe values only, refusing a missing required value; its one read site is seed's --from file, an operator's host.env whose values go only to the parameter store; write's path is fixed and reached only through writeHostEnv file:ReadFile@1500 df4470176b229b311927ff0ac4f268d354c3a2357cac4300ac0992d2ebb90899 |
| - internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| - internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| + internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| + internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |
| -@coverage internal/host 7650e67baecb9b440c10993539befcb35992e55c48c87c90fbdff687818599f0 |
| +@coverage internal/host 43a8c2fe1403991e94c9254c9c08b3fd71dc16e5dc8225356d96042765e42022 |
| internal/host/installchannel.go channelInstall.run authority F,J4 rev3=census-ruling-region-ha-2b;basis=carlos system install-channel (root, at a fresh box's start-up): its one read site opens root's apply lock; the install goes through verifyPointer, hostfloor.CheckImage and stageCandidate into both protected slots, and refuses a box whose guard is armed or that already runs another release file:OpenFile@196 f0cdb6dd28e83da282c3b0d7bc2012dcd80db181a8cecbf1a2db6bcc41d1d5e3 |
| - @coverage internal/hostactors bf971d3b6a02b468d80fc959cc519b05c4976a34823b595cfae228028f5a3296 |
| + @coverage internal/hostactors a1088ca1b4e6bac902a9d2419c101181860381b7749596f2c3a9392894cee91d |
| @coverage internal/hostactors/awsmembership a3defad7179245492a3c1dcfce5e20f0f85db2e240b474c654acf211da16df98 |
| ```` |
| |
| @@ -8037,7 +8289,7 @@ Expected: PASS once the rows are approved. While any built row is pending, `Test |
| +@coverage . 374a07016556167813b331508ee60f696872c43a09ba10904b6f3d949e0b70a6 |
| @coverage internal/hostenv a3f3274a86cdf8194afb871301c1ba3289dfbdc9b703dd547178273df151cdaa |
| internal/hostenv/cli.go runHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env: write runs as root at a box's start-up and writes /etc/carlos/host.env, 0600, whole (temporary file and rename), from allowlisted parameters with safe values only, refusing a missing required value; its one read site is seed's --from file, an operator's host.env whose values go only to the parameter store; write's path is fixed and reached only through writeHostEnv file:ReadFile@1500 df4470176b229b311927ff0ac4f268d354c3a2357cac4300ac0992d2ebb90899 |
| - internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=PROPOSED-region-ha-2b-r4;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| + internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| @@ -24,3 +25,5 @@ |
| @coverage internal/host/units 18a7bb1fe57b2044517e7acd3f97780beae35fd55e695c593f87a4ae7f3af27d |
| internal/host/units/carlos-system-join.path external authority F,J4 rev3=census-ruling-region-ha-2b;basis=systemd (PID 1) starts carlos-system-join.service when the joining agent touches /data/system-join-requested; the file is only a trigger: root reads nothing from it and takes no parameter from it file:external@0 8af1bea945a0ec5a475b766e6b55d7168bf6eff1bf963bd5c7d46f90130f4c5c |
| @@ -8069,7 +8321,7 @@ for os in linux darwin; do GOOS=$os nice -n 19 go vet . ./internal/host/ ./inter |
| ```` |
| |
| - [ ] Run once, on the final tree: `nice -n 19 go test ./internal/hostenv/ ./internal/hostactors/... -count=1`, the console's enrolment, capacity and authority-census tests with a `-run` filter, and the targeted `internal/host` tests of Tasks 3, 6, 7 and 8. Never the whole `internal/host` suite; CI's `make test` is that run. |
| -- [ ] Put the census overlay's rows and the three claim-authority sites in front of platform-1e as census ruling region-ha-2b (the table above), recomputed on the build base; get its ruling on the two rows marked `PROPOSED-region-ha-2b-r4` (`writeHostEnv`, `checkedDir`), and record it in the overlay's first line, as 2a's overlay does. |
| +- [ ] Put the census overlay's rows and the three claim-authority sites in front of platform-1e as census ruling region-ha-2b (the table above), recomputed on the build base, and record its ruling in the overlay's first line, as 2a's overlay does. |
| - [ ] Delete `$TMPDIR/census` by exact path. |
| |
| ## 2c's seams |
| @@ -8103,7 +8355,7 @@ What 2c builds, and exactly what 2b needs from it: |
| |
| 1. **Who writes a region's group into the roster?** Answered (2026-10-16): a `region-group` phase of `carlos system claims run`, in a later, separate plan built on `feat/claim-activation-tools` once that branch merges. It takes the group's name and ARN from tofu's outputs (2c) and writes them, with the region's account, AWS region, namespaces and pools, in one signed roster write. 2b does not write it. Until it lands, no region has a group, the pass begins no attempt, and automatic enrolment stays dormant even with region HA on. |
| 2. **Namespace and pool.** The pass enrolls a group box into the console's own namespace and the `default` pool, and refuses a region that does not serve both. Is that right for every carloku.com region, or does a region's group need its own pool? |
| -3. **Census classes.** Ruled (census-ruling-region-ha-2b), but for round 4's two new units: A,J4 is proposed for `writeHostEnv` and `checkedDir`, under `runHostEnv`'s ruling. The classes in the census table (A,J4 for `runHostEnv`, F,J4 for `channelInstall.run` and the join units, accepted-legacy for `AutoEnroller.inputs`, the re-sealed `statusLoop` row, nonrecord for `deadlineStore.Put`, reviewed sites for the claim-authority census) follow the nearest precedents; platform-1e may class them otherwise. In particular `runHostEnv` has no close precedent: it writes the root-owned host.env every unit reads. |
| +3. **Census classes.** Ruled (census-ruling-region-ha-2b), round 4's `writeHostEnv` and `checkedDir` included (A,J4, round 5). The classes in the census table (A,J4 for `runHostEnv`, F,J4 for `channelInstall.run` and the join units, accepted-legacy for `AutoEnroller.inputs`, the re-sealed `statusLoop` row, nonrecord for `deadlineStore.Put`, reviewed sites for the claim-authority census) follow the nearest precedents; platform-1e may class them otherwise. In particular `runHostEnv` has no close precedent: it writes the root-owned host.env every unit reads. |
| 4. **Box references outside the label space.** Answered and built (round 2). platform-1e approved the `statusLoop` change round 1 stopped at, as a re-sealed `@legacy` row under census ruling region-ha-2b, on four conditions, all met: only group boxes take the override; the reference is `g-` and 20 hex digits of `sha256("carlos-box-ref-v1|" + account + "|" + instance)`, a pseudonym in the public feed; the console derives the key itself from the pinned instance (`PrivateBoxReport`, the pass, `checkConfirmed`) and never trusts the box's; and the four tests (Review Focus 13). See "Global Constraints: Box references" and Task 6. Automatic enrolment stays off for a region until a release with it is rolled there ("Activation gates", 1). |
| 5. **`DescribeAutoScalingGroups`.** Not needed: the console's read binds to the group name and ARN the signed roster names. Spec rev 16 drops it from §4. |
| 6. **The invitation's confirmation.** Resolved by H28: the console reads membership itself immediately before the invitation and again before the enrolment, so the same attempt serves both and no fresh attempt is begun after the guard-ready report. Spec rev 16's §5 says so. |