carlos / platform Public

spec: host lifecycle claim rev 6.32 (Plan 12: J2(b) recovery handover evidence, J5 fenced-target release, RunHeld and the M6 lease set, retired-location census, cross-region materialisation per §6.6 P6-D3)

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codex agent, operated by paul@keymail.dev pushed by claude agent, operated by paul@keymail.dev 0fd83867c01f1d22db0940f7516d6274957c9930 parent 5c4718e
1 file changed, +167 −4
  • docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md +167 −4

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.013s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	159.255s
ok  	github.com/carlosframework/platform/cmd/console	2.703s
ok  	github.com/carlosframework/platform/internal/activator	55.848s
ok  	github.com/carlosframework/platform/internal/agentsock	0.030s
ok  	github.com/carlosframework/platform/internal/analytics	0.022s
ok  	github.com/carlosframework/platform/internal/appconfig	1.049s
ok  	github.com/carlosframework/platform/internal/apperr	0.180s
ok  	github.com/carlosframework/platform/internal/applog	0.201s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/caps	0.012s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.046s
ok  	github.com/carlosframework/platform/internal/clamd	0.057s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	53.917s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	1.472s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.025s
ok  	github.com/carlosframework/platform/internal/dbverify	0.005s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.014s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	1.183s
ok  	github.com/carlosframework/platform/internal/doorkey	0.009s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	23.595s
ok  	github.com/carlosframework/platform/internal/edgesfile	0.012s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.209s
ok  	github.com/carlosframework/platform/internal/gate	1.754s
ok  	github.com/carlosframework/platform/internal/host	6.903s
ok  	github.com/carlosframework/platform/internal/infraview	0.006s
ok  	github.com/carlosframework/platform/internal/instancectl	0.419s
ok  	github.com/carlosframework/platform/internal/instanceexec	0.023s
ok  	github.com/carlosframework/platform/internal/instancefs	0.009s
ok  	github.com/carlosframework/platform/internal/lease	5.255s
ok  	github.com/carlosframework/platform/internal/ledger	0.027s
ok  	github.com/carlosframework/platform/internal/mail	0.075s
ok  	github.com/carlosframework/platform/internal/maintenance	0.188s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.614s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.057s
ok  	github.com/carlosframework/platform/internal/metrics	0.011s
ok  	github.com/carlosframework/platform/internal/objstore	0.134s
ok  	github.com/carlosframework/platform/internal/ops	0.141s
ok  	github.com/carlosframework/platform/internal/privatefile	0.018s
ok  	github.com/carlosframework/platform/internal/provision	0.908s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
ok  	github.com/carlosframework/platform/internal/registry	1.343s
ok  	github.com/carlosframework/platform/internal/relay	7.247s
ok  	github.com/carlosframework/platform/internal/release	3.695s
ok  	github.com/carlosframework/platform/internal/s3compat	0.007s
ok  	github.com/carlosframework/platform/internal/schedule	1.519s
ok  	github.com/carlosframework/platform/internal/status	0.129s
ok  	github.com/carlosframework/platform/internal/steering	0.010s
ok  	github.com/carlosframework/platform/internal/svcrole	0.022s
ok  	github.com/carlosframework/platform/internal/telemetry	0.006s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.014s
ok  	github.com/carlosframework/platform/internal/unixdial	0.013s
ok  	github.com/carlosframework/platform/internal/usage	0.013s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.711s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.029s
minio contract: PASS
diff --git a/docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md b/docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md
index 71bf7d4e6..a862b6f5b 100644
--- a/docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md
+++ b/docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md
@@ -1,8 +1,10 @@
# The host lifecycle claim
+**Rev 6.32 (2026-10-10):** completes the claim interfaces for moves M2, M3, M5, M6, M7, M9, M12 and M21. Claim Plan 12 supplies loss-bound Group D admission, recovery predecessor evidence, atomic fenced-target release and restore retirement, held-lease parks, local park authority and retained retirement-location evidence.
+
**Rev 6.31 (2026-10-09):** records the accepted region HA contracts in amadan #236: `AutomaticLoss` in claim-level `evidence_refs`, the stop-only `relocation-release` obligation, and immutable signed `enrolled_at`. Section 6.2a binds moves' push, landing-install and rollback-return parks by name and region; R6 refuses automatic empty failover after any writer session. Claim Plan 11 supplies the integration.
-Design, 2026-10-09, **rev 6.31**. Joint rounds 6 and 7 judged the claim ready for primitive planning. Rev
+Design, 2026-10-10, **rev 6.32**. Joint rounds 6 and 7 judged the claim ready for primitive planning. Rev
6.5 added the R6-4 rule. Rev 6.6 added writer epochs (R7-1, amadan #225).
Rev 6.7 answered the moves session's notes on §6.4a. Rev 6.8 rewrote §6.4a
around writer sessions, for joint round 8. Rev 6.10 answers joint round 10
@@ -1605,6 +1607,60 @@ IAM: the box needs `s3:ListBucketVersions`, `s3:GetObjectVersion` and
`s3:DeleteObjectVersion` on its own data bucket. Today it has only
`Get`, `Put`, `Delete` and `List` (region-host main.tf:166, :174).
+#### Held location leases and the lease set (moves M5, M6)
+
+`park.Engine.RunHeld(ctx, work, held)` runs the same authorizer, version
+census, archive, delete and completion path as `Run`. Its configured
+`AggregateLeases` borrows only a still-valid lease whose acquisition-bound
+bucket and location equal the persisted scope exactly. The location includes
+the host. A lease for another host, bucket, incarnation or staging lane is
+refused before data effects. The engine may acquire other required locks,
+but never releases or closes the borrowed lease. The landing keeps renewing
+that lease through `published`, as moves section 7.6 requires.
+
+For `moves/<host>/<op>` scopes, the lease set contains the scope's own
+lock. Staging signatures are bounded by the persisted `evidence_at`: the
+seal, abort, revert or return-seal bound. The same own-lock rule applies
+to an eligible whole-host moves sweep. For `instances/<host>[/<inc>]`,
+the set contains the location's replica lease and the scope lock. These
+are the same location name and are deduplicated. Every name is relative
+to the engine's data bucket. Deployment-bucket staging leases are never
+passed as bucket-qualified names. `ReplicaParkLeaseSet` states this contract.
+
+#### Archived-incarnation census (moves M7)
+
+A bare legacy object key does not identify its incarnation. The existing
+`retired_incarnations` list proves retirement, but does not say which
+incarnation occupied `instances/<host>`. Neither its ordering nor a move's
+`LastMove` is sufficient to infer that mapping.
+
+Retirement retains `retired_locations` in its existing CAS only while
+`CARLOS_HOST_CLAIM_RETIRED_LOCATIONS=1`. Leave this write gate off until
+every strict claim reader has the rev 6.32 reader. Entries follow the
+32-entry history cap. Old claims remain readable; missing, trimmed or
+ambiguous bare-location history produces a typed dependency refusal.
+The holder's own bare location is labelled with its incarnation in other
+regions when the history excludes another occupant. Region-specific live-set
+protection still applies in the authoritative region.
+
+Qualified `instances/<host>/<incarnation>` paths identify themselves. Their
+cleanup uses the ordinary retirement authority and live-set checks; it never
+requires membership of the bounded `retired_incarnations` list.
+
+The claim census labels instance residue. Moves proves its own staging
+namespace. `View.ArchivedIncarnations` separates their labels, and
+`View.RefusedLocations` excludes only the ambiguous location. A refused
+moves proof drops that lane through `VerifyMovesSweep`, leaving proven
+instance candidates eligible. A bare-location refusal cannot relabel it
+with the move's incarnation or block a separately proved lane.
+
+For a free claim, no move remains live. The claim supplies the lanes' proof
+using its newest retained incarnation and the maximum nonzero `QuietAfter`
+bound; the bound must precede the version listing. Without that evidence,
+lanes remain refused while independently proven instance residue can proceed.
+The census establishes a conservative initial observation-time bound only
+after proving retirement. Every retry retains the scope's original bound.
+
### 6.3 Local retirement (J-I-7, 199-13, 199-14)
Retirement covers both layouts.
@@ -2295,6 +2351,113 @@ cold successor has a new generation. A warm successor exists only after
the old session certified that every one of its requests had completed.
Dead generations are parked by the at-rest sweep (§6.2a).
+### 6.4b Moves recovery admission completion (J2(b), J5)
+
+The first `restoring/probing` admission under `loss.<id>` uses
+`AdmitWriterWithLoss`, including a loss before target admission. The frozen
+sources may name the source-cut predecessor or contain no target source.
+`hostops.MoveLossRecoveryOwner` supplies the existing loss owner's signed-intent
+policy. `LossAdmissionSources` validates each frozen baseline; the writer
+fills both primary and additional remote maxima from that one result.
+`ParseMoveLossBaseline` is the shared decoder of replica and seal baselines.
+The writer first compares Select's restore maximum with its non-nil restore
+plan. Frozen loss maxima are independent: target M can exceed restored source N.
+Only a frozen ordinary predecessor replaces `RemoteMaxTXID`; an empty frozen
+set preserves Select's validated maximum. AdmissionEvidence receives that loss
+maximum alongside the unchanged restore plan. Never infer zero from no sources.
+
+The admission CAS records `loss_admitted.<id>` with the admitted session.
+This operation-local marker survives admission-history trimming. A restarted
+probe and later source admissions use ordinary admission after consumption,
+retaining the original loss evidence. Every admission under this operation
+still refuses sealed or clean-return coverage. Omitting `Admission.LossID`
+cannot bypass first consumption. Claim-level historical references alone do
+not turn a later operation into loss recovery. Multiple current declarations
+remain refused pending a combination protocol.
+
+Only a `restoring` source whose nonempty predecessor writer is the move's
+target uses cross-box recovery handover. Initial source returns, including session zero, use the exact source cut,
+source stamp and canonical complete source report through the normal stop
+wiring. A missing report, mismatched cut or changed claim refuses. Later source
+sessions retain their own local writer-stop receipts. Recovery accepts any of these evidence forms:
+
+- The target's own `TargetReleased` exec-to-none CAS records
+ `target_session` and `target_generation` from the holder when the target
+ admitted. Recovery compares those journal values with the predecessor and
+ checks the complete unsigned `stopped-target.json` report's role, host,
+ operation, incarnation and box. This is the same claim authority as the
+ source cut's `cut_session`; `claim_gen` is never a session. Keys come from
+ `moves.StoppedKey(moves.ReportPrefix(...), "target")`. No moves stop adapter
+ or new signature scheme is required.
+- A root-signed `StopReceipt`, where available, bound through
+ `target_stop_receipt` and `target_stop_sha256`; verify its exact digest,
+ enrolled box signature, installation stamp, session, operation, phase,
+ region, enrollment and completed stop evidence.
+- This operation's committed, signed `loss.<id>` naming the exact target
+ predecessor in its frozen sources.
+- A consumed fence whose journal marker, claim-level automatic loss identity,
+ enrollment, target and holder incarnation agree. Consumption removes target
+ execution and participation. Completion and handover do not revalidate the
+ dead target or witness against a later roster. An unconsumed fence still
+ requires fresh verification against the target enrollment and region.
+
+Before admission the claim checks the source executor and predecessor;
+afterward it checks the cold session and fresh stamp. The source never
+manufactures a local receipt for the target. Restore retirement is claim-owned
+for both restoring and reverting: the guarded local hook retires the source's
+old database and marker at `JournalSourceStamp`, using `src_gen` and the
+source cut evidence. Later source sessions retain the root writer-stop receipt
+requirement. After activation and before prelaunch, the source reads its exact original
+StopInventoryDB installation. Under the descriptor guard it rebinds the exact
+registry route and timers transactionally, then transfers the inventoried paths
+and ownership to the admitted stamp. Physical identities and old inventory are
+retained. Missing inventory refuses; a replay verifies the existing successor.
+Root's inventory-write transport still validates registry ownership. The
+registry call uses the existing isolated transport; root opens no SQLite DB.
+
+`ReleaseFencedTarget` both declares and consumes a verified `ReturnFence` in
+one CAS. It rolls back an uncommitted landing if needed, removes the target
+from execution and participants, reaches `released`, covers target-region
+predecessors and queues the target location, staging lane and any partial
+return lane as obligations. It records a release marker that `ReturnToSource`
+accepts without giving the source console authority. The reference never
+acknowledges those parks. Replays preserve scopes and do not requeue completed
+work. A fenced recovery cannot claim clean-return coverage. The same CAS appends
+Lost, LossRefs and AutomaticLoss outside the operation journal, retaining the
+original fence observation time, last target admission, regional chores,
+unknown timer knowledge, no local-discharge assertion and pending reporting.
+No quarantine revision is invented. These values survive operation and holder
+replacement. The console recovery pass materializes a signed loss record with
+an optional `automatic` authority payload before marking the retained evidence
+reported. The signed record also retains `alerts_pending:true`; materialization
+does not count as notification, and the delivery owner must consume this work.
+Retries preserve and compare the exact consequences. The optional
+payload is part of the reader rollout before this writer is enabled.
+
+Moves consumes `MoveRecoveryGrant.ReleaseFencedTarget` from the console's
+`Recovery.Moves` hook. Its first pass records a fence rollback decision when
+needed, preserving the first rollback reason. `LossRef` identifies that fence
+form. The next pass revokes the claim; the final pass completes the record from
+the consumed claim evidence. Each grant performs at most one mutation and
+rechecks record ETag and sequence before its claim CAS. New parks use the
+verified fence's original observation time, never a retry or rollback clock.
+If a target wrote a clean released record and died before releasing the claim,
+the grant verifies its canonical complete stopped-target report and revokes the
+claim directly, preserving the record and its original evidence.
+`MoveRecoveryGrant.StampLostMoveTargetParked` replaces the console's separate
+raw-record closure. It requires revoked target authority and no owed target or
+target-region chore. A target fenced before install needs no invented local
+park. Existing local parks remain owed until their own authorized completion.
+A dirty target still requires J4 declared loss. The unused abort-complete action
+is removed; abort owners retain their record-first close.
+`ClaimProviders.VerifyMoveTargetFence` exposes the box-side roster-backed
+verifier for callers that need a typed fence, without granting a write.
+
+`MoveParks` also returns the enrollment-bound local authorizer used by the
+claim's own `park.Local`. Moves uses it for local retirement, with its
+operation-specific checks in addition. The regional engine keeps its
+stronger `VerifyBound` wrapper. Neither authorizer substitutes for the other.
+
### 6.5 Door reads pinned to versions (J-N10)
The pull door's listing:
@@ -2595,7 +2758,7 @@ Before binding a scope or issuing storage mutations, the adapter must:
1. Validate its enrollment, effective region, bucket and retirement authority.
2. Resolve immutable evidence references covering every producer that could write the scope.
-3. Acquire all required replica-location and staging-work leases in a deterministic order.
+3. Acquire the data-bucket lease set defined in section 6.2a in a deterministic order, or borrow the exact caller-held location lease through RunHeld. Staging writers are bounded by the persisted evidence_at.
4. Re-read the claim and validate the exact obligation, original operation, phase and immutable scope.
5. Obtain P6-D3’s view under that observation.
@@ -2618,8 +2781,8 @@ Completion uses the chore’s original operation identity and updates `quiet_aft
Rejected alternatives:
-- Holding only the replica lock: staging can still write into the retirement scope.
-- Replacing evidence time with `time.Now()`: it invents evidence.
+- Holding a scope lock without the staging signature bound: staging can still write into the retirement scope.
+- Replacing a persisted evidence time with a retry clock, or using the clock without retirement proof. The archived-incarnation census in section 6.2a first proves retirement, then establishes the initial conservative bound; retries retain it.
- Treating publication as proof that every old writer stopped: publication establishes coverage, not termination.
#### P6-D6. Cross-process denial and returning-box cleanup