carlos / platform Public

spec: region HA spec 1 rev 5.2, Astra rev 5 round 2 (B2-1 to B2-6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 693f4a059eeff9fb9acc51c986e17b4f1485b2b8 parent 26674ee
1 file changed, +227 −65
  • docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md +227 −65
diff --git a/docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md b/docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md
index 184b425ff..2c590675a 100644
--- a/docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md
+++ b/docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md
@@ -1,8 +1,21 @@
# Region HA, spec 1: placing an instance on a box
-Design, 2026-10-06, **rev 5.1**. DRAFT. Plan 1a is built and merged
+Design, 2026-10-06, **rev 5.2**. DRAFT. Plan 1a is built and merged
(aadb860); nothing else here is built.
+**Rev 5.2** answers Astra's second review (`CODEX-FINDINGS-REGION-HA-REV5-2.md`,
+cited **B2-n**): four P1 and two P2, all accepted; §21 maps each, with
+the round 1 items it marked partly resolved. The volume guard now
+covers root-volume replacement, snapshots and images (B2-1), and Auto
+Scaling's own service-linked role, through the APIs that start its work
+(B2-3). A hold is closed by revoking the change role's sessions and
+waiting for in-flight disk tasks before the validating record (B2-2).
+The files manifest is pinned from the newest admitted generation, not
+from the database baseline's (B2-4). The local install receipt is bound
+to the installation stamp, and a box that has admitted a writer never
+reinstalls (B2-5). H17's confirmer may treat an enrolled box as gone on
+a positive `terminated` read (B2-6).
+
**Rev 5.1** answers Astra's first review of rev 5
(`CODEX-FINDINGS-REGION-HA-REV5-1.md`, cited **B1-n**): five P1, three P2
and one P3, all accepted; §20 maps each. A restarted probe now restores
@@ -319,9 +332,9 @@ the group name from the deployment's configuration. Anything else, a
fixed box included, is enrolled only with an operator's approval. Spec 4
builds this; the claim's D7 needs to allow it (L19).
-**A region with no enrolled box (H17).** When the roster shows no
-`active` box in the target region, the confirmer may be any `active`
-enrolled box of another region, acting as a regional worker of its own
+**A region with no enrolled box (H17).** When the target region has no
+**live** enrolled box, the confirmer may be any `active` enrolled box of
+another region, acting as a regional worker of its own
region and checking its own entry first. This is the only thing it ever
does across regions. Its confirmation binds to the target region's
group, not to anything the new box says:
@@ -335,6 +348,19 @@ group, not to anything the new box says:
zone in that AWS region; then `DescribeInstances` there, requiring the
instance `pending` or `running`, the deployment's `OwnerId` and the
`carlos:role` tag;
+- **the region is empty (B2-6).** The roster entry of a dead box stays
+ `active` until a fence retires it, and only a regional worker can write
+ the fence, so the roster alone would never show the region empty. The
+ confirmer therefore describes, in the same region and the same pass,
+ every other enrollment the roster lists there as `active` or
+ `quarantined`, and requires each to read **`terminated`**, positively,
+ for its roster `cloud`. `InvalidInstanceID.NotFound`, any other state,
+ an error or a partial reply refuses the confirmation (absence is never
+ evidence; a box that has left `DescribeInstances` goes to an operator).
+ The confirmation lists those reads (`terminated_enrollments: [{box,
+ enrollment, instance_id, observed_at, request_id}]`). They only show
+ the region has no live worker. They are not a termination fence, they
+ discharge nothing, and they never retire a roster entry;
- it writes the same create-only object,
`control/box-enrollment/<box reference>/<instance id>.json`, with
`{target_placement, aws_region, group_arn, instance_id,
@@ -343,12 +369,14 @@ group, not to anything the new box says:
write: one creates it, the other gets a refusal, reads it, and stops.
The console accepts a cross-region confirmation only if, at enrollment,
-the roster still shows no `active` box in the target region, the
-confirmer was `active`, the group ARN equals the roster's for that
+every enrollment the roster lists as `active` or `quarantined` in the
+target region appears in the confirmation's `terminated_enrollments`
+(any new one refuses), the confirmer was `active`, the group ARN equals the roster's for that
placement, and the `boxid` record's region equals `aws_region`. Once
enrolled, the new box is the region's worker: it writes its own volume
-records, fences its predecessor and recovers the hosts (§5.9, §5.8). So
-a one-box region recovers automatically (§7).
+records, fences its predecessor through the ordinary §5.9 path (its own
+terminated read and the predecessor's last volume record), and recovers
+the hosts (§5.8). So a one-box region recovers automatically (§7).
A box whose entry is not `active` with a proven `cloud` is never
eligible, and a claim naming one is never relocated automatically: an
@@ -567,8 +595,14 @@ stands.
- **Each cold admission starts a new manifest.** Under P6-D4 every wake
is a cold admission into a new generation. Before the app starts, the
admitting box writes `files/<new generation>/manifest.json`
- create-only, as a copy of the predecessor generation's manifest read by
- exact version from the same baseline it restores. Its entries keep
+ create-only, as a copy of the **files pin**: the newest manifest in the
+ admitted chain, read by exact version (B2-4). That is the manifest of
+ `holder.generation` as observed, or, if that generation has none (its
+ copy never completed, so its app never started), of the nearest
+ predecessor in `holder.admissions` that has one. It is chosen
+ independently of the database baseline: the files publisher and the
+ replicator make progress separately, so a generation can hold
+ published files before its first database upload. Its entries keep
naming the predecessor's objects where they are, so an unchanged file
is never uploaded again. A late write from the predecessor's publisher
lands in the predecessor's prefix, which nothing admitted reads again.
@@ -576,7 +610,11 @@ stands.
after a day" is withdrawn (plan 1a never built it). A dead generation's
`files/<generation>/` prefix is retired with that generation, by the
`park-generation` chore `PublishGeneration` queues, except objects that
- a manifest in the live set still names (L13). The at-rest sweep treats
+ a manifest in the live set still names (L13). The **files live set** is
+ every manifest from the files pin through `holder.generation`, plus any
+ manifest a running operation has journaled in its pin, and every
+ object they name. A park that runs while an operation is in progress
+ keeps all of them. The at-rest sweep treats
`files/` as part of the location, never as residue to classify
blindly (claim P6-D3).
@@ -662,7 +700,7 @@ replication.
| `pinning` | a worker CAS citing the receipt or the fence: `op.exec: none`, a non-installing transition that keeps the stamp (claim R6-4, D5's form). Then it selects and journals the **pinned recovery set** (below) | `none` | unchanged | `from` held (if alive); `to` held | nobody |
| `installing` | a worker CAS that journals the pin and a fresh prepared-generation id `P`, hands `op.exec` to `to`, and writes `holder.stamp` = this CAS's `gen` (R6-4: a new installation), adding `to` to `touched.boxes`. `to` activates that stamp under the host guard (claim §6.4), writes its inventory entry, **builds `P`** and makes its **local install** (below) | `to` | `S'` | `to` held; `from` held | nobody: `to` writes local state under the guard, starts no app and admits no writer |
| `installed` | a worker CAS once `P`'s install manifest is complete and verified, journaling `P`'s prepared authority (claim P6-D3). The executor then journals its **local install receipt** by CAS, if this CAS did not already carry it (a replacement makes its own, below) | `to` | `S'` | as `installing` | nobody: the executor may make its local install, nothing else |
-| `probing` | `to` enters it by CAS, which requires a local install receipt for exactly `op.exec`, its enrollment and `holder.stamp`; then each probe attempt is one cold `AdmitWriter` (below), then the probe (moves §7.7, same region), within the probe deadline | `to` | `S'`, kept by the first admission | as `installing` | `to`, writer, probe only |
+| `probing` | `to` enters it by CAS, which requires a local install receipt for exactly `op.exec`, its enrollment and `op.journal.install_stamp`; then each probe attempt is one cold `AdmitWriter` (below), then the probe (moves §7.7, same region), within the probe deadline | `to` | `S'`, kept by the first admission | as `installing` | `to`, writer, probe only |
| `probe-failed` | `to` runs the release sequence for its own session | `to` until evidence | unchanged | as `installing` | nobody |
| `serving` | a worker CAS back to `serving` with `participants: [to]`; `from` gets a `park-local` chore unless it is discharged under H10 (§5.9) | `to` | `S'` | `to` ready; `from` held until its chore retires the row | `to` |
@@ -713,8 +751,11 @@ The journal entry is `{"released_by": "<box>", "generation": "<G>",
`holder.generation`, the unfinished run in `holder.admissions` and the
marker, all in this region's bucket (claim §6.2a, §6.4a item 3). It
journals the plan's exact object versions (the snapshot and every LTX
-file after it, contiguous) and the exact version of the files manifest
-of the same generation, files and timers included.
+file after it, contiguous), and, chosen separately, the exact version of
+the **files pin** (§5.6: the newest manifest in the admitted chain, not
+the database baseline's generation), with files and timers included.
+The two can come from different generations; each is the newest
+durable state of its own publisher (B2-4).
- **After a release sequence** (drain, rebalance, probe failure): the set
must reach the journaled `final_txid` and `manifest_seq`. If the listing
@@ -761,13 +802,28 @@ activated:
as the relocation's inputs (P6-D3);
- journals each step locally, and then, last, records its **local
install receipt** in the op journal by CAS:
- `local_install: {box, enrollment, stamp, files_manifest_version,
- journal_sha256}`.
-
-A receipt counts only for the box, enrollment and stamp it names, so a
-replacement executor (new stamp, below) never inherits its
-predecessor's. Remaking the local install is not a reinstall: `P` is not
-touched.
+ `local_install: {box, enrollment, install_stamp,
+ files_manifest_version, journal_sha256}`.
+
+**`install_stamp`** is the stamp of the CAS that handed this box
+`op.exec` (`installing`, or a replacement's), journaled by that CAS as
+`op.journal.install_stamp`. It is the installation's provenance, not the
+current writer's stamp: later probe admissions take new stamps (§6.4a
+item 4) and leave it alone (B2-5). A receipt counts only when its box,
+enrollment and `install_stamp` equal `op.exec`, its enrollment and
+`op.journal.install_stamp`, so a replacement executor never inherits its
+predecessor's. Remaking the local install before the first admission is
+not a reinstall: `P` is not touched.
+
+**After the first admission, the local install is never remade
+(B2-5).** Once any admission entry names `P` (or a later generation of
+this op), the box's local files, env and wake state are the live state
+of that writer lineage. A new agent on the same box stops the app and
+admits a new session, as for any cold wake; it never reapplies `P`'s
+files or timers. A box that finds its receipt missing after that point
+has lost local state it cannot rebuild from `P`: the op goes to `stuck`,
+and the operator resolves it (or a fence makes it a nested failover
+from `holder.generation` and the files pin).
**The probe's admission.** Each probe attempt is one `AdmitWriter`
(claim §6.4a), made by `to` as `op.exec`, bound to the observed `op.id`
@@ -829,8 +885,9 @@ admitted reads again, and which publication retires.
**The no-reinstall boundary (A1-3).** Once the `installed` CAS succeeds:
- nobody builds `P` again: replay from `installed` or `probing` goes
- forward (local install if the executor has no receipt, probe, then
- `serving`), never back to `installing`;
+ forward (the local install only if the executor has no receipt and no
+ admission has named `P`; then the probe, then `serving`), never back to
+ `installing`;
- `P` is opened at most once. After its first admission, every restart
takes a fresh generation from its predecessor;
- a worker or `to` that paused and resumes revalidates the phase under
@@ -957,8 +1014,8 @@ record is never more than about an hour old:
never itself termination evidence.
- A record showing a `false` volume on a running box alerts at once:
that box can never be fenced automatically until the volume is fixed.
-- The record also lists the instance's tags as described, so the fence
- can check the guard's tag (§6).
+- The record also lists the instance's and each volume's tags as
+ described, so the fence can check the guard's tag on all of them (§6).
**Suspending disk evidence (B1-3).** A record's age bound cannot cover an
operator's change made just before a termination, and a detached volume
@@ -969,22 +1026,48 @@ accounted for:
- **Before.** The only principal the guard exempts is the volume-change
role (§6). `carlos ops box volumes change <box>` first writes a
create-only hold, `control/box-volumes/<enrollment>/hold-<id>.json`
- (`{id, operator, reason, at}`), and only then assumes that role with
- the session tag `carlos:volume-hold = <id>`, which the role's trust
- policy requires. A box with a hold that has no later release is never
- fenced automatically: its termination goes to the operator path.
+ (`{id, operator, reason, instance_id, at}`), and only then assumes that
+ role with the session tags `carlos:volume-hold = <id>` and
+ `carlos:instance = <instance id>`, for at most one hour. The role's
+ trust policy requires both tags, and the guard's exemption covers only
+ requests on the instance the tag names (§6). Session tags are the
+ caller's own word, so they do not prove a hold exists: the operator
+ workflow is what writes the hold first, and an operator who bypasses
+ it is outside this design's trust, as an operator who attests falsely
+ under claim D2 is. A box with a hold that has no later release is
+ never fenced automatically: its termination goes to the operator
+ path.
- **Removed volumes.** Whenever a worker's describe of a running box
- shows a volume that the previous record listed and this one does not,
- the new record carries `removed_volumes`, and that also suspends disk
+ shows a volume that the previous record listed and this one does not
+ (a detach, or a root volume replaced by a new volume id), the new
+ record carries `removed_volumes`, and that also suspends disk
evidence, hold or no hold: the volume may survive with the box's data.
-- **After.** `carlos ops box volumes release <box> <hold id>` resumes it
- only when a worker's running record written after the hold shows every
- attached volume `true`, and, for every volume removed since the
- enrollment's last release, the operator attests (as in claim D2) that
- it was destroyed or never held host artifacts. The CLI writes
- `release-<id>.json` create-only, naming the record and each
- attestation. The fence then needs a running record newer than the
- release.
+- **After (B2-2).** `carlos ops box volumes release <box> <hold id>`
+ closes the change in this order, and resumes disk evidence only at the
+ end:
+ 1. **No further requests.** It revokes every session of the
+ volume-change role issued before now (an IAM deny on
+ `aws:TokenIssueTime` earlier than the revocation, AWS's "revoke
+ active sessions"), and records the revocation time `R` in a
+ create-only `closing-<id>.json`. From `R` no credential the hold's
+ change obtained can sign a request AWS accepts;
+ 2. **Nothing in flight.** It waits until a worker's describe after `R`
+ shows no volume of the instance `attaching` or `detaching`,
+ `DescribeReplaceRootVolumeTasks` shows no task for it `pending` or
+ `in-progress`, and no snapshot or image of its volumes is `pending`.
+ Requests signed before `R` are bounded by S3 and EC2's 15-minute
+ signature age, so it also waits until `R + 15 minutes`;
+ 3. **A validating record after the barrier.** A worker's running record
+ observed after both points shows every attached volume `true`;
+ 4. **Removed volumes accounted for.** For every volume removed since the
+ enrollment's last release (including a replaced root volume, and
+ any snapshot or image taken), the operator attests (as in claim D2)
+ that it was destroyed or never held host artifacts.
+
+ The CLI then writes `release-<id>.json` create-only, naming the
+ closing record, the validating record and each attestation. The fence
+ needs a running record newer than the release. A release missing any
+ step is invalid, and the hold stays open.
A hold or removal therefore never leaves a window: from the hold's write
until a release, the box cannot be fenced automatically, whatever its
@@ -1200,29 +1283,76 @@ operations.
- **The volume guard (H15, B1-4).** An Organizations service control
policy on the deployment's account denies, to every principal whose
ARN is not the deployment's **volume-change role**:
- - `ec2:ModifyInstanceAttribute`, `ec2:AttachVolume` and
- `ec2:DetachVolume` on instances tagged
- `aws:ResourceTag/carlos:role = platform-box`. `DetachVolume` is there
- because a volume detached from a running box survives termination
- with the box's data, which is exactly what the fence must rule out;
- - `ec2:CreateTags` and `ec2:DeleteTags` on any instance when
+ - `ec2:ModifyInstanceAttribute`, `ec2:AttachVolume`,
+ `ec2:DetachVolume` and `ec2:CreateReplaceRootVolumeTask` on instances
+ tagged `aws:ResourceTag/carlos:role = platform-box`, and on volumes
+ tagged the same (the launch template tags each box's volumes too).
+ `DetachVolume` is there because a volume detached from a running box
+ survives termination with the box's data, which is exactly what the
+ fence must rule out. `CreateReplaceRootVolumeTask` is there for the
+ same reason: it replaces a running box's root disk and by default
+ keeps the old volume (B2-1);
+ - `ec2:CreateSnapshot`, `ec2:CreateSnapshots` and `ec2:CreateImage` on
+ those instances and volumes: a snapshot or image is a copy of the
+ disk that outlives it just as a kept volume does. No Data Lifecycle
+ Manager or AWS Backup policy may select them (checked by the
+ attestation);
+ - `ec2:CreateTags` and `ec2:DeleteTags` on any instance or volume when
`aws:TagKeys` includes `carlos:role`, except `CreateTags` with
- `ec2:CreateAction = RunInstances`, so a launch can tag a new box but
- nobody can strip, add or change the tag on a running one.
+ `ec2:CreateAction = RunInstances`, so a launch can tag a new box and
+ its volumes but nobody can strip, add or change the tag on a running
+ one or its disks.
+
+ - `autoscaling:StartInstanceRefresh`, `autoscaling:UpdateAutoScalingGroup`,
+ `autoscaling:CreateAutoScalingGroup`, `autoscaling:PutWarmPool` and
+ `autoscaling:AttachInstances` on the deployment's platform groups:
+ these are the requests that start Auto Scaling's own disk and tag
+ work (B2-3, below).
The volume-change role is assumable only by operators, and its trust
- policy requires the session tag `carlos:volume-hold` (§5.9's hold).
- Every other operator role is denied these actions like everyone else.
+ policy requires the session tags `carlos:volume-hold` and
+ `carlos:instance` (§5.9's hold). Its exemption from the EC2 statements
+ applies only where the resource is that instance or its volumes. Every
+ other operator role is denied these actions like everyone else.
+
+ **Auto Scaling's service-linked role (B2-3).** No SCP binds a
+ service-linked role, and `AWSServiceRoleForAutoScaling`'s managed
+ policy (`AutoScalingServiceRolePolicy`) includes
+ `ec2:ModifyInstanceAttribute`, `ec2:CreateReplaceRootVolumeTask` and
+ tag changes, which cannot be edited away. So the guard does not claim
+ that no service-linked role can touch a disk. It constrains what makes
+ the service act. The service role acts on a group's instances only on
+ behalf of that group's configuration and of requests to it: launches
+ and terminations, an instance refresh (whose `ReplaceRootVolume`
+ strategy replaces root disks), warm pools (which stop instances with
+ their disks), and attaching existing instances. Every one of those is
+ started by an Auto Scaling API in the list above, denied to all but
+ the volume-change role; a launch template change only affects new
+ instances, whose volume records then show what they got. Spec 4 must
+ not use a warm pool or a `ReplaceRootVolume` refresh for enrolled
+ boxes; an operator who must, does it under a hold.
**Coverage is attested, not assumed.** An SCP does not bind the
Organizations management account or service-linked roles. The
operator workflow therefore verifies, with operator credentials, that
- the deployment's account is a member account, that the policy is
- attached with exactly these statements, and that no service-linked
- role in the account can modify, attach or detach volumes on running
- instances, and writes a root-signed **guard attestation**,
- `control/volume-guard.json` (`{account, policy_id, policy_sha256,
- checked_at, operator}`). The fence requires a valid one for the box's
+ the deployment's account is a member account and that the policy is
+ attached with exactly these statements. It then lists every
+ service-linked role in the account and reads each one's managed policy
+ **at its current default version**. For every action in them that can
+ change, copy or replace a disk, its `DeleteOnTermination`, or the
+ `carlos:role` tag, it requires an entry in a reviewed table naming the
+ initiating API that makes the service use it, and that API must be one
+ the SCP denies (as above for Auto Scaling). An action with no entry,
+ or a service-linked role the table does not know, fails the
+ attestation. No Data Lifecycle Manager or AWS Backup policy may select
+ platform boxes or their volumes. It writes a root-signed **guard
+ attestation**, `control/volume-guard.json` (`{account, policy_id,
+ policy_sha256, service_roles: [{role, policy_arn, default_version,
+ document_sha256}], checked_at, operator}`). A service-linked policy
+ whose default version changes after the attestation makes it invalid
+ until rechecked; the regional workers' sweep reads the versions
+ (`iam:GetRole` and `iam:GetPolicy`, read-only, on those roles and
+ policies only) and alerts on a change. The fence requires a valid one for the box's
account (§5.9). An account with no Organizations, or none attested,
has **no automatic fence**: every termination there goes to the
operator path. This is H15's condition (2) unmet, not a weaker
@@ -1290,7 +1420,10 @@ operations.
| Box alive but failing its health check | the group terminates it, then as above. Instances that were healthy lose up to H3's window, declared by `writes_lost`. |
| A terminated box had a volume without `DeleteOnTermination` | no automatic fence: `awaiting-evidence`, an alert, and the operator's `--declare-lost` (§5.9). The running box's record already alerted. |
| A terminated box's newest volume record is missing or over 2 hours old | no automatic fence: the operator path (§5.9, H15). |
-| An operator changes a box's volumes, or a volume vanishes from a running box's mapping | disk evidence is suspended from the hold (or the record showing the removal) until a release; a termination meanwhile goes to the operator (§5.9, B1-3). |
+| An operator changes a box's volumes, or a volume vanishes from a running box's mapping | disk evidence is suspended from the hold (or the record showing the removal) until a release, which first revokes the change role's sessions and waits out in-flight disk tasks; a termination meanwhile goes to the operator (§5.9, B1-3, B2-2). |
+| A root volume is replaced, or a snapshot or image is taken, outside a hold | refused by the guard; Auto Scaling cannot be asked to do it either (§6, B2-1, B2-3). |
+| An Auto Scaling service-linked policy gains a new disk action | the attestation is invalid until reviewed; no automatic fence meanwhile (§6, B2-3). |
+| A region's only box dies and its roster entry is still `active` | the cross-region confirmer reads it `terminated` and confirms the replacement; the replacement fences it (§5.1, B2-6). |
| The account has no attested volume guard | no automatic fence anywhere in it (§6, B1-4). |
| A participant's execution lease is lost and its cleanup fails | its controller record stays; root refuses executable updates on that box until the host is resolved (§5.10, B1-5). |
| The source dies while a drain is in `starting` | the op becomes a failover in place (§5.8, B1-6). |
@@ -1349,8 +1482,10 @@ operations.
`available`. The fence therefore reads volumes from the running-time
record (§5.9). The rehearsal repeats this with the record and the
volume guard in place, and checks that an attach, a detach, a flag
- change and a removal of the `carlos:role` tag by a non-operator role
- and by an ordinary operator role are refused, and that the
+ change, a root-volume replacement that keeps the old volume, a
+ snapshot, an image, an instance refresh and a removal of the
+ `carlos:role` tag by a non-operator role and by an ordinary operator
+ role are refused, and that the
volume-change role cannot be assumed without a hold tag.
The rehearsal also enrolls a box into a dev region with no `active`
box, confirmed from another region (H17), and checks that the
@@ -1367,8 +1502,8 @@ the text.
| # | Amends | Change | Status |
|---|---|---|---|
-| L1 | claim §4, §8, §6.4a (which kinds and phases admit), §7.3 D5 and D6 | New op kind `relocating` (§5.8): same region, stateful, from `serving`; its phase table with `op.exec`, stamp, rows and admission per phase; the `installed` no-reinstall boundary for the remote build, with a per-box local install receipt required before `probing`; probe attempts after the first restoring from their predecessor; the replacement table; the source-loss conversion to failover from `starting` or `releasing`; two replacements then `stuck`; the release sequence. Recovery owner `region:<r>`, any regional worker (P6-D5). The D6 barrier needs only `to` when the start cites a termination fence or after a source-loss CAS. Excludes moves, placements and pushes. | Ruled in direction (H10; #236: "a new op kind, or an equivalent that the claim prefers") |
-| L2 | claim §4, C5, §7.3 D2 | A termination fence (§5.9) is a lost-box fence, written by a regional worker with no person in the loop: `terminated` only (by event, describe or, later, the group's history), bound to the roster entry's `cloud`, with a running-time volume record no more than 2 hours old showing every EBS volume `DeleteOnTermination` and the guard's tag, no unreleased hold or removal, and an attested volume guard (no `ModifyInstanceAttribute`, `AttachVolume`, `DetachVolume` or `carlos:role` tag change on platform boxes except by the hold-gated volume-change role). A `shutting-down` read is never evidence. Stopped, unclear and vanished boxes keep D2's operator path unchanged. | Ruled (H10, H15) |
+| L1 | claim §4, §8, §6.4a (which kinds and phases admit), §7.3 D5 and D6 | New op kind `relocating` (§5.8): same region, stateful, from `serving`; its phase table with `op.exec`, stamp, rows and admission per phase; the `installed` no-reinstall boundary for the remote build, with a per-box local install receipt, bound to the stamp of the CAS that handed the box `op.exec`, required before `probing` and never remade after the first admission; probe attempts after the first restoring from their predecessor; the replacement table; the source-loss conversion to failover from `starting` or `releasing`; two replacements then `stuck`; the release sequence. Recovery owner `region:<r>`, any regional worker (P6-D5). The D6 barrier needs only `to` when the start cites a termination fence or after a source-loss CAS. Excludes moves, placements and pushes. | Ruled in direction (H10; #236: "a new op kind, or an equivalent that the claim prefers") |
+| L2 | claim §4, C5, §7.3 D2 | A termination fence (§5.9) is a lost-box fence, written by a regional worker with no person in the loop: `terminated` only (by event, describe or, later, the group's history), bound to the roster entry's `cloud`, with a running-time volume record no more than 2 hours old showing every EBS volume `DeleteOnTermination` and the guard's tag, no unreleased hold or removal, and an attested volume guard (no `ModifyInstanceAttribute`, `AttachVolume`, `DetachVolume`, `CreateReplaceRootVolumeTask`, snapshot, image, Auto Scaling refresh or warm pool, or `carlos:role` tag change on platform boxes except by the hold-gated volume-change role; service-linked roles covered through the APIs that start them). A hold is released only after revoking the role's sessions and waiting out in-flight disk tasks. A `shutting-down` read is never evidence. Stopped, unclear and vanished boxes keep D2's operator path unchanged. | Ruled (H10, H15) |
| L3 | claim §7.3 D5 | A fenced terminated box's `park-local` chores are discharged in the fence CAS: H10's volume check is D5's destroyed-local proof. Regional chores move to `region:<r>` as D5 says. | Ruled (H10) |
| L4 | claim §7.3 D3 and D4, §5.9 P5-D3 | The fence record `control/box-fences/<enrollment>.json`, create-only, box-written, cited by `evidence_refs`. A terminated box goes on `lost` with that reference and no quarantine revision; it needs no readmission. The console later copies it into a D4 loss record and retires the roster entry. | Ruled in direction (H10, H1); record format for the claim session |
| L5 | claim §10.1 D7 (required actors, console enrollments) | An enrolled box with a valid fence record is excluded from the census without waiting for the console to retire it. A console enrollment gains `compute: {box, enrollment}`, checked against its reports; a console whose `compute` box is fenced is excluded the same way (B1-7). | Ruled (H10, H14; #236: failover works when the console's box died) |
@@ -1379,13 +1514,13 @@ the text.
| L10 | claim §6.1, §6.6 P6-D1, `internal/stopevidence` | A writers-stopped check for release step 1: cgroup empty, no member processes, and database holders limited to the agent's own identified replicator descriptors. `Collect` stays unchanged for step 4. | Requested |
| L11 | claim §4 (CC11), `hostclaim.StartGuard` | A `park-generation` chore in the region does not block a `relocating` start there: relocation writes only its own new prepared generation, never one being retired. Under P6-D4 nearly every recently woken host has one, which would hold failover up to the park horizon. | Requested |
| L12 | claim C4, §8 (CC10), §6.6 P6-D2; moves §9.6 | Reconcile's row cases for `relocating` (§5.3): held participant rows; a former executor's row kept held until its `park-local` retires it; the 10-minute sweep as a stated exception to C4 for kept rows (H9). | Requested |
-| L13 | claim C3, §6.2a, §6.6 P6-D3 | The files publisher is a second ordinary-replication exception, bound to `{location, generation, session}`. `files/<generation>/` belongs to that generation for the classifier; `park-generation` retires it except objects a live manifest names. | Requested |
+| L13 | claim C3, §6.2a, §6.6 P6-D3 | The files publisher is a second ordinary-replication exception, bound to `{location, generation, session}`. `files/<generation>/` belongs to that generation for the classifier; `park-generation` retires it except objects named by the files live set: every manifest from the files pin through `holder.generation`, and any manifest a running operation's pin journals (B2-4). | Requested |
| L14 | moves §7.10 | A target terminated mid-move may be taken off the move by the termination fence, as M16's evidence, automatically, under the same before-commit rules. Moves decides what the move does then; the placer only supplies the fence. | Ruled (H10) |
| L15 | moves §7.1, §7.3, §19.1 | A move into a multi-box region names its target **box** in the start CAS, chosen by `choose()`. Readiness, evidence, install and recovery bind to that box; replacing it is an explicit fenced transition (A1-10). | Ruled (H12) |
| L16 | moves §5.1, §7.6, P6-D3's provider | Relocation's install reuses the landing's `building` and `published` phases and the prepared-generation authority, with a pinned set of remote versions as the source in place of a staged replica. One shared routine, so both get the same anchor and manifest rules. | Requested |
| L17 | moves §6.1, §8, §9.4 | Every installation, a move's landing included, writes a files manifest in its new generation (§5.6), so unchanged files stay recoverable after the next failure; the archive at the cut stays. Timer freezing and draining apply to `relocating` holds. | Requested |
| L18 | joint model, moves §14.2 | `internal/sim/hostlife` extends to two boxes per region: create spread, relocation by drain, termination mid-serve and in every relocation phase, nested failover, a stale edges file, an unenrolled reference, a volume without `DeleteOnTermination`, a crash mid-install, late file uploads and late PUTs after termination. | Requested |
-| L19 | claim §10.1 D7 (enrollment), with P3 | An Auto Scaling box is enrolled by the console with no operator when its `boxid` evidence verifies (deployment account, the region's AWS region) and a regional worker's create-only confirmation shows it a current member of the region's group (§5.1). Every other enrollment stays operator-approved. Spec 4 builds it. For a region with no `active` box, any `active` box of another region confirms, by the same create-only write, bound to the group ARN the roster records for that placement; its cross-region rights are the two read-only describes and nothing else (H17). | Ruled (H16, H17) |
+| L19 | claim §10.1 D7 (enrollment), with P3 | An Auto Scaling box is enrolled by the console with no operator when its `boxid` evidence verifies (deployment account, the region's AWS region) and a regional worker's create-only confirmation shows it a current member of the region's group (§5.1). Every other enrollment stays operator-approved. Spec 4 builds it. For a region with no live enrolled box (every enrollment there read `terminated`, positively, in the same pass, B2-6), any `active` box of another region confirms, by the same create-only write, bound to the group ARN the roster records for that placement; its cross-region rights are the two read-only describes and nothing else (H17). | Ruled (H16, H17) |
| L20 | claim §10.1 D7 (required actors, console reports) | For box-started relocations only, a console's newest report counts however old if it met every floor check and the console's `compute` box has reported its D1 root update guard armed with the same floor digest. Every other census rule is unchanged. | Ruled (H18) |
**Rev 4.2's requests, mapped.** K1 is L8. K2 is L1, with L9 to L11. K3
@@ -1426,8 +1561,12 @@ rev 4.2's table; this mapping says where each now lives.
`removed_volumes` when a volume leaves the mapping); cross-region
confirmation (accepted only when the target region has no `active`
box, refused for another group, an instance not `Pending`/`InService`,
- a confirmer not `active`, a region mismatch, and a second confirmer
- losing the create-only race); the console census for a box-started
+ a confirmer not `active`, a region mismatch, a second confirmer
+ losing the create-only race, and an emptied region whose dead box is
+ still `active`: confirmed on a `terminated` read, refused on NotFound
+ or `stopped`, B2-6); a hold released while a detach is in flight, or
+ before `R + 15 minutes`, refused (B2-2); the attestation failing on an
+ unknown service-linked disk action (B2-3); the console census for a box-started
relocation (a stale report accepted with an armed guard, refused with
no `compute` binding, no armed-guard report or a floor mismatch, and
unchanged for a console-started operation); continuous files with a
@@ -1444,7 +1583,11 @@ rev 4.2's table; this mapping says where each now lives.
any other holder fails step 1); a failover admission declaring
`writes_lost` for `from`'s session and a drain declaring nothing; the
start refused when the roster reserves an update for `from` or `to`;
- probe attempts one, two and three, with attempt three restoring
+ a files update published by a probe session before its first database
+ upload, then termination: the files pin keeps it while the database
+ baseline falls back (B2-4); a crash during `probing` after the second
+ admission: no reinstall, and the receipt still valid by
+ `install_stamp` (B2-5); probe attempts one, two and three, with attempt three restoring
attempt two's replicated writes and declaring attempt two's session
(B1-1); a target terminated at `installed` replaced by one that makes
its own local install before probing, and a `probing` CAS refused with
@@ -1685,3 +1828,22 @@ before acceptance.
| B1-7 P2 a dead console stays in the D7 census | accepted: claim D7 does require every console. Console enrollments gain a `compute` binding; a console on a fenced box is excluded. A live console with a stale report was ruled as H18 (§5.9, L5, L20) |
| B1-8 P2 automatic enrollment cannot bootstrap an emptied region | accepted: §2 makes a confirmer an active enrolled box in the region, so none is left. ruled as H17: an enrolled box in another region confirms, with read-only cross-region describes only, so one-box regions recover automatically (§5.1, §6, §7, L19) |
| B1-9 P3 stale code citations | accepted, checked on main: `provision.go:49`, `reconcile.go:348`/`:820`, `:192`, `:215`, `activator.go:1164-1168`, `:2204`, `edge.go:2466`, `main.tf:521`. Disposition tables §13 to §17 keep their historical anchors |
+
+## 21. Astra rev 5 round 2 dispositions
+
+Reviewed rev 5.1 at fdc0fe07 (`CODEX-FINDINGS-REGION-HA-REV5-2.md`).
+Each finding was checked against claim 98b45332, moves a4711728, main
+and AWS's documented API and policy contracts before acceptance.
+
+| Finding | Disposition |
+|---|---|
+| B1-2 partly resolved: the receipt dies with the next probe stamp | answered by B2-5 |
+| B1-3 partly resolved: release can precede authorized mutations | answered by B2-2 |
+| B1-4 partly resolved: root replacement unguarded; the attestation conflicts with Auto Scaling | answered by B2-1 and B2-3 |
+| B1-8 partly resolved: H17's emptiness test is circular | answered by B2-6 |
+| B2-1 P1 `CreateReplaceRootVolumeTask` bypasses the guard | accepted: added to the deny, with snapshots and images (which also leave a copy of the disk), the hold workflow and the rehearsal; a replaced root volume shows as removed (§5.9, §6, §8, L2) |
+| B2-2 P1 a release does not close the change's authority | accepted: the hold names its instance and the role's exemption is limited to it; release revokes the role's earlier sessions, waits for no attaching, detaching, replace-root, snapshot or image task and for the signature-age bound, then needs a validating record after that barrier. Session tags are acknowledged as the caller's word; the hold-first order is the operator trust boundary, as in claim D2 (§5.9) |
+| B2-3 P1 Auto Scaling's service-linked role defeats the attestation | accepted: the attestation no longer asserts that no service role can touch a disk. It reads every service-linked role's managed policy at its default version and requires each disk or tag action to map to an initiating API the SCP denies; Auto Scaling's (refresh, group create and update, warm pool, attach) are denied but for the hold-gated role; a policy version change invalidates it (§6, §7) |
+| B2-4 P1 the files pin follows the database baseline and can drop published files | accepted: the files pin is the newest manifest in the admitted chain, chosen independently; the files live set covers it through `holder.generation` and any running op's pin (§5.6, §5.8, L13) |
+| B2-5 P2 later probe stamps invalidate the receipt and invite a reinstall | accepted: the receipt binds to `op.journal.install_stamp`, the stamp of the CAS that handed `op.exec`; after the first admission the local install is never remade, and a lost receipt is `stuck` (§5.8, L1) |
+| B2-6 P2 the dead box stays `active`, so H17 never fires | accepted: the confirmer requires a positive `terminated` read for every `active` or `quarantined` enrollment in the region, in the same pass, as part of confirming membership; it is not a fence and retires nothing. The new box then fences through the ordinary path (§5.1, §7, L19) |