carlos / platform Public

spec: region HA spec 1 rev 5, Paul's rulings H14-H16 (relocation starts, volume records, enrollment)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 2494b05eb5a5dba41dccdeefb10439f1782fbe4e parent 805ea66
1 file changed, +253 −103
  • docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md +253 −103
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 17ece865c..ec08eea19 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
@@ -7,7 +7,7 @@ Design, 2026-10-06, **rev 5**. DRAFT. Plan 1a is built and merged
stands, rev 6.25 (413b12cb on `design-replica-ownership`), with its
library on main (`internal/hostclaim`, `internal/hostguard`,
`internal/stopevidence`, `internal/park`, `internal/genselect`), and
-records Paul's rulings H10 to H13 of 2026-10-06. Rev 4.2 was written
+records Paul's rulings H10 to H16 of 2026-10-06. Rev 4.2 was written
against claim rev 6.3 and asked for changes K1 to K11. Those sessions
never saw the request, and the claim moved on: box references are
opaque, `control/host-actors.json` is the one roster, every writer is
@@ -19,9 +19,14 @@ records as authority (H11), the trusted local copy (H13), and its own
it had, and installs through the claim's prepared-generation and
`AdmitWriter` rules instead. The termination fence stays automatic, now
also requiring every EBS volume to be deleted with the instance (H10).
-§9's K-table is replaced by requests L1 to L18 against claim rev 6.25
-and moves rev 7.17. §19 says what of plan 1a is superseded. §11 lists
-three points the claim's current text leaves for Paul.
+A dev test showed a terminated instance's describe lists no volumes, so
+that proof comes from a record taken while the box runs, kept true by
+IAM (H15). A relocation is started without a console: the boxes, not a
+roster reservation, keep it apart from executable updates (H14, §5.10).
+§9's K-table is replaced by requests L1 to L19 against claim rev 6.25
+and moves rev 7.17. §19 says what of plan 1a is superseded. The three
+points rev 5 first left open were ruled the same day as H14 to H16
+(§11).
Rev 4.2 answered Astra round 5 (`CODEX-FINDINGS-REGION-HA-5.md`, A5-1),
and Astra round 6 (`CODEX-FINDINGS-REGION-HA-6.md`) found it ready for
@@ -79,10 +84,15 @@ are alive, and on #212 option B for box references.
| H11 | **(2026-10-06) The roster is the claim's `control/host-actors.json`** (claim §10.1 D7). Region HA drops its own `control/boxes/<id>.json` authority. `internal/boxid`'s AWS-signed IMDS identity verification becomes the evidence required to enroll a box. Enrollment records the proven account, region and instance id in the roster entry's `cloud` field, and the termination fence binds to that entry. Box references are opaque (claim §2, rev 6.16) and follow #212 option B: the label is the key, the instance id is private. |
| H12 | **(2026-10-06) Executor choice at create** is spec 1's `choose()`: prefer a box running no instance of the app; then the most headroom (until spec 2, the fewest claimed hosts); then the claim's P5-D2 SHA-256 of `host + "\x00" + box reference` as the final tie-break. It is recorded once, in the create intent. Recovery never substitutes. |
| H13 | **(2026-10-06) Claim ruling R6 supersedes HA3 and §5.5.** A box never reuses its local database. Every admission is cold until claim P6-D4 lifts. HA3 and §5.5 are deleted, and the activator half of plan 1a's install marker is not used. |
+| H14 | **(2026-10-06) Relocation starts need no console reservation (L6).** A relocation start requires that no executable update is reserved for `from` or `to` in `control/host-actors.json`, which the worker reads and never writes. Root on each box refuses to start an executable update while that box is a participant in a running claim operation. The update side's refusal is what closes the race with a reservation made after the start's read. (§5.10.) |
+| H15 | **(2026-10-06) H10's volume check rests on a running-time record, not the terminated read.** Tested on dev 2026-10-06: while `shutting-down`, `DescribeInstances` still listed both volumes of a test instance with their `DeleteOnTermination` flags; once `terminated`, `BlockDeviceMappings` was empty, the `true` root volume was already gone (`InvalidVolume.NotFound`) and the `false` volume survived as `available`. So the fence rests on (1) a record of the box's block device mappings taken by regional workers while it runs, refreshed on change, kept with its roster-bound evidence, showing every attached EBS volume `DeleteOnTermination: true`; (2) IAM denying `ec2:ModifyInstanceAttribute` and `ec2:AttachVolume` on platform boxes to every principal except operator roles, so the record cannot go stale behind the platform's back; (3) the terminated read (describe, event, later the group's history). A `shutting-down` read that still lists mappings may refresh the record but is never itself evidence. A missing record, one older than the stated bound, or one with any `false` volume sends the box to the operator path. (§5.9, §6.) |
+| H16 | **(2026-10-06) Auto Scaling boxes are enrolled automatically, with evidence (spec 4, L19).** The console enrolls an Auto Scaling box itself when its `boxid` evidence verifies (the deployment's account, the region's AWS region) and a regional worker confirms, with read-only Auto Scaling rights, that the instance is a current member of that region's group. Anything else still needs an operator's approval. |
-H10 to H13 supersede only what they name: H10 adds the volume condition
-to H1, H11 replaces §5.1's box records as authority, H13 replaces HA3
-and §5.5. Everything else in H1 to H9 stands.
+H10 to H16 supersede only what they name: H10 adds the volume condition
+to H1 and H15 says where it is read from, H11 replaces §5.1's box records
+as authority, H13 replaces HA3 and §5.5, H14 replaces claim D7's console
+reservation for relocation starts, and H16 settles enrollment for spec 4.
+Everything else in H1 to H9 stands.
Standing rulings this spec keeps:
@@ -274,6 +284,18 @@ Enrollment is the claim's (D7, L7). For region HA it must:
the operator-approved enrollment (claim §10.1 P3), so the separate bind
object is not used.
+**Auto Scaling boxes (H16, spec 4).** The console enrolls a box launched
+by the region's Auto Scaling group itself, with no operator, when two
+pieces of evidence agree: its `boxid` record verifies with the
+deployment's account and the region's AWS region, and a regional worker
+has written a create-only membership confirmation,
+`control/box-enrollment/<box reference>/<instance id>.json`, from a fresh
+read-only `autoscaling:DescribeAutoScalingInstances` showing the instance
+a current member (`Pending` or `InService`) of that region's group, with
+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 box whose entry is not `active` with a proven `cloud` is never
eligible, and a claim naming one is never relocated automatically: an
operator resolves it.
@@ -556,13 +578,16 @@ executor change the claim's `placing` refuses (P5-D5).
other effect (claim C2):
{from, to, to_enrollment, region, reason, stamp_from,
- generation_from, fence?}
+ generation_from, fence?, directory_revision}
`reason` is `drain`, `rebalance` or `failover`. For failover, `fence`
names the termination fence record (§5.9) by key and SHA-256. The start
is refused by the claim's start guard (claim §4, CC11, as amended by
-L11), and when `to` is not eligible for the host (§2). Participants are
-`from` and `to`, or `to` alone for a failover.
+L11), when `to` is not eligible for the host (§2), and when the roster
+shows an executable update reserved for `from` or `to` (§5.10, H14).
+`directory_revision` is the roster revision that check read. No console
+reservation is taken. Participants are `from` and `to`, or `to` alone
+for a failover.
**Who acts.** Every phase is a claim CAS conditional on `op.id`, the
phase and the ETag. The recovery owner for every phase is the region's
@@ -779,19 +804,63 @@ detection plus restore (spec 3).
### 5.9 The termination fence
-**What counts (A1-1, A1-11, H10).** A box is fenced by termination only
-when one fresh `DescribeInstances` reply, made by a regional worker in
-the box's AWS region with its own read-only rights, shows all of:
+**What counts (A1-1, A1-11, H10, H15).** A box is fenced by termination
+only when all three of these hold:
+
+1. **A volume record.** The newest **volume record** taken while the
+ instance was `running` (below) is no more than **2 hours** older than
+ the terminated observation, names the instance in the roster entry's
+ `cloud`, and lists at least one EBS volume. It, and every record
+ written after it, shows every attached EBS volume
+ `DeleteOnTermination: true`.
+2. **The IAM guard is in place** (§6): nobody but an operator role can
+ change a platform box's block device mapping or attach a volume to it,
+ so the record cannot go stale behind the platform's back.
+3. **A terminated read.** A regional worker, with its own read-only
+ rights in the box's AWS region, observes state exactly `terminated`
+ for the instance id, account (`OwnerId`) and region in the roster
+ entry's `cloud` (H11, HA7), by one of the three sources below.
+
+The terminated read itself never carries the volumes. On dev on
+2026-10-06 (§8 step 5), a terminated instance's `BlockDeviceMappings`
+was empty, its `DeleteOnTermination: true` root volume was already gone
+(`InvalidVolume.NotFound`), and its `false` volume survived as
+`available`. That is why the proof is the running-time record.
+
+If the newest record is missing, is older than the bound, or shows any
+volume `false`, the box is not fenced automatically; the operator path
+applies.
+
+**The volume record.** Regional workers describe each enrolled box in
+their region while it runs: at enrollment, on every 10-minute sweep, and
+at once when a describe shows `shutting-down`. A worker writes a new
+record, create-only, whenever the mapping differs from the newest
+record, and otherwise at least once an hour, so a healthy box's newest
+record is never more than about an hour old:
+
+ control/box-volumes/<enrollment>/<observed_at>.json
+
+```json
+{"v": 1, "box": "<box reference>", "enrollment": "<enrollment>",
+ "cloud": {"provider": "aws", "account": "123…",
+ "region": "eu-west-1", "instance_id": "i-0abc…"},
+ "instance_state": "running",
+ "block_devices": [{"device": "/dev/xvda", "volume_id": "vol-…",
+ "delete_on_termination": true, "status": "attached"}],
+ "observed_at": "<RFC3339>", "request_id": "<EC2 request id>",
+ "written_by": {"box": "<reference>", "enrollment": "<enrollment>"}}
+```
-- the instance id, account (`OwnerId`) and region equal to the box's
- roster entry's `cloud` (H11, HA7);
-- state exactly `terminated`;
-- every EBS volume in `BlockDeviceMappings` with `DeleteOnTermination:
- true`, and at least one mapping present.
+- A record is bound to the roster entry: its `cloud` must equal the
+ entry's, and a record for another instance is ignored.
+- A **`shutting-down`** read that still lists mappings may write a fresh
+ record, with `instance_state: "shutting-down"`. If it shows any volume
+ `false`, that record is the newest and the fence is refused. It is
+ 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.
-If any volume was not `DeleteOnTermination`, or the reply carries no
-mappings, the box is not fenced automatically; the operator path
-applies (§11 question 2 is about the second case).
+**What never counts.**
- `stopped` and `stopping` do not count, because a stopped box can start
again with its disk. A missing instance, `InvalidInstanceID.NotFound`,
@@ -814,11 +883,12 @@ enrollment, create-only (L4):
"cloud": {"provider": "aws", "account": "123…",
"region": "eu-west-1", "instance_id": "i-0abc…"},
"state": "terminated",
- "block_devices": [{"device": "/dev/xvda", "volume_id": "vol-…",
- "delete_on_termination": true}],
- "observed_at": "<RFC3339>", "request_id": "<EC2 request id>",
- "trigger": "describe" | "event" | "activity",
- "event_id": "<EventBridge event id, when trigger=event>",
+ "volume_record": {"key": "control/box-volumes/<enrollment>/<…>.json",
+ "sha256": "…"},
+ "observed_at": "<RFC3339>",
+ "via": "describe" | "event" | "activity",
+ "request_id": "<EC2 request id, when via=describe>",
+ "event_id": "<EventBridge event id, when via=event>",
"written_by": {"box": "<reference>", "enrollment": "<enrollment>"}}
```
@@ -827,27 +897,31 @@ worker's authenticated report of a cloud read, made with rights the
console does not hold. Every relocation and claim CAS that relies on it
cites its key and SHA-256 (`evidence_refs`, claim P5-D3).
-**Three triggers, one proof.** The record always comes from a describe,
-because only a describe carries `BlockDeviceMappings`. The other two
-sources say when to look:
+**Three ways to observe `terminated`.** Since the volumes come from the
+record, any of these is the terminated read:
1. **The durable event.** An EventBridge rule sends EC2 "Instance
State-change Notification" events with `state: terminated` for the
deployment's account to an SQS queue (14-day retention). Plan 1a
built one queue per box. The queue's policy accepts messages only from
that rule's ARN. An event whose account, region and instance id match
- a roster entry makes the worker describe the instance at once.
-2. **A direct read.** A worker describes the region's enrolled instances
- on its sweep.
+ a roster entry's `cloud` is the read, `via: "event"`.
+2. **A direct read.** `DescribeInstances` for the instance id, in the
+ region, returning `terminated` with the deployment's `OwnerId`,
+ `via: "describe"`. Workers make it on their sweep and whenever an
+ event or the group's history suggests a termination.
3. **The group's own history.** For a box launched by an Auto Scaling
- group, a `DescribeScalingActivities` activity terminating that
- instance id with status `Successful` makes the worker describe it.
- Spec 4 verifies this source on dev; until then it is off.
+ group, `DescribeScalingActivities` for the region's group: an activity
+ terminating that instance id with status `Successful`,
+ `via: "activity"`. Spec 4 verifies on dev that `Successful` is
+ recorded only once the instance has reached `terminated`; until then
+ this source is off.
**What is not guaranteed (A2-4).** EC2 delivers state-change events to
EventBridge on a best-effort basis, and a terminated instance leaves
-`DescribeInstances` after about an hour. When no describe returns the
-proof, there is **no automatic recovery**. The placer marks the box's
+`DescribeInstances` after about an hour. When no source shows
+`terminated`, or the volume record does not qualify, there is **no
+automatic recovery**. The placer marks the box's
hosts `awaiting-evidence`, alerts at once, and keeps retrying. The only
way out is the operator's `--declare-lost` (claim §7.3 D2). A CloudTrail
`TerminateInstances` record is **not** evidence (A3-1): it proves a
@@ -868,7 +942,7 @@ alerting, and owns the loop that reads the queue and history.
quarantine and no `--readmit` (H1). Its reference never names a
working box again.
- **Its local chores are discharged.** Every volume was deleted with the
- instance (H10), so its `park-local` chores are discharged in the same
+ instance (H10, H15: the record, kept true by IAM), so its `park-local` chores are discharged in the same
CAS, by `CompleteObligation` semantics, without retirement. That is
claim D5's destroyed-local exception, now proven (L3). Regional chores
are reassigned to `region:<r>`, keeping their ids, operations and
@@ -883,6 +957,67 @@ alerting, and owns the loop that reads the queue and history.
durable loss record when it next runs, with timer knowledge `partial`:
wake state was published continuously, up to H3's window.
+### 5.10 Relocation starts and executable updates (H14)
+
+Claim D7 serializes a lifecycle start against executable updates with a
+reservation the console writes into the roster. A relocation must start
+when the console is down, so H14 moves the serialization to the boxes.
+Each side records its own intent durably first, then checks the
+other's, so of two that race at least one sees the other.
+
+**The worker, at the start.** Before the start CAS, the worker reads
+`control/host-actors.json` as an exact object and verifies its root
+signature (an unreadable or unverifiable roster refuses the start). It
+requires `to`, and `from` unless the start cites a fence, to be
+`active`, and requires that no entry in `activations` names `from`'s or
+`to`'s enrollment. It records the revision it read as
+`directory_revision` in the intent. It writes nothing to the roster.
+
+**A participant, before it acknowledges.** A box becomes a participant
+in practice when it acknowledges the operation (claim D6). Before its
+first execution-lease PUT, its controller:
+
+1. writes its `.hostexec/<host>.json` state as `pending`, durably (claim
+ D6's existing state and write protocol);
+2. then reads root's update state: the system-update journal and root's
+ **update-begun** marker (below). If an update is begun or in progress
+ for this box, it refuses to acknowledge, clears its `pending` state,
+ and retries after the update finishes. The operation cannot leave
+ `starting` without `to`'s acknowledgement, so nothing has moved; the
+ worker waits, or replaces `to` (§5.8).
+
+**Root, before it starts an update.** Every path that begins an
+executable change (system apply, explicit rollback, a guarded
+activation under claim D1, the console's activation reservation being
+acted on), under root's existing system-update lock:
+
+1. writes an **update-begun** marker, durably, beside the update journal
+ (`/opt/carlos/system/guard/update-begun.json`, naming the update);
+2. then scans `<DataDir>/.hostexec/` for any host whose state is
+ `pending` or `active`. If any is found, the box is a participant in a
+ running claim operation: root removes its marker, refuses the update,
+ and leaves the console's activation reservation held, to retry later;
+3. otherwise it proceeds. The marker stays until the update finishes or
+ is recovered.
+
+Boot recovery of an update already begun before any participation needs
+no new check: no participant acknowledged meanwhile, because step 2 of
+the participant's side saw the marker.
+
+**The race.** If the console reserves an update for `to` after the
+worker's roster read, the start proceeds, and the reservation is
+harmless by itself. Whichever of `to`'s acknowledgement and root's
+begin writes its marker first, the other sees it and refuses: either
+the update waits until the operation returns to `serving` and D6 clears
+the controller's state, or the acknowledgement waits until the update
+finishes. Root's refusal is what closes the race (H14). A box stays a
+participant from its `pending` state until D6 clears it, so an update
+never runs under a relocation's `installing`, `probing` or release.
+
+This holds for every claim operation a box takes part in, not only
+relocations. L6 asks the claim to make it D7's rule for box-started
+operations.
+
## 6. Infrastructure and IAM (A1-12)
- **Box role** keeps plan 1a's `ec2:DescribeInstances` with a condition
@@ -896,12 +1031,30 @@ alerting, and owns the loop that reads the queue and history.
own grants: `s3:ListBucketVersions`, `s3:GetObjectVersion` and
`s3:DeleteObjectVersion` on its region's data bucket (claim §6.2a),
which `genselect` and the parker use.
+- **The volume guard (H15).** An Organizations service control policy on
+ the deployment's account denies `ec2:ModifyInstanceAttribute` and
+ `ec2:AttachVolume` on instances tagged as platform boxes
+ (`aws:ResourceTag/carlos:role = platform-box`) to every principal whose
+ ARN is not one of the deployment's operator roles. Where the account
+ has no Organizations, tofu puts the same Deny in every role and user it
+ manages; that is weaker, since it cannot bind principals tofu does not
+ know, and `carlos system` says so. The launch template and the
+ `region-host` module create every volume with `DeleteOnTermination`.
+ An operator who attaches or changes a volume on a platform box must
+ refresh its volume record before leaving it (`carlos ops box volumes
+ refresh <box>`); until the next record, the box's fence rests on the
+ previous one, which is why §5.9 bounds its age.
+- **Auto Scaling enrollment (H16, spec 4).** The box role gains
+ `autoscaling:DescribeAutoScalingInstances`, conditioned on
+ `aws:RequestedRegion`, read-only.
- **Console role:** unchanged, no EC2 rights.
- **S3 (A2-6).** Two kinds of write, kept apart:
- **Create-only** (`If-None-Match: *`): content objects,
`instances/<host>/*/files/*/objects/`, in the region's data bucket;
- box identity records, `control/boxes/`, and fence records,
- `control/box-fences/`, in the control bucket.
+ box identity records, `control/boxes/`, volume records,
+ `control/box-volumes/`, membership confirmations,
+ `control/box-enrollment/`, and fence records, `control/box-fences/`,
+ in the control bucket.
- **Conditional replace** (`If-Match`, or `If-None-Match: *` for the
first write): `instances/<host>/*/files/*/manifest.json` only.
@@ -916,6 +1069,7 @@ alerting, and owns the loop that reads the queue and history.
tested against both refusals. The control bucket denies `PutObject`
without `s3:if-none-match: *`, and `DeleteObject` and
`DeleteObjectVersion`, on `control/boxes/*` (plan 1a's policy) and on
+ `control/box-volumes/*`, `control/box-enrollment/*` and
`control/box-fences/*`. Each Deny is scoped to its prefix only: a
broad Deny on this bucket once broke every lease release. The
`*.bind.json` statement plan 1a added is no longer needed (§19). Data
@@ -936,7 +1090,9 @@ alerting, and owns the loop that reads the queue and history.
| The console's box is the one that died | failover does not need it: the fence, the census exclusion and every relocation CAS are a regional worker's (§5.9, L5, L6). |
| Box partitioned from the control bucket | it keeps serving what it serves (no execution lease while `serving`, claim §7.1). It can't take new work. The group's health check decides whether it is terminated. |
| 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). |
+| 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 update is reserved for `to` while a relocation starts | root refuses to begin it while `to` is a participant, or `to` refuses to acknowledge while it runs; never both running (§5.10). |
| A forged or confused edges file names a terminated instance | ignored: identity comes only from the roster's `cloud`, proven at enrollment (HA7). |
| Placer dies mid-relocation | another regional worker replays from the claim (§5.8). |
| Target dies after its probe admitted | a nested failover from `holder.generation` (§5.8). |
@@ -963,8 +1119,9 @@ alerting, and owns the loop that reads the queue and history.
`region-ha-1` and runs at or above the HA floor;
- every box in the region is `active` in the roster with a proven
`cloud`;
- - the region's termination queues exist, and every attached EBS volume
- has `DeleteOnTermination`.
+ - the region's termination queues exist, every box has a current
+ volume record showing every attached EBS volume
+ `DeleteOnTermination`, and the volume guard (§6) is in place.
The HA floor joins the protocol floor (claim §10, D1): every path that
restores an executable (system apply, rollback, boot recovery from
@@ -978,27 +1135,35 @@ alerting, and owns the loop that reads the queue and history.
5. Rehearse on dev with two fixed boxes: create spreads across both;
drain one (§5.8) with `moveprobe` shaped like ips (moves §14.4),
including timers and a `continuous` files app; terminate one by hand
- and run the spec 3 placer manually. On the same dev box, record what
- `DescribeInstances` returns for `BlockDeviceMappings` once an instance
- is `terminated` (§11 question 2).
+ and run the spec 3 placer manually. **Done on 2026-10-06 (H15):** a
+ t4g.nano launched with a `DeleteOnTermination: true` root and a
+ `DeleteOnTermination: false` extra volume, then terminated. While
+ `shutting-down`, `DescribeInstances` still listed both volumes, with
+ their flags and status `attached`. Once `terminated`,
+ `BlockDeviceMappings` was `[]`, the root volume was already gone
+ (`InvalidVolume.NotFound`), and the `false` volume survived as
+ `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 by a non-operator
+ role is refused.
6. Then specs 2, 3, 4.
## 9. Changes requested of the claim and moves specs
For platform-1e (claim rev 6.25, 413b12cb) and platform-d8 (moves rev
7.17, 39904c84). They replace rev 4.2's K1 to K11, which those sessions
-never received. "Ruled" means Paul's rulings of 2026-10-06 (H10 to H13,
+never received. "Ruled" means Paul's rulings of 2026-10-06 (H10 to H16,
and amadan #236) decide the direction; the owning session still writes
the text.
| # | Amends | Change | Status |
|---|---|---|---|
| L1 | claim §4, §8, §6.4a (which kinds and phases admit), §7.3 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; the replacement table; 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. 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, bound to the roster entry's `cloud`, every EBS volume `DeleteOnTermination`, from a describe. Stopped, unclear and vanished boxes keep D2's operator path unchanged. | Ruled (H10) |
+| 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`, under the IAM volume guard. 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) | An enrolled box with a valid fence record is excluded from the census without waiting for the console to retire it. | Ruled (H10; #236: failover works when the console's box died) |
-| L6 | claim §10.1 D7 (lifecycle reservations, `actors_hold`) | A relocation started by a regional worker cannot get a console-created reservation if the console is down. The claim needs a box path. | **Open: needs Paul** (§11 question 1) |
+| L6 | claim §10.1 D7 (lifecycle reservations, `actors_hold`) | For operations a box starts, replace the console reservation with §5.10's box-side check: the worker reads (never writes) the roster and refuses if an update is reserved for `from` or `to`; a participant writes its `pending` controller state, then refuses to acknowledge while root has an update begun; root writes its update-begun marker, then refuses an update while any host's controller state is `pending` or `active`. | Ruled (H14) |
| L7 | claim §10.1 D7, §7.3 D2, §5.9 P5-D2, §6.6 P6-D2 | Enrollment of a platform box requires `internal/boxid` evidence; it records `cloud` from it, and checks the tofu `instance_id` for a reference already in claims or edges files. No other roster, and no `control/boxes` authority. | Ruled (H11) |
| L8 | claim §5.1 steps 2 and 4, §5.9 P5-D2 and P5-D6 | The executor in the create intent is `choose()` (§5.2), with P5-D2's SHA-256 as the final tie-break; chosen once, never substituted; a create whose box can't complete is cancelled. | Ruled (H12) |
| L9 | claim §6.4a item 2, `hostclaim.AdmitWriter` | A coverage value `released`: a prepared generation built from a pin that reaches the `final_txid` a released box recorded from its stopped database, under that box's stop receipt. It declares nothing. Without it every drain declares a false loss. | Requested |
@@ -1011,6 +1176,7 @@ the text.
| 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. | Ruled (H16) |
**Rev 4.2's requests, mapped.** K1 is L8. K2 is L1, with L9 to L11. K3
is L2 to L5. K7 is L14. K8 is L16 and L17. K9 is L18. K10 is L15. K11 is
@@ -1039,8 +1205,13 @@ rev 4.2's table; this mapping says where each now lives.
objects, tombstones, late uploads, `If-Match` races, a cold admission's
copied manifest naming the predecessor's objects); timer catch-up;
termination-fence acceptance (NotFound, wrong account, wrong region,
- stopped, a volume without `DeleteOnTermination`, an empty mapping list,
- an event with no describe, all sources empty); continuous files with a
+ stopped, all sources empty, an event alone with a good record, a
+ record with a `false` volume, a missing record, a record just inside
+ and just past the 2-hour bound, a record for another instance, a
+ `shutting-down` read offered as evidence, a `shutting-down` read that
+ refreshes the record with a `false` volume); the volume recorder
+ (writes on change, at least hourly, never on an unchanged sweep
+ otherwise); continuous files with a
file held open (published at the deadline), inotify overflow, a file
that changes during its read; the create-only and replace split in
`hostfiles.Publish`.
@@ -1052,7 +1223,16 @@ rev 4.2's table; this mapping says where each now lives.
level-0 anchor; a release with a live embedded replicator (step 1
passes with only its descriptors, step 4 passes only after `DB.Close`,
any other holder fails step 1); a failover admission declaring
- `writes_lost` for `from`'s session and a drain declaring nothing.
+ `writes_lost` for `from`'s session and a drain declaring nothing; the
+ start refused when the roster reserves an update for `from` or `to`.
+- **Starts and updates (§5.10):** root's begin and a participant's
+ acknowledgement interleaved at every point, with a crash between each
+ marker and its check: never an update and an acknowledged
+ participation at once, and a crash leaves at most a refusal, cleared
+ by retry.
+- **IAM:** the volume guard refuses `AttachVolume` and a block device
+ mapping change by the box role and a non-operator role, and allows an
+ operator role (rehearsed on dev, §8 step 5).
- **Integration:** two agents against one fake region, one control
bucket: create lands on the chosen box only; the other box mints no
row; the edge backhauls to the executor; relocate by drain through
@@ -1064,51 +1244,17 @@ rev 4.2's table; this mapping says where each now lives.
## 11. Open questions
-Rev 4.2's three were ruled as H7, H8 and H9 (2026-10-04). The claim's
-current text leaves three for Paul.
-
-1. **Who reserves the actors for a relocation a box starts (L6).** Claim
- D7 has every protected start record a lifecycle reservation, created
- by the console with a CAS on the root-signed roster, so a start cannot
- race an executable change. A regional worker cannot write the roster,
- and #236 wants failover to work when the console's box is the one
- that died. Options:
- - **(a)** A box path: a relocation's start CAS requires the roster to
- show no executable activation reservation for `from` or `to`, and
- root's update path refuses to begin an activation on a box that is
- a participant of a running operation, read under the host guard.
- The serialization moves to the boxes. *Recommended.*
- - **(b)** Failover waits for a console to reserve. Simple, but a
- console outage stalls every failover, against #236's intent.
- - **(c)** A standing per-region reservation the console keeps for
- relocations. It blocks executable updates on every box it names for
- as long as it is held, so updates would need to release and retake
- it.
-2. **What if a terminated instance's describe has no block devices.**
- H10 reads `DeleteOnTermination` from the same `DescribeInstances`
- reply. AWS may return an empty `BlockDeviceMappings` once an instance
- is `terminated` (to be confirmed on dev, §8 step 5), and EventBridge's
- state-change event carries none. If so, H10 as written never fences.
- Options:
- - **(a)** Keep H10 as written and accept that failover is manual
- whenever the mappings are gone. *Only if dev shows they stay.*
- - **(b)** Record each box's mappings from a describe while it is
- `running` (at enrollment and on every sweep), deny
- `ec2:ModifyInstanceAttribute` and `ec2:AttachVolume` on platform
- boxes to every role but the operators', and fence on the terminated
- read plus the newest running record. *Recommended if (a) fails.*
- - **(c)** Also accept EventBridge's "EBS Volume Notification"
- `deleteVolume` result `deleted` for each recorded volume as positive
- proof. Best-effort delivery, so it adds to (b) and never replaces it.
-3. **How an Auto Scaling box is enrolled (spec 4, not blocking spec 1).**
- Claim P3's recommendation is operator-approved enrollment, but H5
- scales out automatically. Options: **(a)** every enrollment is
- approved by an operator, so scale-out waits for a person; **(b)** the
- console enrolls a box automatically when its `boxid` evidence is in
- the deployment's account and region and a regional worker reports the
- instance a member of the region's group
- (`autoscaling:DescribeAutoScalingInstances`, read-only), and asks an
- operator for anything else. *Recommended.*
+None. Rev 4.2's three were ruled as H7, H8 and H9 (2026-10-04). The
+three rev 5 first left open were ruled on 2026-10-06:
+
+1. **Who reserves the actors for a relocation a box starts (L6).**
+ Ruled as H14: the box-side check, §5.10.
+2. **A terminated instance's describe has no block devices.** Tested on
+ dev and ruled as H15: a running-time volume record kept true by IAM,
+ §5.9 and §6.
+3. **How an Auto Scaling box is enrolled.** Ruled as H16: automatically,
+ on `boxid` evidence plus a worker's group-membership confirmation,
+ §5.1, L19. Spec 4 builds it.
## 12. Out of scope, for the later specs
@@ -1118,7 +1264,8 @@ current text leaves three for Paul.
every host off a terminated box in bounded parallel, waking always-on
and scheduled instances, the failover gap target.
- **Spec 4:** the Auto Scaling group per region, launch template and
- join without SSM hand steps, enrollment of new boxes (§11 question 3),
+ join without SSM hand steps, automatic enrollment of new boxes (H16,
+ L19),
ingress for boxes that come and go (a per-region NLB with fixed EIPs,
or an EIP pool claimed at boot), HTTP-01 certificates, steering,
scale-out on headroom, scale-in by drain (§5.8), per-region minimum
@@ -1253,11 +1400,14 @@ changes how some of it is used. Plan 1b is written against rev 5.
- **`chooseBox`'s last tie-break.** The lowest box id becomes P5-D2's
greatest SHA-256 of `host + "\x00" + reference` (H12). The first two
rules stand.
-- **`termfence`'s binding and source.** `Verify` binds to a
- `boxid.Identity`; it must bind to the roster entry's `cloud`, require
- every block device `DeleteOnTermination` and at least one mapping, and
- produce §5.9's record. `FromEvent` becomes a trigger: an event alone
- never fences (H10).
+- **`termfence`'s binding and inputs.** `Verify` binds to a
+ `boxid.Identity`; it must bind to the roster entry's `cloud`, take the
+ newest volume record as an input and require it within the 2-hour
+ bound with every volume `DeleteOnTermination` (H15), and produce §5.9's
+ record. `FromDescribe` and `FromEvent` stay as the two terminated
+ reads; a `shutting-down` observation is never accepted. Plan 1b adds
+ the volume recorder, §5.10's two checks, the volume guard and, for
+ spec 4, the membership confirmation (H16).
**Stays as built.**