plan: claim Plan 13 rev 2 (Astra r1: remote home history through the verified binding, binding carried through every moves engine, legacy-name compatibility, receipt and pre-flip comparisons through the alias; whole-plan sweep of placement vs engine comparisons; re-anchored on main 0715faca)
Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+1577
−434
docs/superpowers/plans/2026-10-12-host-lifecycle-claim-13-home-region.md+1577 −434
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.011s
? 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 150.524s
ok github.com/carlosframework/platform/cmd/console 2.168s
ok github.com/carlosframework/platform/internal/activator 55.708s
ok github.com/carlosframework/platform/internal/agentsock 0.014s
ok github.com/carlosframework/platform/internal/analytics 0.020s
ok github.com/carlosframework/platform/internal/appconfig 0.954s
ok github.com/carlosframework/platform/internal/apperr 0.074s
ok github.com/carlosframework/platform/internal/applog 0.138s
ok github.com/carlosframework/platform/internal/awsinv 0.003s
ok github.com/carlosframework/platform/internal/caps 0.011s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.038s
ok github.com/carlosframework/platform/internal/clamd 0.054s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 50.609s
? 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.403s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.017s
ok github.com/carlosframework/platform/internal/dbverify 0.017s
ok github.com/carlosframework/platform/internal/dnsrec 0.017s
ok github.com/carlosframework/platform/internal/docsite 0.017s
ok github.com/carlosframework/platform/internal/domains 1.039s
ok github.com/carlosframework/platform/internal/doorkey 0.017s
ok github.com/carlosframework/platform/internal/economics 0.017s
ok github.com/carlosframework/platform/internal/edge 22.975s
ok github.com/carlosframework/platform/internal/edgesfile 0.017s
ok github.com/carlosframework/platform/internal/fleetapi 0.110s
ok github.com/carlosframework/platform/internal/gate 1.667s
ok github.com/carlosframework/platform/internal/host 6.650s
ok github.com/carlosframework/platform/internal/infraview 0.017s
ok github.com/carlosframework/platform/internal/instancectl 0.415s
ok github.com/carlosframework/platform/internal/instanceexec 0.017s
ok github.com/carlosframework/platform/internal/instancefs 0.017s
ok github.com/carlosframework/platform/internal/lease 5.092s
ok github.com/carlosframework/platform/internal/ledger 0.030s
ok github.com/carlosframework/platform/internal/mail 0.076s
ok github.com/carlosframework/platform/internal/maintenance 0.092s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.469s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.062s
ok github.com/carlosframework/platform/internal/metrics 0.004s
ok github.com/carlosframework/platform/internal/objstore 0.082s
ok github.com/carlosframework/platform/internal/ops 0.109s
ok github.com/carlosframework/platform/internal/privatefile 0.007s
ok github.com/carlosframework/platform/internal/provision 0.833s
ok github.com/carlosframework/platform/internal/publicrelease 0.009s
ok github.com/carlosframework/platform/internal/registry 1.241s
ok github.com/carlosframework/platform/internal/relay 7.217s
ok github.com/carlosframework/platform/internal/release 3.552s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/schedule 1.389s
ok github.com/carlosframework/platform/internal/status 0.026s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.002s
ok github.com/carlosframework/platform/internal/usage 0.005s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.535s
=== 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.022s
minio contract: PASS
diff --git a/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-13-home-region.md b/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-13-home-region.md| index e7a185b9b..6ffd57850 100644 |
| --- a/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-13-home-region.md |
| +++ b/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-13-home-region.md |
| @@ -1,23 +1,23 @@ |
| # Host lifecycle claim, Plan 13: home-region naming |
| -Implementation plan, 2026-10-12, rev 1. Implements the controller's three binding home-region answers and both addendum answers in `plan13-brief.md`. Apply Tasks A–I in order, including G2 before H. Task I amends the claim spec to rev 6.33. This plan authorizes no production changes or commits. |
| +Implementation plan, 2026-10-12, rev 2. Implements the controller's three binding home-region answers and both addendum answers in `plan13-brief.md`. Apply Tasks A–I in order, including G2 and G3 before H. Task I amends the claim spec to rev 6.33. This plan authorizes no production changes or commits. |
| Requirements: `/home/paulca/.cache/tmp/claude-1001/own/after236/plan13-brief.md`, which carries the controller's answers to moves on amadan discussion #236. This document and the write report are implementation instructions for the controller. |
| ## Global Constraints |
| -- Every code hunk is based on `1b5802fb75864e7659bd44ca1f90ad20a1979cc9`, with `internal/park/region.go` from `66b4e84d17649d2c0aad83127694aae86167f08f`, followed by earlier tasks here. Never substitute a moving main or export the older moves branch over the merged Plans 1–12. |
| +- Every code hunk is based on `0715faca664041152e2a437a70debd06c7177669`, with `internal/park/region.go` from `66b4e84d17649d2c0aad83127694aae86167f08f`, followed by earlier tasks here. This is the available origin/main including moves Plan 7; a fetch was attempted but DNS was denied. Verify the remote tip before integration. Never export the older moves branch over main. |
| - Keep `CARLOS_HOST_CLAIM` inert when off. The existing moves switch also remains required. The console's legacy dbClaimRegion fallback is byte-for-byte unchanged in the off arm; shared homeRegionIn retains the same rule. No service, initializer or production activation is added. |
| - Preserve #267: no by-name privileged filesystem effects, no root SQLite open, and no new AWS configuration loader. This plan adds no AWS client. The guard, root descriptor handling, stop-evidence signatures and leases retain their existing checks. |
| - Empty remains unset at every engine or claim boundary. Do not change Validate, parkQueueGuard, AdmitWriter, PublishGeneration or Cover*Predecessors to accept empty. PlacementRegion is applied to placement inputs only; applying it a second time to `@home` must still refuse. |
| - The claim engine keeps a home box's verified roster name. The alias is limited to that name and `@home`, under a verified empty local placement or the console-bound operation home name. No region-only fallback, bucket shortcut, authority widening to other regions or rewrite of stored claims is allowed. |
| - Census changes need the classifications in Task H. Do not regenerate inventories wholesale. No authority call site is added; Task H updates one existing call expression to name its verified-region adapter. |
| -- No subagents, Claude/Anthropic tools, git writes, commits or production actions. Use the controller-designated shared GOCACHE, never delete it, and remove only the exact scratch directory created for this replay. |
| +- No subagents, Claude/Anthropic tools, worktree git-directory writes, commits or production actions. Use the controller-designated shared GOCACHE, never delete it, and remove only the exact scratch directory created for this replay. |
| - No new product copy ships here. Existing refusal messages remain unchanged. The copy inventory at the end distinguishes internal protocol names from product language. |
| ## Home-name decision and compatibility |
| -Keep the roster name as the claim engine's name. If a home box is enrolled in `ie`, its engine, ordinary admissions, writer identity and root stop identity continue to use `ie`. The engine name passes through `park.PlacementRegion`; it is a valid named code. The box's empty `CARLOS_REGION` separately establishes that `ie` and `@home` denote its same storage region. On a named box the placement must equal its roster region and no alias is installed. |
| +Keep the roster name as the claim engine's name. If a home box is enrolled in `ie`, its engine, ordinary admissions, writer identity and root stop identity continue to use `ie`. The engine name passes through `park.PlacementRegion`; it is a valid named code. The box's empty `CARLOS_REGION` separately establishes that `ie` and `@home` denote its same storage region. On a named box the placement must equal its roster region and no ownership alias is installed. A bound operation can supply the home name for remote history reads. |
| This is the brief's permitted “stays the name” choice. Changing the existing engine identity to `@home` would also change admission chains, signed receipts, manifests and recovery comparisons. Preserving the name avoids a migration that would have to prove every retained evidence reference still means the same thing. There is no bulk rewrite or opportunistic CAS conversion. Old named claims continue through the existing writer path. The worker also recognizes moves' `region:@home` chores and reads the maximum of the two home quiet bounds. |
| @@ -25,6 +25,11 @@ The alias has explicit context. `SameRegion("", "ie", "@home")` is false. A name |
| For a move, the request door and BeginClaimMove use the same exported roster-placement predicate. Its HomePlacementCode rule is the moves branch's existing homeRegionIn rule. BeginClaimMove receives the console's deployment home name and retains it in `op.intent.home_region` in the same start CAS. The record's From/To stay placement codes. The claim's target and the loss intent's SourceRegion/TargetRegion are engine values. SourceRegion is never fed into a record or placement resolver; current readers use it as an admission/write-source/park region. |
| +## Revision log |
| + |
| +- Rev 1, c82a060d: retained the home roster name and introduced the verified local alias, claim-side home loss mappings and rev 6.33 amendment. |
| +- Rev 2, 2026-10-12: answers all five round-one findings in Task G3; re-anchors every patch to the available origin/main 0715faca (moves Plan 7), propagates view/engine bindings, restores unnamed-console compatibility, verifies home stop receipts and separates pre-flip record codes from engine identities. Adds the omitted-region lifecycle guard fix found by the held-handle recovery test, the complete comparison sweep and explicit census classifications. Fetch and the unchanged HTTP test are blocked by sandbox networking; the report distinguishes these from passing local checks. |
| + |
| ## Review Focus |
| 1. A named-box roster mismatch refuses; an empty roster or an engine token used as a placement refuses. VerifyRegional still checks the entire fresh actor, bucket and request limit, including on alias chores. |
| @@ -37,11 +42,11 @@ For a move, the request door and BeginClaimMove use the same exported roster-pla |
| ## Integration with moves-home-parks |
| -Land the moves home work first: `6dd7297f` supplies HomeRegion, PlacementRegion and reserved-token tests; `7fb8079a` maps move scopes, views, cleanup, predecessor regions and recovery commits; `89dbfe95` followed by `66b4e84d` puts the home staging lease mapping in host wiring without an import cycle. Read the individual commits, not a whole-tree diff against the moves branch's older base. |
| +Integration prerequisite (not yet on the available main): land the moves home work first: `6dd7297f` supplies HomeRegion, PlacementRegion and reserved-token tests; `7fb8079a` maps move scopes, views, cleanup, predecessor regions and recovery commits; `89dbfe95` followed by `66b4e84d` puts the home staging lease mapping in host wiring without an import cycle. Read the individual commits, not a whole-tree diff against the moves branch's older base. |
| -The requested isolated replay overlays only region.go onto the pinned main. It proves these claim-side patches compile and their tests run against merged Plans 1–12. It is not a replay of the entire not-yet-merged moves branch. On integration, retain those moves changes and resolve overlapping functions to the complete versions in this plan: the shared console/hostops roster predicate, the alias-aware armMovesParks condition, productionRosterRegion's checked placement result, and the alias-aware moves policy/probe checks. Do not restore old placement comparisons while resolving a patch conflict. |
| +The replay overlays only region.go onto the pinned main. Rev 2 includes the overlapping placement mappings required by its real view and engine tests in Task G3. It proves these claim-side patches compile and their tests run against merged Plans 1–12. It is not a replay of the entire not-yet-merged moves branch. On integration, retain those moves changes and resolve overlapping functions to the complete versions in this plan: the shared console/hostops roster predicate, the alias-aware armMovesParks condition, productionRosterRegion's checked placement result, and the alias-aware moves policy/probe checks. Do not restore old placement comparisons while resolving a patch conflict. |
| -In particular, movesBoxRegion continues to require `PlacementRegion(CARLOS_REGION)`; productionRosterRegion now returns that token after checking the signed roster binding. The claim engine may retain the roster name because armMovesParks uses Engine.OwnsRegion. Moves' homePlacementCode wrapper, if retained when merging, must delegate to park.HomePlacementCode; there must be one rule, not two independently edited copies. Preserve the moves branch's predecessorRegion mapping and stagingLeaseKey changes, neither of which this replay replaces. Re-run both branches' home witnesses after combining them. |
| +In particular, movesBoxRegion continues to require `PlacementRegion(CARLOS_REGION)`; productionRosterRegion now returns that token after checking the signed roster binding. The claim engine may retain the roster name because armMovesParks uses Engine.OwnsRegion. Moves' homePlacementCode wrapper, if retained when merging, must delegate to park.HomePlacementCode; there must be one rule, not two independently edited copies. Task G3 includes the same predecessorRegion mapping; retain it when integrating. Preserve the prerequisite stagingLeaseKey changes, which this replay does not replace. Re-run both branches' home witnesses after combining them. |
| ## Replay without git writes |
| @@ -50,7 +55,7 @@ The spec is a separate design-branch input: rev 6.32 at SHA-256 `6b7b1db1b51571e |
| ```bash |
| CLAIM13_REPO=/home/paulca/amadan.net/carlos/platform/.claude/worktrees/replica-ownership |
| CLAIM13_PLAN="$CLAIM13_REPO/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-13-home-region.md" |
| -CLAIM13_BASE=1b5802fb75864e7659bd44ca1f90ad20a1979cc9 |
| +CLAIM13_BASE=0715faca664041152e2a437a70debd06c7177669 |
| CLAIM13_SCRATCH=$(mktemp -d "$TMPDIR/claim13.XXXXXX") |
| export CLAIM13_REPO CLAIM13_PLAN CLAIM13_BASE CLAIM13_SCRATCH |
| mkdir "$CLAIM13_SCRATCH/tree" |
| @@ -81,7 +86,7 @@ for body in blocks: |
| raise SystemExit('patch did not apply exactly') |
| ``` |
| -For A–G and G2, apply the tests, run the task command and observe the stated regression, then apply implementation and rerun. H uses the existing censuses; I uses the spec assertions below. A missing cache or denied socket is not a regression witness. |
| +For A–G, G2 and G3, apply the tests, run the task command and observe the stated regression, then apply implementation and rerun. H uses the existing censuses; I uses the spec assertions below. A missing cache or denied socket is not a regression witness. |
| ```bash |
| python3 "$CLAIM13_SCRATCH/apply.py" "$CLAIM13_PLAN" A tests |
| @@ -452,33 +457,17 @@ Expected first failure: WorkSource.HomeRegion and Reconciler.HomeRegion do not e |
| <!-- apply: B implementation --> |
| ```diff |
| ---- a/internal/hostbox/work.go |
| -+++ b/internal/hostbox/work.go |
| -@@ -3,6 +3,7 @@ |
| - import ( |
| - "context" |
| - "errors" |
| -+ "github.com/carlosframework/platform/internal/park" |
| - "sync" |
| - "time" |
| - |
| -@@ -22,6 +23,7 @@ |
| - } |
| - |
| - type WorkSource struct { |
| -+ HomeRegion string |
| - Prepare func(context.Context, hostclaim.Observed) error |
| - Guard hostguard.Guard |
| - Box, Region string |
| -@@ -86,7 +88,7 @@ |
| +--- a/internal/hostbox/park.go |
| ++++ b/internal/hostbox/park.go |
| +@@ -41,7 +41,7 @@ |
| + if ob.ID != work.ID || ob.Op != work.Op { |
| + continue |
| } |
| - switch selected.Action { |
| - case "install", "restore", "spawn", "seed", "writer", "probe": |
| -- if until, ok := c.QuietAfter[s.Region]; ok && !s.Now().After(until) { |
| -+ if until := park.QuietUntil(c.QuietAfter, s.HomeRegion, s.Region); !until.IsZero() && !s.Now().After(until) { |
| - return ErrChanged |
| - } |
| +- if ob.Region != r.Engine.Region || ob.Owner != "region:"+r.Engine.Region || (ob.Kind != "park-replica" && ob.Kind != "park-generation") { |
| ++ if !r.Engine.OwnsRegion(ob.Region) || ob.Owner != "region:"+ob.Region || (ob.Kind != "park-replica" && ob.Kind != "park-generation") { |
| + return ErrUnsupported |
| } |
| + return r.Engine.Run(ctx, work) |
| --- a/internal/hostbox/reconcile.go |
| +++ b/internal/hostbox/reconcile.go |
| @@ -18,6 +18,7 @@ |
| @@ -518,17 +507,33 @@ Expected first failure: WorkSource.HomeRegion and Reconciler.HomeRegion do not e |
| continue |
| } |
| if o.Kind != "park-replica" && o.Kind != "park-generation" { |
| ---- a/internal/hostbox/park.go |
| -+++ b/internal/hostbox/park.go |
| -@@ -41,7 +41,7 @@ |
| - if ob.ID != work.ID || ob.Op != work.Op { |
| - continue |
| +--- a/internal/hostbox/work.go |
| ++++ b/internal/hostbox/work.go |
| +@@ -3,6 +3,7 @@ |
| + import ( |
| + "context" |
| + "errors" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "sync" |
| + "time" |
| + |
| +@@ -22,6 +23,7 @@ |
| + } |
| + |
| + type WorkSource struct { |
| ++ HomeRegion string |
| + Prepare func(context.Context, hostclaim.Observed) error |
| + Guard hostguard.Guard |
| + Box, Region string |
| +@@ -86,7 +88,7 @@ |
| } |
| -- if ob.Region != r.Engine.Region || ob.Owner != "region:"+r.Engine.Region || (ob.Kind != "park-replica" && ob.Kind != "park-generation") { |
| -+ if !r.Engine.OwnsRegion(ob.Region) || ob.Owner != "region:"+ob.Region || (ob.Kind != "park-replica" && ob.Kind != "park-generation") { |
| - return ErrUnsupported |
| + switch selected.Action { |
| + case "install", "restore", "spawn", "seed", "writer", "probe": |
| +- if until, ok := c.QuietAfter[s.Region]; ok && !s.Now().After(until) { |
| ++ if until := park.QuietUntil(c.QuietAfter, s.HomeRegion, s.Region); !until.IsZero() && !s.Now().After(until) { |
| + return ErrChanged |
| + } |
| } |
| - return r.Engine.Run(ctx, work) |
| ``` |
| ## Task C: Wire the verified box identity into claim engines |
| @@ -596,127 +601,6 @@ Expected first failure: claimRegionBinding and IdentityProvider.HomeRegion do no |
| <!-- apply: C implementation --> |
| ```diff |
| ---- /dev/null |
| -+++ b/internal/host/claimregion.go |
| -@@ -0,0 +1,20 @@ |
| -+package host |
| -+ |
| -+import ( |
| -+ "github.com/carlosframework/platform/internal/hostbox" |
| -+ "github.com/carlosframework/platform/internal/park" |
| -+) |
| -+ |
| -+// The signed roster retains the claim's engine name. CARLOS_REGION supplies |
| -+// this box's placement code; only its empty home placement binds an alias. |
| -+func claimRegionBinding(roster, code string) (engine, home string, err error) { |
| -+ engine = park.PlacementRegion(roster) |
| -+ placement := park.PlacementRegion(code) |
| -+ if roster == "" || engine == "" || placement == "" || (code != "" && engine != placement) { |
| -+ return "", "", hostbox.ErrChanged |
| -+ } |
| -+ if code == "" { |
| -+ home = engine |
| -+ } |
| -+ return engine, home, nil |
| -+} |
| ---- a/internal/hostbox/identity.go |
| -+++ b/internal/hostbox/identity.go |
| -@@ -3,6 +3,7 @@ |
| - import ( |
| - "context" |
| - "crypto/x509" |
| -+ "github.com/carlosframework/platform/internal/park" |
| - |
| - "github.com/carlosframework/platform/internal/boxid" |
| - "github.com/carlosframework/platform/internal/hostclaim" |
| -@@ -12,6 +13,7 @@ |
| - // Actor is the verified projection of the sole control/host-actors.json roster. |
| - // Roster authenticates that document and its revision before returning a row. |
| - type Actor struct { |
| -+ HomeRegion string |
| - ControlBucket string |
| - Box, Enrollment string |
| - Revision uint64 |
| -@@ -31,6 +33,7 @@ |
| - } |
| - |
| - type IdentityProvider struct { |
| -+ HomeRegion string |
| - AWSRegion string |
| - Roster Roster |
| - Evidence BoxEvidence |
| -@@ -67,6 +70,10 @@ |
| - if ev.Box != p.Box || ev.Cloud != cloud { |
| - return Actor{}, ErrChanged |
| - } |
| -+ if p.HomeRegion != "" && (p.HomeRegion != a.Region || park.PlacementRegion(p.HomeRegion) != p.HomeRegion) { |
| -+ return Actor{}, ErrChanged |
| -+ } |
| -+ a.HomeRegion = p.HomeRegion |
| - return a, nil |
| - } |
| - |
| ---- a/internal/host/claimproduction.go |
| -+++ b/internal/host/claimproduction.go |
| -@@ -118,7 +118,11 @@ |
| - return out, hostwrite.ErrUnavailable |
| - } |
| - authority := productionClaimRoster{Directory: directory, DataBucket: dataBucket, FloorDir: filepath.Join(r.OptDir, "system", "guard")} |
| -- out.Identity = hostbox.IdentityProvider{AWSRegion: row.Cloud.Region, Account: row.Cloud.Account, Box: row.BoxID, Region: row.Region, Bucket: dataBucket, Namespace: row.Namespace, Certs: certs, Roster: authority, Evidence: productionBoxEvidence{store}} |
| -+ _, homeRegion, err := claimRegionBinding(row.Region, os.Getenv("CARLOS_REGION")) |
| -+ if err != nil { |
| -+ return out, err |
| -+ } |
| -+ out.Identity = hostbox.IdentityProvider{HomeRegion: homeRegion, AWSRegion: row.Cloud.Region, Account: row.Cloud.Account, Box: row.BoxID, Region: row.Region, Bucket: dataBucket, Namespace: row.Namespace, Certs: certs, Roster: authority, Evidence: productionBoxEvidence{store}} |
| - var actor hostbox.Actor |
| - if r.Mode == "instance-control" { |
| - out.cleanupEnrollment, err = verifyClaimCleanupEnrollment(ctx, out.Identity, directory) |
| -@@ -355,13 +359,15 @@ |
| - if p.Parks == nil || p.Parks.View == nil { |
| - return park.View{}, &ClaimDependencyError{Contracts: []string{"moves Plan 5 exact-version generation and files-manifest view"}} |
| - } |
| -- if _, err := p.Identity.Verify(ctx); err != nil { |
| -+ actor, err := p.Identity.Verify(ctx) |
| -+ if err != nil { |
| - return park.View{}, err |
| - } |
| -- v, err := p.Parks.View(ctx, o) |
| -+ v, err := p.Parks.View(ctx, park.HomeView(o, actor.HomeRegion)) |
| - if err != nil { |
| - return park.View{}, err |
| - } |
| -+ v.HomeRegion = actor.HomeRegion |
| - v.ArchivedIncarnations = claimArchivedLocations(o.Claim) |
| - v.RefusedLocations = map[string]error{} |
| - if _, err := claimLegacyIncarnation(o.Claim); err != nil { |
| -@@ -393,7 +399,7 @@ |
| - if err != nil { |
| - return err |
| - } |
| -- if o.Claim == nil || ob.Op != o.Claim.Op.ID || (ob.Owner != "box:"+a.Box && ob.Owner != "region:"+a.Region) || (ob.Box != "" && ob.Box != a.Box) || (ob.Region != "" && ob.Region != a.Region) { |
| -+ if o.Claim == nil || ob.Op != o.Claim.Op.ID || (ob.Owner != "box:"+a.Box && (ob.Owner != "region:"+ob.Region || !park.SameRegion(a.HomeRegion, ob.Region, a.Region))) || (ob.Box != "" && ob.Box != a.Box) || (ob.Region != "" && !park.SameRegion(a.HomeRegion, ob.Region, a.Region)) { |
| - return hostbox.ErrChanged |
| - } |
| - found := false |
| -@@ -450,7 +456,7 @@ |
| - if c.Op.Kind != sel.Kind || c.Op.ID != sel.Op || c.Op.Phase != sel.Phase || c.Holder.Incarnation != sel.Captured.Incarnation || c.Holder.Stamp != sel.Captured.Stamp || c.Holder.Session != sel.Session || c.Holder.Generation != sel.Generation { |
| - return hostbox.ErrChanged |
| - } |
| -- if until, ok := c.QuietAfter[sel.Region]; ok && !t.Now().After(until) { |
| -+ if until := park.QuietUntil(c.QuietAfter, actor.HomeRegion, sel.Region); !until.IsZero() && !t.Now().After(until) { |
| - return hostbox.ErrChanged |
| - } |
| - if err := t.Local(ctx, c, sel); err != nil { |
| ---- a/internal/host/claimcleanup_identity.go |
| -+++ b/internal/host/claimcleanup_identity.go |
| -@@ -40,6 +40,7 @@ |
| - if _, err := identity.Verify(ctx); err != nil { |
| - return nil, err |
| - } |
| -+ actor.HomeRegion = identity.HomeRegion |
| - actor.Eligible, actor.Quarantined = false, true |
| - return &claimCleanupEnrollment{actor: actor, directory: directory}, nil |
| - } |
| --- a/internal/host/claimassembly.go |
| +++ b/internal/host/claimassembly.go |
| @@ -156,12 +156,13 @@ |
| @@ -762,6 +646,27 @@ Expected first failure: claimRegionBinding and IdentityProvider.HomeRegion do no |
| return hostbox.ErrChanged |
| } |
| return nil |
| +--- a/internal/host/claimcleanup_identity.go |
| ++++ b/internal/host/claimcleanup_identity.go |
| +@@ -40,6 +40,7 @@ |
| + if _, err := identity.Verify(ctx); err != nil { |
| + return nil, err |
| + } |
| ++ actor.HomeRegion = identity.HomeRegion |
| + actor.Eligible, actor.Quarantined = false, true |
| + return &claimCleanupEnrollment{actor: actor, directory: directory}, nil |
| + } |
| +--- a/internal/host/claimconstruction.go |
| ++++ b/internal/host/claimconstruction.go |
| +@@ -258,7 +258,7 @@ |
| + var publisher ClaimFencedPublication |
| + var groupD claimGroupD |
| + if movesC1 { |
| +- serving = &claimServingOwners{Data: hostbox.RequestStore{Store: owners.Data, Check: func(ctx context.Context) error { _, err := owners.Identity.Verify(ctx); return err }}, Bucket: actor.Bucket, Region: actor.Region, Box: actor.Box, NewID: newID, Now: time.Now, Sources: owners.Serving} |
| ++ serving = &claimServingOwners{Data: hostbox.RequestStore{Store: owners.Data, Check: func(ctx context.Context) error { _, err := owners.Identity.Verify(ctx); return err }}, Bucket: actor.Bucket, Region: park.PlacementRegion(actor.Region), Box: actor.Box, NewID: newID, Now: time.Now, Sources: owners.Serving} |
| + groupD, err = claimGroupDWiring(claimEntryEnv(ctx), serving.owners(), owners.Moves, owners.Relocation) |
| + if err != nil { |
| + return nil, err |
| --- a/internal/host/claimlaunch_authority.go |
| +++ b/internal/host/claimlaunch_authority.go |
| @@ -3,6 +3,7 @@ |
| @@ -781,43 +686,143 @@ Expected first failure: claimRegionBinding and IdentityProvider.HomeRegion do no |
| return hostbox.ErrChanged |
| } |
| return nil |
| ---- a/internal/host/claimconstruction.go |
| -+++ b/internal/host/claimconstruction.go |
| -@@ -258,7 +258,7 @@ |
| - var publisher ClaimFencedPublication |
| - var groupD claimGroupD |
| - if movesC1 { |
| -- serving = &claimServingOwners{Data: hostbox.RequestStore{Store: owners.Data, Check: func(ctx context.Context) error { _, err := owners.Identity.Verify(ctx); return err }}, Bucket: actor.Bucket, Region: actor.Region, Box: actor.Box, NewID: newID, Now: time.Now, Sources: owners.Serving} |
| -+ serving = &claimServingOwners{Data: hostbox.RequestStore{Store: owners.Data, Check: func(ctx context.Context) error { _, err := owners.Identity.Verify(ctx); return err }}, Bucket: actor.Bucket, Region: park.PlacementRegion(actor.Region), Box: actor.Box, NewID: newID, Now: time.Now, Sources: owners.Serving} |
| - groupD, err = claimGroupDWiring(claimEntryEnv(ctx), serving.owners(), owners.Moves, owners.Relocation) |
| - if err != nil { |
| - return nil, err |
| ---- a/internal/host/movessources.go |
| -+++ b/internal/host/movessources.go |
| -@@ -586,7 +586,7 @@ |
| - if err != nil { |
| - return fmt.Errorf("the claim's MoveParks: %w", err) |
| - } |
| -- if engine.Bucket != cfg.OwnBucket || engine.Region != cfg.Region { |
| -+ if engine.Bucket != cfg.OwnBucket || !engine.OwnsRegion(park.PlacementRegion(cfg.Region)) { |
| - return fmt.Errorf("%w: the claim's park engine is %s in %q, this box's moves use %s in %q", hostbox.ErrChanged, engine.Bucket, engine.Region, cfg.OwnBucket, cfg.Region) |
| +--- a/internal/host/claimproduction.go |
| ++++ b/internal/host/claimproduction.go |
| +@@ -118,7 +118,11 @@ |
| + return out, hostwrite.ErrUnavailable |
| } |
| - if engine.Leases == nil || engine.View == nil || engine.Authorize == nil { |
| -@@ -646,6 +646,10 @@ |
| - if err != nil { |
| - return "", err |
| - } |
| -- return rr.row.Region, nil |
| -- } |
| --} |
| -+ _, _, err = claimRegionBinding(rr.row.Region, getenv("CARLOS_REGION")) |
| -+ if err != nil { |
| -+ return "", err |
| -+ } |
| -+ return park.PlacementRegion(getenv("CARLOS_REGION")), nil |
| + authority := productionClaimRoster{Directory: directory, DataBucket: dataBucket, FloorDir: filepath.Join(r.OptDir, "system", "guard")} |
| +- out.Identity = hostbox.IdentityProvider{AWSRegion: row.Cloud.Region, Account: row.Cloud.Account, Box: row.BoxID, Region: row.Region, Bucket: dataBucket, Namespace: row.Namespace, Certs: certs, Roster: authority, Evidence: productionBoxEvidence{store}} |
| ++ _, homeRegion, err := claimRegionBinding(row.Region, os.Getenv("CARLOS_REGION")) |
| ++ if err != nil { |
| ++ return out, err |
| + } |
| -+} |
| ---- /dev/null |
| ++ out.Identity = hostbox.IdentityProvider{HomeRegion: homeRegion, AWSRegion: row.Cloud.Region, Account: row.Cloud.Account, Box: row.BoxID, Region: row.Region, Bucket: dataBucket, Namespace: row.Namespace, Certs: certs, Roster: authority, Evidence: productionBoxEvidence{store}} |
| + var actor hostbox.Actor |
| + if r.Mode == "instance-control" { |
| + out.cleanupEnrollment, err = verifyClaimCleanupEnrollment(ctx, out.Identity, directory) |
| +@@ -355,13 +359,15 @@ |
| + if p.Parks == nil || p.Parks.View == nil { |
| + return park.View{}, &ClaimDependencyError{Contracts: []string{"moves Plan 5 exact-version generation and files-manifest view"}} |
| + } |
| +- if _, err := p.Identity.Verify(ctx); err != nil { |
| ++ actor, err := p.Identity.Verify(ctx) |
| ++ if err != nil { |
| + return park.View{}, err |
| + } |
| +- v, err := p.Parks.View(ctx, o) |
| ++ v, err := p.Parks.View(ctx, park.HomeView(o, actor.HomeRegion)) |
| + if err != nil { |
| + return park.View{}, err |
| + } |
| ++ v.HomeRegion = actor.HomeRegion |
| + v.ArchivedIncarnations = claimArchivedLocations(o.Claim) |
| + v.RefusedLocations = map[string]error{} |
| + if _, err := claimLegacyIncarnation(o.Claim); err != nil { |
| +@@ -393,7 +399,7 @@ |
| + if err != nil { |
| + return err |
| + } |
| +- if o.Claim == nil || ob.Op != o.Claim.Op.ID || (ob.Owner != "box:"+a.Box && ob.Owner != "region:"+a.Region) || (ob.Box != "" && ob.Box != a.Box) || (ob.Region != "" && ob.Region != a.Region) { |
| ++ if o.Claim == nil || ob.Op != o.Claim.Op.ID || (ob.Owner != "box:"+a.Box && (ob.Owner != "region:"+ob.Region || !park.SameRegion(a.HomeRegion, ob.Region, a.Region))) || (ob.Box != "" && ob.Box != a.Box) || (ob.Region != "" && !park.SameRegion(a.HomeRegion, ob.Region, a.Region)) { |
| + return hostbox.ErrChanged |
| + } |
| + found := false |
| +@@ -450,7 +456,7 @@ |
| + if c.Op.Kind != sel.Kind || c.Op.ID != sel.Op || c.Op.Phase != sel.Phase || c.Holder.Incarnation != sel.Captured.Incarnation || c.Holder.Stamp != sel.Captured.Stamp || c.Holder.Session != sel.Session || c.Holder.Generation != sel.Generation { |
| + return hostbox.ErrChanged |
| + } |
| +- if until, ok := c.QuietAfter[sel.Region]; ok && !t.Now().After(until) { |
| ++ if until := park.QuietUntil(c.QuietAfter, actor.HomeRegion, sel.Region); !until.IsZero() && !t.Now().After(until) { |
| + return hostbox.ErrChanged |
| + } |
| + if err := t.Local(ctx, c, sel); err != nil { |
| +--- /dev/null |
| ++++ b/internal/host/claimregion.go |
| +@@ -0,0 +1,20 @@ |
| ++package host |
| ++ |
| ++import ( |
| ++ "github.com/carlosframework/platform/internal/hostbox" |
| ++ "github.com/carlosframework/platform/internal/park" |
| ++) |
| ++ |
| ++// The signed roster retains the claim's engine name. CARLOS_REGION supplies |
| ++// this box's placement code; only its empty home placement binds an alias. |
| ++func claimRegionBinding(roster, code string) (engine, home string, err error) { |
| ++ engine = park.PlacementRegion(roster) |
| ++ placement := park.PlacementRegion(code) |
| ++ if roster == "" || engine == "" || placement == "" || (code != "" && engine != placement) { |
| ++ return "", "", hostbox.ErrChanged |
| ++ } |
| ++ if code == "" { |
| ++ home = engine |
| ++ } |
| ++ return engine, home, nil |
| ++} |
| +--- a/internal/host/movessources.go |
| ++++ b/internal/host/movessources.go |
| +@@ -586,7 +586,7 @@ |
| + if err != nil { |
| + return fmt.Errorf("the claim's MoveParks: %w", err) |
| + } |
| +- if engine.Bucket != cfg.OwnBucket || engine.Region != cfg.Region { |
| ++ if engine.Bucket != cfg.OwnBucket || !engine.OwnsRegion(park.PlacementRegion(cfg.Region)) { |
| + return fmt.Errorf("%w: the claim's park engine is %s in %q, this box's moves use %s in %q", hostbox.ErrChanged, engine.Bucket, engine.Region, cfg.OwnBucket, cfg.Region) |
| + } |
| + if engine.Leases == nil || engine.View == nil || engine.Authorize == nil { |
| +@@ -646,6 +646,10 @@ |
| + if err != nil { |
| + return "", err |
| + } |
| +- return rr.row.Region, nil |
| +- } |
| +-} |
| ++ _, _, err = claimRegionBinding(rr.row.Region, getenv("CARLOS_REGION")) |
| ++ if err != nil { |
| ++ return "", err |
| ++ } |
| ++ return park.PlacementRegion(getenv("CARLOS_REGION")), nil |
| ++ } |
| ++} |
| +--- a/internal/hostbox/identity.go |
| ++++ b/internal/hostbox/identity.go |
| +@@ -3,6 +3,7 @@ |
| + import ( |
| + "context" |
| + "crypto/x509" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + |
| + "github.com/carlosframework/platform/internal/boxid" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| +@@ -12,6 +13,7 @@ |
| + // Actor is the verified projection of the sole control/host-actors.json roster. |
| + // Roster authenticates that document and its revision before returning a row. |
| + type Actor struct { |
| ++ HomeRegion string |
| + ControlBucket string |
| + Box, Enrollment string |
| + Revision uint64 |
| +@@ -31,6 +33,7 @@ |
| + } |
| + |
| + type IdentityProvider struct { |
| ++ HomeRegion string |
| + AWSRegion string |
| + Roster Roster |
| + Evidence BoxEvidence |
| +@@ -67,6 +70,10 @@ |
| + if ev.Box != p.Box || ev.Cloud != cloud { |
| + return Actor{}, ErrChanged |
| + } |
| ++ if p.HomeRegion != "" && (p.HomeRegion != a.Region || park.PlacementRegion(p.HomeRegion) != p.HomeRegion) { |
| ++ return Actor{}, ErrChanged |
| ++ } |
| ++ a.HomeRegion = p.HomeRegion |
| + return a, nil |
| + } |
| + |
| +--- /dev/null |
| +++ b/internal/park/home_view.go |
| @@ -0,0 +1,23 @@ |
| +package park |
| @@ -984,45 +989,6 @@ Expected first failure: The console helper and BeginClaimMove home argument do n |
| for _, region := range []string{cfg.Region, cfg.HomeRegion, cfg.HibernateRegion} { |
| if region = strings.TrimSpace(region); region != "" { |
| return region |
| ---- a/internal/console/apiinstances.go |
| -+++ b/internal/console/apiinstances.go |
| -@@ -1395,10 +1395,7 @@ |
| - claimRequest, createErr = s.beginClaimCreate(r.Context(), acct, app, host, principal, func(now time.Time, acquisition string) (hostops.Record, error) { |
| - rec.CreatedAt = time.Time{} |
| - rec.Acquisition = acquisition |
| -- region := s.cfg.dbClaimRegion() |
| -- if rec.Region != "" { |
| -- region = rec.Region |
| -- } |
| -+ region := s.cfg.claimPlacementRegion(rec.Region) |
| - prepared, err := hostops.PrepareInstance(rec, now, region) |
| - if err == nil { |
| - rec.CreatedAt = now |
| ---- a/internal/console/apisidecars.go |
| -+++ b/internal/console/apisidecars.go |
| -@@ -343,10 +343,7 @@ |
| - request, err := s.beginClaimCreate(r.Context(), acct, app, rec.Host, principal, func(now time.Time, acquisition string) (hostops.Record, error) { |
| - rec.CreatedAt = time.Time{} |
| - rec.Acquisition = acquisition |
| -- region := s.cfg.dbClaimRegion() |
| -- if rec.Region != "" { |
| -- region = rec.Region |
| -- } |
| -+ region := s.cfg.claimPlacementRegion(rec.Region) |
| - prepared, err := hostops.PrepareSidecar(rec, now, region) |
| - if err == nil { |
| - rec.CreatedAt = now |
| ---- a/internal/console/claim_first_route.go |
| -+++ b/internal/console/claim_first_route.go |
| -@@ -17,7 +17,7 @@ |
| - } |
| - request, err := s.beginClaimCreate(ctx, acct, app, host, principal, func(now time.Time, acquisition string) (hostops.Record, error) { |
| - rec := provision.Record{Schema: 1, Host: host, App: app, Account: acct.Sqid, Channel: channel, Kind: "static", CreatedBy: principal, Acquisition: acquisition} |
| -- record, err := hostops.PrepareInstance(rec, now, s.cfg.dbClaimRegion()) |
| -+ record, err := hostops.PrepareInstance(rec, now, s.cfg.claimPlacementRegion(rec.Region)) |
| - record.VerifiedOwnership = true |
| - return record, err |
| - }) |
| --- a/internal/console/apiinstancemove.go |
| +++ b/internal/console/apiinstancemove.go |
| @@ -7,6 +7,7 @@ |
| @@ -1077,6 +1043,45 @@ Expected first failure: The console helper and BeginClaimMove home argument do n |
| } |
| // sourceEnrollment is the enrollment of the box that executes the claim, and |
| +--- a/internal/console/apiinstances.go |
| ++++ b/internal/console/apiinstances.go |
| +@@ -1395,10 +1395,7 @@ |
| + claimRequest, createErr = s.beginClaimCreate(r.Context(), acct, app, host, principal, func(now time.Time, acquisition string) (hostops.Record, error) { |
| + rec.CreatedAt = time.Time{} |
| + rec.Acquisition = acquisition |
| +- region := s.cfg.dbClaimRegion() |
| +- if rec.Region != "" { |
| +- region = rec.Region |
| +- } |
| ++ region := s.cfg.claimPlacementRegion(rec.Region) |
| + prepared, err := hostops.PrepareInstance(rec, now, region) |
| + if err == nil { |
| + rec.CreatedAt = now |
| +--- a/internal/console/apisidecars.go |
| ++++ b/internal/console/apisidecars.go |
| +@@ -343,10 +343,7 @@ |
| + request, err := s.beginClaimCreate(r.Context(), acct, app, rec.Host, principal, func(now time.Time, acquisition string) (hostops.Record, error) { |
| + rec.CreatedAt = time.Time{} |
| + rec.Acquisition = acquisition |
| +- region := s.cfg.dbClaimRegion() |
| +- if rec.Region != "" { |
| +- region = rec.Region |
| +- } |
| ++ region := s.cfg.claimPlacementRegion(rec.Region) |
| + prepared, err := hostops.PrepareSidecar(rec, now, region) |
| + if err == nil { |
| + rec.CreatedAt = now |
| +--- a/internal/console/claim_first_route.go |
| ++++ b/internal/console/claim_first_route.go |
| +@@ -17,7 +17,7 @@ |
| + } |
| + request, err := s.beginClaimCreate(ctx, acct, app, host, principal, func(now time.Time, acquisition string) (hostops.Record, error) { |
| + rec := provision.Record{Schema: 1, Host: host, App: app, Account: acct.Sqid, Channel: channel, Kind: "static", CreatedBy: principal, Acquisition: acquisition} |
| +- record, err := hostops.PrepareInstance(rec, now, s.cfg.dbClaimRegion()) |
| ++ record, err := hostops.PrepareInstance(rec, now, s.cfg.claimPlacementRegion(rec.Region)) |
| + record.VerifiedOwnership = true |
| + return record, err |
| + }) |
| --- a/internal/hostops/move_enrollment.go |
| +++ b/internal/hostops/move_enrollment.go |
| @@ -3,6 +3,7 @@ |
| @@ -1266,6 +1271,44 @@ Expected first failure: The home-source and home-target loss intent cases refuse |
| <!-- apply: E implementation --> |
| ```diff |
| +--- a/internal/hostops/move_fence_release.go |
| ++++ b/internal/hostops/move_fence_release.go |
| +@@ -7,6 +7,7 @@ |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostops/internal/fencerelease" |
| + "github.com/carlosframework/platform/internal/moves" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/release" |
| + ) |
| +@@ -94,7 +95,7 @@ |
| + return writeRecord() |
| + } |
| + h := o.Claim.Holder |
| +- r := hostclaim.FencedTargetRelease{OpID: m.ID, RollbackID: rb.ID, Target: o.Claim.Op.Intent["target"], TargetRegion: m.To, SourceRegion: m.From, |
| ++ r := hostclaim.FencedTargetRelease{OpID: m.ID, RollbackID: rb.ID, Target: o.Claim.Op.Intent["target"], TargetRegion: park.PlacementRegion(m.To), SourceRegion: park.PlacementRegion(m.From), |
| + Location: moves.TargetLocationScope(*h, fence.ObservedAt()), Lane: moves.StagingScope(o.Host, m.ID, h.Incarnation, fence.ObservedAt()), NewID: moveLossNewID(ref.SHA256), Fence: fence} |
| + if err := CheckCreateFreeze(ctx, g.w.raw, g.w.pub); err != nil { |
| + return err |
| +--- a/internal/hostops/move_loss_admission.go |
| ++++ b/internal/hostops/move_loss_admission.go |
| +@@ -2,6 +2,7 @@ |
| + |
| + import ( |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "strconv" |
| + "strings" |
| + ) |
| +@@ -41,7 +42,7 @@ |
| + c.Op.ID != in.Move || c.Op.Kind != hostclaim.KindRestoring || c.Op.Phase != hostclaim.PhaseProbing || |
| + c.Op.Intent["rollback"] != in.Rollback || c.Op.Exec != in.Source || c.Op.Intent["source"] != in.Source || |
| + c.Op.Intent["target"] != in.Target || c.Op.Intent["to"] != in.TargetRegion || |
| +- !hostclaim.LostTargetReleased(&c, loss.ID) || a.Box != in.Source || a.Region != in.SourceRegion || |
| ++ !hostclaim.LostTargetReleased(&c, loss.ID) || a.Box != in.Source || !park.SameRegion(c.Op.Intent["home_region"], a.Region, in.SourceRegion) || |
| + a.Mode != "cold" || a.Coverage != "" || a.KeepStamp || a.PredecessorCertified { |
| + return hostclaim.ErrLossRecord |
| + } |
| --- a/internal/hostops/move_loss_owner.go |
| +++ b/internal/hostops/move_loss_owner.go |
| @@ -10,6 +10,7 @@ |
| @@ -1292,44 +1335,6 @@ Expected first failure: The home-source and home-target loss intent cases refuse |
| switch m.State { |
| case provision.MoveLanding: |
| if op.Kind != hostclaim.KindLanding || m.Rollback != nil { |
| ---- a/internal/hostops/move_loss_admission.go |
| -+++ b/internal/hostops/move_loss_admission.go |
| -@@ -2,6 +2,7 @@ |
| - |
| - import ( |
| - "github.com/carlosframework/platform/internal/hostclaim" |
| -+ "github.com/carlosframework/platform/internal/park" |
| - "strconv" |
| - "strings" |
| - ) |
| -@@ -41,7 +42,7 @@ |
| - c.Op.ID != in.Move || c.Op.Kind != hostclaim.KindRestoring || c.Op.Phase != hostclaim.PhaseProbing || |
| - c.Op.Intent["rollback"] != in.Rollback || c.Op.Exec != in.Source || c.Op.Intent["source"] != in.Source || |
| - c.Op.Intent["target"] != in.Target || c.Op.Intent["to"] != in.TargetRegion || |
| -- !hostclaim.LostTargetReleased(&c, loss.ID) || a.Box != in.Source || a.Region != in.SourceRegion || |
| -+ !hostclaim.LostTargetReleased(&c, loss.ID) || a.Box != in.Source || !park.SameRegion(c.Op.Intent["home_region"], a.Region, in.SourceRegion) || |
| - a.Mode != "cold" || a.Coverage != "" || a.KeepStamp || a.PredecessorCertified { |
| - return hostclaim.ErrLossRecord |
| - } |
| ---- a/internal/hostops/move_fence_release.go |
| -+++ b/internal/hostops/move_fence_release.go |
| -@@ -7,6 +7,7 @@ |
| - "github.com/carlosframework/platform/internal/hostclaim" |
| - "github.com/carlosframework/platform/internal/hostops/internal/fencerelease" |
| - "github.com/carlosframework/platform/internal/moves" |
| -+ "github.com/carlosframework/platform/internal/park" |
| - "github.com/carlosframework/platform/internal/provision" |
| - "github.com/carlosframework/platform/internal/release" |
| - ) |
| -@@ -94,7 +95,7 @@ |
| - return writeRecord() |
| - } |
| - h := o.Claim.Holder |
| -- r := hostclaim.FencedTargetRelease{OpID: m.ID, RollbackID: rb.ID, Target: o.Claim.Op.Intent["target"], TargetRegion: m.To, SourceRegion: m.From, |
| -+ r := hostclaim.FencedTargetRelease{OpID: m.ID, RollbackID: rb.ID, Target: o.Claim.Op.Intent["target"], TargetRegion: park.PlacementRegion(m.To), SourceRegion: park.PlacementRegion(m.From), |
| - Location: moves.TargetLocationScope(*h, fence.ObservedAt()), Lane: moves.StagingScope(o.Host, m.ID, h.Incarnation, fence.ObservedAt()), NewID: moveLossNewID(ref.SHA256), Fence: fence} |
| - if err := CheckCreateFreeze(ctx, g.w.raw, g.w.pub); err != nil { |
| - return err |
| ``` |
| ## Task F: Preserve mixed home history in generation selection |
| @@ -1403,52 +1408,66 @@ Expected first failure: HolderRegionView is missing and the home target policy r |
| <!-- apply: F implementation --> |
| ```diff |
| ---- a/internal/park/home_view.go |
| -+++ b/internal/park/home_view.go |
| -@@ -1,22 +1,32 @@ |
| - package park |
| +--- a/internal/console/apidbclaim.go |
| ++++ b/internal/console/apidbclaim.go |
| +@@ -202,7 +202,7 @@ |
| + out := dbdoor.ListResponse{Protocol: 2, Plan: true, Files: []dbdoor.LTXFile{}} |
| + h := g.claim.Claim.Holder |
| + plan, err := genselect.Select(ctx, s.cfg.DBVersions, genselect.Request{ |
| +- Bucket: s.cfg.HibernateBucket, Region: s.cfg.dbClaimRegion(), Host: g.host, Holder: *h, |
| ++ Bucket: s.cfg.HibernateBucket, Region: s.cfg.dbClaimRegion(), Host: g.host, Holder: park.HolderRegionView(*h, s.cfg.HomeRegion, s.cfg.dbClaimRegion()), |
| + }) |
| + if err != nil { |
| + return dbdoor.ListResponse{}, errDBClaimChanged |
| +--- a/internal/host/claimassembly.go |
| ++++ b/internal/host/claimassembly.go |
| +@@ -178,7 +178,7 @@ |
| + a.Provision = a.Reconcile.Configure |
| + check := claimBoundIdentity(a.Identity.Verify, actor) |
| + data := hostbox.RequestStore{Store: a.Data, Check: check} |
| +- writer := hostbox.Writer{Loss: a.LossAdmission, Claims: a.Claims, Data: data, Bucket: actor.Bucket, Region: park.PlacementRegion(actor.Region), Box: actor.Box, AdmissionEvidence: a.AdmissionEvidence, PublicationEvidence: a.PublicationEvidence, NewID: a.NewID} |
| ++ writer := hostbox.Writer{HomeRegion: actor.HomeRegion, Loss: a.LossAdmission, Claims: a.Claims, Data: data, Bucket: actor.Bucket, Region: park.PlacementRegion(actor.Region), Box: actor.Box, AdmissionEvidence: a.AdmissionEvidence, PublicationEvidence: a.PublicationEvidence, NewID: a.NewID} |
| + a.bindClaimWakeStop() |
| + if local, ok := a.Local.(*productionClaimLocal); ok { |
| + if err := local.bind(ctx, &a); err != nil { |
| +--- a/internal/host/claimconstruction.go |
| ++++ b/internal/host/claimconstruction.go |
| +@@ -258,7 +258,7 @@ |
| + var publisher ClaimFencedPublication |
| + var groupD claimGroupD |
| + if movesC1 { |
| +- serving = &claimServingOwners{Data: hostbox.RequestStore{Store: owners.Data, Check: func(ctx context.Context) error { _, err := owners.Identity.Verify(ctx); return err }}, Bucket: actor.Bucket, Region: park.PlacementRegion(actor.Region), Box: actor.Box, NewID: newID, Now: time.Now, Sources: owners.Serving} |
| ++ serving = &claimServingOwners{HomeRegion: actor.HomeRegion, Data: hostbox.RequestStore{Store: owners.Data, Check: func(ctx context.Context) error { _, err := owners.Identity.Verify(ctx); return err }}, Bucket: actor.Bucket, Region: park.PlacementRegion(actor.Region), Box: actor.Box, NewID: newID, Now: time.Now, Sources: owners.Serving} |
| + groupD, err = claimGroupDWiring(claimEntryEnv(ctx), serving.owners(), owners.Moves, owners.Relocation) |
| + if err != nil { |
| + return nil, err |
| +--- a/internal/host/claimowners.go |
| ++++ b/internal/host/claimowners.go |
| +@@ -4,6 +4,7 @@ |
| + "context" |
| + "encoding/json" |
| + "errors" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "path/filepath" |
| + "reflect" |
| + "strconv" |
| +@@ -306,6 +307,7 @@ |
| + // acknowledged (INV-11), and publication evidence binds the exact marker |
| + // version this process wrote and the exact root version it named. |
| + type claimServingOwners struct { |
| ++ HomeRegion string |
| + Data genselect.Reader |
| + Bucket, Region, Box string |
| + NewID func() string |
| +@@ -330,7 +332,7 @@ |
| + } |
| - import "github.com/carlosframework/platform/internal/hostclaim" |
| -+ |
| -+// HolderRegionView is for read-only generation selection. Never persist it |
| -+// or use it to validate signed stop/loss evidence; those keep exact tuples. |
| -+func HolderRegionView(h hostclaim.Holder, home, region string) hostclaim.Holder { |
| -+ if home == "" || !SameRegion(home, region, HomeRegion) { |
| -+ return h |
| -+ } |
| -+ h.Admissions = append([]hostclaim.AdmissionRecord(nil), h.Admissions...) |
| -+ for i := range h.Admissions { |
| -+ a := &h.Admissions[i] |
| -+ if SameRegion(home, a.Region, region) { |
| -+ a.Region = region |
| -+ } |
| -+ if SameRegion(home, a.PredecessorRegion, region) { |
| -+ a.PredecessorRegion = region |
| -+ } |
| -+ } |
| -+ return h |
| -+} |
| + func (s *claimServingOwners) baseline(ctx context.Context, o hostclaim.Observed) (*genselect.Plan, error) { |
| +- return genselect.Select(ctx, s.Data, genselect.Request{Bucket: s.Bucket, Region: s.Region, Host: o.Host, Holder: *o.Claim.Holder}) |
| ++ return genselect.Select(ctx, s.Data, genselect.Request{Bucket: s.Bucket, Region: s.Region, Host: o.Host, Holder: park.HolderRegionView(*o.Claim.Holder, s.HomeRegion, s.Region)}) |
| + } |
| - func HomeView(o hostclaim.Observed, home string) hostclaim.Observed { |
| - if home == "" || o.Claim == nil || o.Claim.Holder == nil { |
| - return o |
| - } |
| -- c, h := *o.Claim, *o.Claim.Holder |
| -- h.Admissions = append([]hostclaim.AdmissionRecord(nil), h.Admissions...) |
| -- for i := range h.Admissions { |
| -- a := &h.Admissions[i] |
| -- if a.Region == home { |
| -- a.Region = HomeRegion |
| -- } |
| -- if a.PredecessorRegion == home { |
| -- a.PredecessorRegion = HomeRegion |
| -- } |
| -- } |
| -+ c := *o.Claim |
| -+ h := HolderRegionView(*c.Holder, home, HomeRegion) |
| - c.Holder = &h |
| - o.Claim = &c |
| - return o |
| + // claimEmptyBootstrap: a created incarnation before its first writer. |
| --- a/internal/host/movespolicy.go |
| +++ b/internal/host/movespolicy.go |
| @@ -5,6 +5,7 @@ |
| @@ -1558,66 +1577,52 @@ Expected first failure: HolderRegionView is missing and the home target policy r |
| } |
| func (w Writer) Admit(ctx context.Context, observed hostclaim.Observed, in hostclaim.Admission, baseline *genselect.Plan) (hostclaim.Observed, error) { |
| ---- a/internal/host/claimassembly.go |
| -+++ b/internal/host/claimassembly.go |
| -@@ -178,7 +178,7 @@ |
| - a.Provision = a.Reconcile.Configure |
| - check := claimBoundIdentity(a.Identity.Verify, actor) |
| - data := hostbox.RequestStore{Store: a.Data, Check: check} |
| -- writer := hostbox.Writer{Loss: a.LossAdmission, Claims: a.Claims, Data: data, Bucket: actor.Bucket, Region: park.PlacementRegion(actor.Region), Box: actor.Box, AdmissionEvidence: a.AdmissionEvidence, PublicationEvidence: a.PublicationEvidence, NewID: a.NewID} |
| -+ writer := hostbox.Writer{HomeRegion: actor.HomeRegion, Loss: a.LossAdmission, Claims: a.Claims, Data: data, Bucket: actor.Bucket, Region: park.PlacementRegion(actor.Region), Box: actor.Box, AdmissionEvidence: a.AdmissionEvidence, PublicationEvidence: a.PublicationEvidence, NewID: a.NewID} |
| - a.bindClaimWakeStop() |
| - if local, ok := a.Local.(*productionClaimLocal); ok { |
| - if err := local.bind(ctx, &a); err != nil { |
| ---- a/internal/host/claimowners.go |
| -+++ b/internal/host/claimowners.go |
| -@@ -4,6 +4,7 @@ |
| - "context" |
| - "encoding/json" |
| - "errors" |
| -+ "github.com/carlosframework/platform/internal/park" |
| - "path/filepath" |
| - "reflect" |
| - "strconv" |
| -@@ -306,6 +307,7 @@ |
| - // acknowledged (INV-11), and publication evidence binds the exact marker |
| - // version this process wrote and the exact root version it named. |
| - type claimServingOwners struct { |
| -+ HomeRegion string |
| - Data genselect.Reader |
| - Bucket, Region, Box string |
| - NewID func() string |
| -@@ -330,7 +332,7 @@ |
| - } |
| +--- a/internal/park/home_view.go |
| ++++ b/internal/park/home_view.go |
| +@@ -1,22 +1,32 @@ |
| + package park |
| - func (s *claimServingOwners) baseline(ctx context.Context, o hostclaim.Observed) (*genselect.Plan, error) { |
| -- return genselect.Select(ctx, s.Data, genselect.Request{Bucket: s.Bucket, Region: s.Region, Host: o.Host, Holder: *o.Claim.Holder}) |
| -+ return genselect.Select(ctx, s.Data, genselect.Request{Bucket: s.Bucket, Region: s.Region, Host: o.Host, Holder: park.HolderRegionView(*o.Claim.Holder, s.HomeRegion, s.Region)}) |
| - } |
| + import "github.com/carlosframework/platform/internal/hostclaim" |
| ++ |
| ++// HolderRegionView is for read-only generation selection. Never persist it |
| ++// or use it to validate signed stop/loss evidence; those keep exact tuples. |
| ++func HolderRegionView(h hostclaim.Holder, home, region string) hostclaim.Holder { |
| ++ if home == "" || !SameRegion(home, region, HomeRegion) { |
| ++ return h |
| ++ } |
| ++ h.Admissions = append([]hostclaim.AdmissionRecord(nil), h.Admissions...) |
| ++ for i := range h.Admissions { |
| ++ a := &h.Admissions[i] |
| ++ if SameRegion(home, a.Region, region) { |
| ++ a.Region = region |
| ++ } |
| ++ if SameRegion(home, a.PredecessorRegion, region) { |
| ++ a.PredecessorRegion = region |
| ++ } |
| ++ } |
| ++ return h |
| ++} |
| - // claimEmptyBootstrap: a created incarnation before its first writer. |
| ---- a/internal/host/claimconstruction.go |
| -+++ b/internal/host/claimconstruction.go |
| -@@ -258,7 +258,7 @@ |
| - var publisher ClaimFencedPublication |
| - var groupD claimGroupD |
| - if movesC1 { |
| -- serving = &claimServingOwners{Data: hostbox.RequestStore{Store: owners.Data, Check: func(ctx context.Context) error { _, err := owners.Identity.Verify(ctx); return err }}, Bucket: actor.Bucket, Region: park.PlacementRegion(actor.Region), Box: actor.Box, NewID: newID, Now: time.Now, Sources: owners.Serving} |
| -+ serving = &claimServingOwners{HomeRegion: actor.HomeRegion, Data: hostbox.RequestStore{Store: owners.Data, Check: func(ctx context.Context) error { _, err := owners.Identity.Verify(ctx); return err }}, Bucket: actor.Bucket, Region: park.PlacementRegion(actor.Region), Box: actor.Box, NewID: newID, Now: time.Now, Sources: owners.Serving} |
| - groupD, err = claimGroupDWiring(claimEntryEnv(ctx), serving.owners(), owners.Moves, owners.Relocation) |
| - if err != nil { |
| - return nil, err |
| ---- a/internal/console/apidbclaim.go |
| -+++ b/internal/console/apidbclaim.go |
| -@@ -202,7 +202,7 @@ |
| - out := dbdoor.ListResponse{Protocol: 2, Plan: true, Files: []dbdoor.LTXFile{}} |
| - h := g.claim.Claim.Holder |
| - plan, err := genselect.Select(ctx, s.cfg.DBVersions, genselect.Request{ |
| -- Bucket: s.cfg.HibernateBucket, Region: s.cfg.dbClaimRegion(), Host: g.host, Holder: *h, |
| -+ Bucket: s.cfg.HibernateBucket, Region: s.cfg.dbClaimRegion(), Host: g.host, Holder: park.HolderRegionView(*h, s.cfg.HomeRegion, s.cfg.dbClaimRegion()), |
| - }) |
| - if err != nil { |
| - return dbdoor.ListResponse{}, errDBClaimChanged |
| + func HomeView(o hostclaim.Observed, home string) hostclaim.Observed { |
| + if home == "" || o.Claim == nil || o.Claim.Holder == nil { |
| + return o |
| + } |
| +- c, h := *o.Claim, *o.Claim.Holder |
| +- h.Admissions = append([]hostclaim.AdmissionRecord(nil), h.Admissions...) |
| +- for i := range h.Admissions { |
| +- a := &h.Admissions[i] |
| +- if a.Region == home { |
| +- a.Region = HomeRegion |
| +- } |
| +- if a.PredecessorRegion == home { |
| +- a.PredecessorRegion = HomeRegion |
| +- } |
| +- } |
| ++ c := *o.Claim |
| ++ h := HolderRegionView(*c.Holder, home, HomeRegion) |
| + c.Holder = &h |
| + o.Claim = &c |
| + return o |
| --- a/internal/park/remote.go |
| +++ b/internal/park/remote.go |
| @@ -552,7 +552,17 @@ |
| @@ -1734,34 +1739,6 @@ Expected first failure: A legacy named-home quiet bound fails to block the @home |
| <!-- apply: G implementation --> |
| ```diff |
| ---- a/internal/host/movesreport.go |
| -+++ b/internal/host/movesreport.go |
| -@@ -6,6 +6,7 @@ |
| - "encoding/json" |
| - "errors" |
| - "fmt" |
| -+ "github.com/carlosframework/platform/internal/park" |
| - "io" |
| - "io/fs" |
| - "log" |
| -@@ -245,7 +246,7 @@ |
| - rep.Checks[moves.CheckOneOwner] = checkOneOwner(fleet, rec.Move.To) |
| - rep.Checks[moves.CheckClaim] = w.checkClaim(ctx, obs, rec) |
| - rep.Checks[moves.CheckLane] = w.checkLane(ctx, rec, fleet) |
| -- rep.Checks[moves.CheckQuiet] = checkQuiet(obs, rec.Move.To, now) |
| -+ rep.Checks[moves.CheckQuiet] = checkQuiet(obs, park.PlacementRegion(rec.Move.To), now) |
| - rep.Checks[moves.CheckArmed] = checkArmed(cfg) |
| - |
| - // T11 from measured rates: this box's own, and the source's cross rate. |
| -@@ -771,7 +772,7 @@ |
| - |
| - // checkQuiet is T12: the claim's quiet_after for the target region passed. |
| - func checkQuiet(obs hostclaim.Observed, region string, now time.Time) moves.Check { |
| -- if until, ok := obs.Claim.QuietAfter[region]; ok && now.Before(until) { |
| -+ if until := park.QuietUntil(obs.Claim.QuietAfter, obs.Claim.Op.Intent["home_region"], region); !until.IsZero() && now.Before(until) { |
| - return moves.Check{Reason: t12NotQuiet(region, until)} |
| - } |
| - return moves.Check{OK: true} |
| --- a/internal/console/movesrecovery.go |
| +++ b/internal/console/movesrecovery.go |
| @@ -8,6 +8,7 @@ |
| @@ -1876,6 +1853,34 @@ Expected first failure: A legacy named-home quiet bound fails to block the @home |
| if err != nil { |
| return err |
| } |
| +--- a/internal/host/movesreport.go |
| ++++ b/internal/host/movesreport.go |
| +@@ -6,6 +6,7 @@ |
| + "encoding/json" |
| + "errors" |
| + "fmt" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "io" |
| + "io/fs" |
| + "log" |
| +@@ -245,7 +246,7 @@ |
| + rep.Checks[moves.CheckOneOwner] = checkOneOwner(fleet, rec.Move.To) |
| + rep.Checks[moves.CheckClaim] = w.checkClaim(ctx, obs, rec) |
| + rep.Checks[moves.CheckLane] = w.checkLane(ctx, rec, fleet) |
| +- rep.Checks[moves.CheckQuiet] = checkQuiet(obs, rec.Move.To, now) |
| ++ rep.Checks[moves.CheckQuiet] = checkQuiet(obs, park.PlacementRegion(rec.Move.To), now) |
| + rep.Checks[moves.CheckArmed] = checkArmed(cfg) |
| + |
| + // T11 from measured rates: this box's own, and the source's cross rate. |
| +@@ -771,7 +772,7 @@ |
| + |
| + // checkQuiet is T12: the claim's quiet_after for the target region passed. |
| + func checkQuiet(obs hostclaim.Observed, region string, now time.Time) moves.Check { |
| +- if until, ok := obs.Claim.QuietAfter[region]; ok && now.Before(until) { |
| ++ if until := park.QuietUntil(obs.Claim.QuietAfter, obs.Claim.Op.Intent["home_region"], region); !until.IsZero() && now.Before(until) { |
| + return moves.Check{Reason: t12NotQuiet(region, until)} |
| + } |
| + return moves.Check{OK: true} |
| ``` |
| ## Task G2: Map verified target fences through the operation home binding |
| @@ -1972,6 +1977,35 @@ Expected first failure: Target-home recovery rejects the verified named roster f |
| <!-- apply: G2 implementation --> |
| ```diff |
| +--- a/internal/host/claimhandover_recovery.go |
| ++++ b/internal/host/claimhandover_recovery.go |
| +@@ -12,6 +12,7 @@ |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/moves" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "strconv" |
| + ) |
| +@@ -72,7 +73,7 @@ |
| + if err != nil { |
| + return err |
| + } |
| +- if verified.Box() != p.Box || verified.Region() != op.Intent["to"] { |
| ++ if verified.Box() != p.Box || !park.SameRegion(op.Intent["home_region"], verified.Region(), op.Intent["to"]) { |
| + return hostclaim.ErrFence |
| + } |
| + return nil |
| +--- a/internal/host/claimmovefence.go |
| ++++ b/internal/host/claimmovefence.go |
| +@@ -36,6 +36,6 @@ |
| + if json.Unmarshal([]byte(o.Claim.Op.Journal["fence."+o.Claim.Op.Intent["target"]]), &ref) != nil { |
| + return hostclaim.ReturnFence{}, hostclaim.ErrFence |
| + } |
| +- return hostclaim.VerifyMoveTargetFence(ctx, store, o, ref, hostfence.ClaimFenceAuthority{Directory: authority.Directory, Now: time.Now}) |
| ++ return hostclaim.VerifyMoveTargetFence(ctx, store, o, ref, hostops.MoveFenceRegions{Authority: hostfence.ClaimFenceAuthority{Directory: authority.Directory, Now: time.Now}, HomeRegion: o.Claim.Op.Intent["home_region"]}) |
| + } |
| + } |
| --- /dev/null |
| +++ b/internal/hostops/move_fence_regions.go |
| @@ -0,0 +1,34 @@ |
| @@ -2020,40 +2054,1111 @@ Expected first failure: Target-home recovery rejects the verified named roster f |
| if err != nil { |
| return err |
| } |
| ---- a/internal/host/claimmovefence.go |
| -+++ b/internal/host/claimmovefence.go |
| -@@ -36,6 +36,6 @@ |
| - if json.Unmarshal([]byte(o.Claim.Op.Journal["fence."+o.Claim.Op.Intent["target"]]), &ref) != nil { |
| - return hostclaim.ReturnFence{}, hostclaim.ErrFence |
| - } |
| -- return hostclaim.VerifyMoveTargetFence(ctx, store, o, ref, hostfence.ClaimFenceAuthority{Directory: authority.Directory, Now: time.Now}) |
| -+ return hostclaim.VerifyMoveTargetFence(ctx, store, o, ref, hostops.MoveFenceRegions{Authority: hostfence.ClaimFenceAuthority{Directory: authority.Directory, Now: time.Now}, HomeRegion: o.Claim.Op.Intent["home_region"]}) |
| - } |
| +``` |
| + |
| +## Task G3: Close the five round-one comparison gaps |
| + |
| +Use the same nonempty alias rule at every record-to-engine comparison. `PlacementMatches` maps only its placement argument; `BoundHome` combines the verified local and admitted-operation names for history reads, refusing invalid or contradictory bindings. Ownership continues to use the local binding alone. |
| + |
| +The production view projects remote home admissions with the operation binding. `armMovesParks` copies the local binding into `moveCfg.ParkHomeRegion`; installRun, sourceFinal, rollbackRun and laneParkTarget pass it into every engine. Engine.observe retains the operation binding on named workers. Chore filters use the same rule. The executable patches include the overlapping moves-home-parks placement mappings because this pinned main does not yet contain that prerequisite. |
| + |
| +The console binds legacy admissions only to its configured home bucket's HibernateRegion when HomeRegion and the placement are empty. It retains the unresolved-startup refusal. The signed receipt comparison uses SameRegion; canonical bytes, exact roster name and signature checks remain unchanged. |
| + |
| +Pre-flip recovery returns the verified roster name to ConvertMovingToRelocating and keeps empty placement codes in records. Both Convert and RecoverTerminatedMovingHost compare the record with the verified fence through PlacementMatches. CheckMovingBeforeFlip compares the target and source through the same rule. MoveRelocatingDecision validates placement codes, which legitimately include empty; putLifecycle decodes an omitted region as that home code. None of the engine primitives accepts an empty region. |
| + |
| +Focused command: `go test ./internal/park ./internal/hostops ./internal/host ./internal/console -run TestPlan13 -count=1`. |
| + |
| +Expected red results: lost remote view binding, refused home history, missing moves engine bindings, a home lane left queued, valid home receipt refused, and pre-flip conversions refused in both directions. The console tests initially need the new configuration helper; independently running the new unfinished-selection test against rev 1 produces ErrDisagreement. The existing TestDoorPlanUnnamedHomeRegion remains unchanged and is part of final verification. |
| + |
| +### Failing tests |
| + |
| +<!-- apply: G3 tests --> |
| +```diff |
| +--- /dev/null |
| ++++ b/internal/console/plan13_legacy_test.go |
| +@@ -0,0 +1,52 @@ |
| ++package console |
| ++ |
| ++import ( |
| ++ "github.com/carlosframework/platform/internal/hostclaim" |
| ++ "github.com/carlosframework/platform/internal/park" |
| ++ "testing" |
| ++) |
| ++ |
| ++func TestPlan13VerifiedLegacyHomeRead(t *testing.T) { |
| ++ for _, which := range []string{"home", "named", "no-bucket", "invalid", "explicit"} { |
| ++ cfg := Config{HostClaim: true, HibernateRegion: "eu-west-1", HibernateBucket: "data"} |
| ++ switch which { |
| ++ case "named": |
| ++ cfg.Region = "uk" |
| ++ case "no-bucket": |
| ++ cfg.HibernateBucket = "" |
| ++ case "invalid": |
| ++ cfg.HibernateRegion = "@home" |
| ++ case "explicit": |
| ++ cfg.HomeRegion = "ie" |
| ++ } |
| ++ h := hostclaim.Holder{Admissions: []hostclaim.AdmissionRecord{{Region: "eu-west-1", PredecessorRegion: "eu-west-1"}}} |
| ++ got := park.HolderRegionView(h, cfg.dbClaimHomeRegion(), cfg.dbClaimRegion()) |
| ++ want := "eu-west-1" |
| ++ if which == "home" { |
| ++ want = park.HomeRegion |
| ++ } |
| ++ if got.Admissions[0].Region != want || h.Admissions[0].Region != "eu-west-1" { |
| ++ t.Fatal(which, got) |
| ++ } |
| ++ } |
| ++} |
| ++ |
| ++func TestPlan13UnnamedHomeUnfinishedSelection(t *testing.T) { |
| ++ s, g, _, m := doorPlanFixture(t, true) |
| ++ s.cfg.HomeRegion = "" |
| ++ s.cfg.HibernateRegion = "eu-west-1" |
| ++ c := cloneDoorClaim(g.claim.Claim) |
| ++ c.Holder.Generation = "next" |
| ++ c.Holder.Admissions = []hostclaim.AdmissionRecord{{Generation: "next", Predecessor: hostclaim.RootGeneration, Region: "eu-west-1", PredecessorRegion: "eu-west-1", Mode: "cold"}} |
| ++ writeDoorClaim(t, s, g, c) |
| ++ var err error |
| ++ g.claim, err = hostclaim.Read(t.Context(), s.cfg.Releases, g.host) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ m.Put("data", dbVersionKey(c.Holder.Location, 0, 1, 1), doorLTXHeader(t, 1, 1)) |
| ++ got, err := s.listDBVersions(t.Context(), g, 0) |
| ++ if err != nil || len(got.Files) != 1 { |
| ++ t.Fatal(got, err) |
| ++ } |
| ++} |
| +--- /dev/null |
| ++++ b/internal/host/plan13_receipt_test.go |
| +@@ -0,0 +1,81 @@ |
| ++package host |
| ++ |
| ++import ( |
| ++ "crypto/ed25519" |
| ++ "crypto/sha256" |
| ++ "encoding/hex" |
| ++ "encoding/json" |
| ++ "github.com/carlosframework/platform/internal/hostactors" |
| ++ "github.com/carlosframework/platform/internal/hostclaim" |
| ++ "github.com/carlosframework/platform/internal/hostevidence" |
| ++ "github.com/carlosframework/platform/internal/release" |
| ++ "strings" |
| ++ "testing" |
| ++ "time" |
| ++) |
| ++ |
| ++func TestPlan13RecoveryHomeReceipt(t *testing.T) { |
| ++ for _, bad := range []string{"", "session", "stamp", "enrollment", "signature", "operation", "region", "digest", "unbound", "roster-name"} { |
| ++ t.Run(bad, func(t *testing.T) { |
| ++ f, o, h := plan12Handover(t, hostclaim.KindRestoring, "stop") |
| ++ o.Claim.Op.Intent["to"] = "@home" |
| ++ o.Claim.Op.Intent["home_region"] = "uk" |
| ++ if bad == "unbound" { |
| ++ delete(o.Claim.Op.Intent, "home_region") |
| ++ } |
| ++ key := ed25519.NewKeyFromSeed([]byte(strings.Repeat("b", 32))) |
| ++ en := strings.Repeat("a", 32) |
| ++ d := hostactors.Directory{Store: f.claims, PublicKey: f.signer.Public().(ed25519.PublicKey), Bucket: "bucket"} |
| ++ roster := hostactors.Roster{V: 1, Revision: 1, DeploymentBucket: "bucket", Boxes: map[string]hostactors.Box{"box-2": {Enrollment: en, BoxID: "box-2", Label: "box-2", Namespace: "platform", Region: "uk", Pool: "default", State: "active", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-2", InstanceID: "i-target"}, EvidenceSHA256: strings.Repeat("d", 64), CensusPublicKey: key.Public().(ed25519.PublicKey)}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} |
| ++ if bad == "roster-name" { |
| ++ b := roster.Boxes["box-2"] |
| ++ b.Region = "ie" |
| ++ roster.Boxes["box-2"] = b |
| ++ } |
| ++ wire, _ := release.SignRootEnvelope(release.RootSigner(f.signer), roster) |
| ++ if _, err := f.claims.Put(t.Context(), hostactors.Key, wire, ""); err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ r := StopReceipt{V: 1, ID: "stop", Kind: "stopped", Host: o.Host, Incarnation: o.Claim.Holder.Incarnation, InstallationStamp: 2, WriterSession: 2, OperationID: "m1", Phase: "releasing", ObligationID: "writer-session-2", Box: "box-2", Region: "uk", Enrollment: en, EvidenceAt: time.Now(), RequestLimitNS: int64(2 * time.Minute), ClaimGen: 1, FenceID: "fence", InventorySHA256: strings.Repeat("e", 64), LocalJournalSHA256: strings.Repeat("f", 64), ProducerDrainDigest: strings.Repeat("a", 64), CgroupIdentities: []string{"root-role"}} |
| ++ switch bad { |
| ++ case "session": |
| ++ r.WriterSession++ |
| ++ case "stamp": |
| ++ r.InstallationStamp++ |
| ++ case "enrollment": |
| ++ r.Enrollment = strings.Repeat("e", 32) |
| ++ case "operation": |
| ++ r.OperationID = "other" |
| ++ case "region": |
| ++ r.Region = "ie" |
| ++ } |
| ++ body, _ := json.Marshal(r) |
| ++ receiptKey := stopEvidenceKey(o.Host, "m1", "box-2", "stop") |
| ++ sum := sha256.Sum256(body) |
| ++ signer := key |
| ++ if bad == "signature" { |
| ++ signer = f.signer |
| ++ } |
| ++ if err := hostevidence.Publish(t.Context(), f.claims, signer, receiptKey, body); err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ h.Directory = d |
| ++ o.Claim.Op.Journal["target_stop_receipt"] = receiptKey |
| ++ o.Claim.Op.Journal["target_stop_sha256"] = hex.EncodeToString(sum[:]) |
| ++ o.Claim.Op.Journal[hostclaim.JournalTargetStamp] = "2" |
| ++ // Remove the unsigned report path: the receipt alone must suffice. |
| ++ delete(o.Claim.Op.Journal, hostclaim.JournalEvidenceTarget) |
| ++ if bad == "digest" { |
| ++ o.Claim.Op.Journal["target_stop_sha256"] = strings.Repeat("0", 64) |
| ++ } |
| ++ err := h.recoveryEvidence(t.Context(), o) |
| ++ stored, _, readErr := f.claims.Get(t.Context(), receiptKey) |
| ++ if readErr != nil || string(stored) != string(body) { |
| ++ t.Fatal("receipt bytes changed") |
| ++ } |
| ++ if (err == nil) != (bad == "") { |
| ++ t.Fatal(bad, err) |
| ++ } |
| ++ }) |
| ++ } |
| ++} |
| +--- /dev/null |
| ++++ b/internal/host/plan13_views_test.go |
| +@@ -0,0 +1,97 @@ |
| ++package host |
| ++ |
| ++import ( |
| ++ "context" |
| ++ "github.com/carlosframework/platform/internal/hostbox" |
| ++ "github.com/carlosframework/platform/internal/hostclaim" |
| ++ "github.com/carlosframework/platform/internal/lease" |
| ++ "github.com/carlosframework/platform/internal/park" |
| ++ "github.com/carlosframework/platform/internal/provision" |
| ++ "testing" |
| ++) |
| ++ |
| ++func TestPlan13ProductionRemoteHomeView(t *testing.T) { |
| ++ f := newVwFixture(t) |
| ++ f.rec.Region = "" |
| ++ f.rec.Move = &provision.MoveState{ID: vwMove, Incarnation: f.inc, From: "", To: "uk", State: provision.MoveLanding} |
| ++ h := f.holder("src", "src@ie") |
| ++ o := hostclaim.Observed{Host: vwHost, ETag: "tag", Claim: &hostclaim.Claim{Host: vwHost, Holder: h, Op: hostclaim.Op{ID: vwMove, Kind: hostclaim.KindLanding, Phase: "installing", Intent: map[string]string{"home_region": "ie"}}}} |
| ++ identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: "enrolled", Region: "uk", ControlBucket: "control", Revision: 1}) |
| ++ p := productionClaimRegional{Identity: identity, Parks: &claimMoveParkSources{View: f.view("uk")}} |
| ++ if _, err := p.View(t.Context(), hostclaim.Observed{}); err == nil { |
| ++ t.Fatal("missing claim accepted") |
| ++ } |
| ++ v, err := p.View(t.Context(), o) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ if v.AuthoritativeRegion != park.HomeRegion || v.Region != "uk" || v.HomeRegion != "ie" { |
| ++ t.Fatalf("view: %+v", v) |
| ++ } |
| ++ if h.Admissions[0].Region != "ie" { |
| ++ t.Fatal("history rewritten") |
| ++ } |
| ++ if err := p.Authorize(t.Context(), o, hostclaim.Obligation{Op: vwMove, Owner: "region:@home", Region: park.HomeRegion}); err == nil { |
| ++ t.Fatal("named worker owns home") |
| ++ } |
| ++} |
| ++func TestPlan13MovesEngineBindings(t *testing.T) { |
| ++ for _, home := range []string{"ie", ""} { |
| ++ region := "uk" |
| ++ if home != "" { |
| ++ region = "ie" |
| ++ } |
| ++ e := park.Engine{HomeRegion: home, Region: region, Bucket: "data", Leases: &fakeParkLeases{}, View: func(context.Context, hostclaim.Observed) (park.View, error) { return park.View{}, nil }, Authorize: allowParkAuthorize} |
| ++ providers := ClaimProviders{moveParks: &claimMoveParks{engine: e, leases: movesLeaseSet("data"), local: func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error { return nil }, check: func(context.Context) error { return nil }}} |
| ++ cfg := moveCfg{Region: region, OwnBucket: "data"} |
| ++ if home != "" { |
| ++ cfg.Region = "" |
| ++ } |
| ++ if err := armMovesParks(t.Context(), &cfg, providers, func() (movesClaimPlan12, error) { return &fakePlan12{}, nil }); err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ w := &movesWorker{cfg: cfg} |
| ++ engines := []park.Engine{(&installRun{w: w}).engine(nil), (&sourceFinal{w: w}).engine(nil), (&rollbackRun{w: w}).engine(nil)} |
| ++ for _, got := range engines { |
| ++ if got.HomeRegion != home || !got.OwnsRegion(park.PlacementRegion(cfg.Region)) { |
| ++ t.Fatalf("binding lost: %+v", got) |
| ++ } |
| ++ } |
| ++ } |
| ++} |
| ++ |
| ++func TestPlan13HomeLaneEngineBinding(t *testing.T) { |
| ++ h := newAbortHarness(t) |
| ++ h.abort(t) |
| ++ h.pass(t) |
| ++ chore := h.laneChore(t) |
| ++ if chore == nil { |
| ++ t.Fatal("no chore") |
| ++ } |
| ++ h.pastHorizon(t, chore) |
| ++ cfg := h.laneCfg() |
| ++ cfg.Region = "" |
| ++ engine := park.Engine{Region: "uk", HomeRegion: "uk", Bucket: cfg.OwnBucket, Leases: cfg.ParkLeases, Authorize: allowParkAuthorize, View: func(_ context.Context, o hostclaim.Observed) (park.View, error) { |
| ++ return park.View{Region: park.HomeRegion, ClaimETag: o.ETag, AuthoritativeRegion: "ie"}, nil |
| ++ }} |
| ++ p := ClaimProviders{moveParks: &claimMoveParks{engine: engine, leases: movesLeaseSet(cfg.OwnBucket), local: func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error { return nil }, check: func(context.Context) error { return nil }}} |
| ++ if err := armMovesParks(t.Context(), &cfg, p, func() (movesClaimPlan12, error) { return &fakePlan12{}, nil }); err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ o, err := hostclaim.Read(t.Context(), h.s, p1Host) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ if _, err = hostclaim.Update(t.Context(), h.s, o, func(c *hostclaim.Claim) error { c.Op.Intent["home_region"] = "uk"; return nil }); err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ rec := h.read(t, p1Host) |
| ++ rec.Move.To = "" |
| ++ worker := &movesWorker{cfg: cfg, mirrors: map[string]lease.MirrorStore{}} |
| ++ if err := worker.laneParkTarget(t.Context(), h.s, rec, hostclaim.KindMoving, hostclaim.PhaseAborting); err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ if h.laneChore(t) != nil { |
| ++ t.Fatal("named home chore was not acknowledged") |
| ++ } |
| ++} |
| +--- /dev/null |
| ++++ b/internal/hostops/plan13_conversion_test.go |
| +@@ -0,0 +1,78 @@ |
| ++package hostops |
| ++ |
| ++import ( |
| ++ "context" |
| ++ "github.com/carlosframework/platform/internal/hostclaim" |
| ++ "github.com/carlosframework/platform/internal/park" |
| ++ "github.com/carlosframework/platform/internal/provision" |
| ++ "testing" |
| ++) |
| ++ |
| ++func TestPlan13PreFlipHomeConversion(t *testing.T) { |
| ++ for _, direction := range []string{"source-home", "target-home", "named"} { |
| ++ for _, route := range []string{"grant", "held"} { |
| ++ t.Run(direction+"/"+route, func(t *testing.T) { |
| ++ w := newC7World(t, func(c *hostclaim.Claim, r *provision.Record, _ *AppAdmission) { |
| ++ if direction == "source-home" { |
| ++ r.Region, r.Move.From = "", "" |
| ++ c.Op.Intent["home_region"] = "ie" |
| ++ } |
| ++ if direction == "target-home" { |
| ++ r.Move.To = "" |
| ++ c.Op.Intent["to"] = park.HomeRegion |
| ++ c.Op.Intent["home_region"] = "uk" |
| ++ } |
| ++ }) |
| ++ run := func() error { |
| ++ if route == "held" { |
| ++ return RecoverTerminatedMovingHost(t.Context(), w.productionData(t, w.raw), w.mh, w.authority, w.host, w.ref, w.now) |
| ++ } |
| ++ return w.control.RunRecovery(t.Context(), func(ctx context.Context) error { |
| ++ return runMoveRecovery(ctx, w.claim(t), func(ctx context.Context, g *MoveRecoveryGrant) error { return g.Convert(ctx, w.ref) }, w.authority) |
| ++ }) |
| ++ } |
| ++ for i := 0; i < 3; i++ { |
| ++ if err := run(); err != nil { |
| ++ t.Fatalf("pass %d: %v", i, err) |
| ++ } |
| ++ } |
| ++ c := w.claim(t).Claim |
| ++ if c.Op.Kind != hostclaim.KindRelocating || c.Op.Intent["region"] != "ie" { |
| ++ t.Fatalf("claim: %+v", c.Op) |
| ++ } |
| ++ r, err := provision.GetRecord(t.Context(), w.raw, "bab", "app", w.host) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ from, to := "ie", "uk" |
| ++ if direction == "source-home" { |
| ++ from = "" |
| ++ } |
| ++ if direction == "target-home" { |
| ++ to = "" |
| ++ } |
| ++ if r.Region != from || r.Move.From != from || r.Move.To != to || r.Move.Relocation.Region != from { |
| ++ t.Fatalf("rewritten record: %+v", r) |
| ++ } |
| ++ tag := w.claim(t).ETag |
| ++ rtag := w.recordTag(t) |
| ++ if err := run(); err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ if tag != w.claim(t).ETag || rtag != w.recordTag(t) { |
| ++ t.Fatal("replay wrote") |
| ++ } |
| ++ }) |
| ++ } |
| ++ } |
| ++} |
| ++func TestPlan13PreFlipUnboundHomeRefuses(t *testing.T) { |
| ++ w := newC7World(t, func(c *hostclaim.Claim, r *provision.Record, _ *AppAdmission) { r.Region, r.Move.From = "", "" }) |
| ++ tag := w.recordTag(t) |
| ++ if err := RecoverTerminatedMovingHost(t.Context(), w.productionData(t, w.raw), w.mh, w.authority, w.host, w.ref, w.now); err == nil { |
| ++ t.Fatal("unbound home accepted") |
| ++ } |
| ++ if tag != w.recordTag(t) { |
| ++ t.Fatal("refusal wrote") |
| ++ } |
| ++} |
| +--- /dev/null |
| ++++ b/internal/park/plan13_observe_test.go |
| +@@ -0,0 +1,73 @@ |
| ++package park |
| ++ |
| ++import ( |
| ++ "context" |
| ++ "github.com/carlosframework/platform/internal/hostclaim" |
| ++ "testing" |
| ++) |
| ++ |
| ++func TestPlan13ObserveRemoteBinding(t *testing.T) { |
| ++ f := newFixture(t) |
| ++ o, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ _, err = hostclaim.Update(t.Context(), f.e.Claims, o, func(c *hostclaim.Claim) error { c.Op.Intent = map[string]string{"home_region": "ie"}; return nil }) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ f.e.Region = "uk" |
| ++ f.e.View = func(_ context.Context, o hostclaim.Observed) (View, error) { |
| ++ return View{Region: "uk", HomeRegion: "ie", ClaimETag: o.ETag}, nil |
| ++ } |
| ++ _, v, err := f.e.observe(t.Context(), f.w, f.held, f.ob) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ if v.HomeRegion != "ie" || f.e.OwnsRegion(HomeRegion) { |
| ++ t.Fatal("history binding lost or ownership widened") |
| ++ } |
| ++ c := &hostclaim.Claim{Holder: &hostclaim.Holder{Location: f.ob.Park.Location, Generation: "new", Admissions: []hostclaim.AdmissionRecord{{Generation: "new", Predecessor: "old", Region: "ie", PredecessorRegion: HomeRegion, Mode: "cold"}}}, Op: hostclaim.Op{Kind: hostclaim.KindServing}} |
| ++ v.GenerationOf = func(Ref) (string, bool) { return "old", true } |
| ++ v.Region = HomeRegion |
| ++ v.AuthoritativeRegion = HomeRegion |
| ++ v.MarkerKnown = true |
| ++ v.MarkerGeneration = "old" |
| ++ live, err := LiveGenerations(c, v, c.Holder.Location) |
| ++ if err != nil || !live["old"] || !live["new"] { |
| ++ t.Fatal(live, err) |
| ++ } |
| ++} |
| ++func TestPlan13CompletedHomeParkCoversNamedPredecessor(t *testing.T) { |
| ++ f := newFixture(t) |
| ++ f.e.HomeRegion = "ie" |
| ++ f.e.Region = HomeRegion |
| ++ o, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ _, err = hostclaim.Update(t.Context(), f.e.Claims, o, func(c *hostclaim.Claim) error { |
| ++ c.Obligations[0].Region = HomeRegion |
| ++ c.Obligations[0].Owner = "region:" + HomeRegion |
| ++ c.Holder.Generation = "next" |
| ++ c.Holder.Admissions = []hostclaim.AdmissionRecord{{Generation: "next", Predecessor: "old", Region: "uk", PredecessorRegion: "ie", Mode: "cold"}, {Generation: "foreign", Predecessor: "old", Region: "fr", PredecessorRegion: "fr", Mode: "cold"}} |
| ++ return nil |
| ++ }) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ f.e.View = func(_ context.Context, o hostclaim.Observed) (View, error) { |
| ++ return View{Region: HomeRegion, ClaimETag: o.ETag, AuthoritativeRegion: "uk", GenerationOf: func(Ref) (string, bool) { return "old", true }}, nil |
| ++ } |
| ++ f.b.Put("data", f.ob.Park.Location+"/g-old/0000/a", []byte("old")) |
| ++ if err := f.e.Run(t.Context(), f.w); err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ done, err := hostclaim.Read(t.Context(), f.e.Claims, f.w.Host) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ if len(done.Claim.Obligations) != 0 || !done.Claim.Holder.Admissions[0].Covered || done.Claim.Holder.Admissions[1].Covered { |
| ++ t.Fatalf("coverage: %+v", done.Claim.Holder.Admissions) |
| ++ } |
| ++} |
| +``` |
| + |
| +### Implementation |
| + |
| +<!-- apply: G3 implementation --> |
| +```diff |
| +--- a/internal/console/apidbclaim.go |
| ++++ b/internal/console/apidbclaim.go |
| +@@ -185,8 +185,24 @@ |
| + return region |
| } |
| ---- a/internal/host/claimhandover_recovery.go |
| -+++ b/internal/host/claimhandover_recovery.go |
| -@@ -12,6 +12,7 @@ |
| + |
| ++// dbClaimHomeRegion binds the old fallback only for this configured home |
| ++// bucket. Admission contents never choose the compatibility name. |
| ++func (cfg Config) dbClaimHomeRegion() string { |
| ++ if cfg.HomeRegion != "" { |
| ++ return cfg.HomeRegion |
| ++ } |
| ++ legacy := strings.TrimSpace(cfg.HibernateRegion) |
| ++ if cfg.HostClaim && cfg.Region == "" && cfg.HibernateBucket != "" && legacy != "" && park.PlacementRegion(legacy) == legacy { |
| ++ return legacy |
| ++ } |
| ++ return "" |
| ++} |
| ++ |
| + func (cfg Config) dbClaimRegion() string { |
| + if cfg.HostClaim { |
| ++ if cfg.Region == "" && cfg.HomeRegion == "" && strings.TrimSpace(cfg.HibernateRegion) == "" { |
| ++ return "" |
| ++ } |
| + return cfg.claimPlacementRegion(cfg.Region) |
| + } |
| + for _, region := range []string{cfg.Region, cfg.HomeRegion, cfg.HibernateRegion} { |
| +@@ -202,7 +218,7 @@ |
| + out := dbdoor.ListResponse{Protocol: 2, Plan: true, Files: []dbdoor.LTXFile{}} |
| + h := g.claim.Claim.Holder |
| + plan, err := genselect.Select(ctx, s.cfg.DBVersions, genselect.Request{ |
| +- Bucket: s.cfg.HibernateBucket, Region: s.cfg.dbClaimRegion(), Host: g.host, Holder: park.HolderRegionView(*h, s.cfg.HomeRegion, s.cfg.dbClaimRegion()), |
| ++ Bucket: s.cfg.HibernateBucket, Region: s.cfg.dbClaimRegion(), Host: g.host, Holder: park.HolderRegionView(*h, s.cfg.dbClaimHomeRegion(), s.cfg.dbClaimRegion()), |
| + }) |
| + if err != nil { |
| + return dbdoor.ListResponse{}, errDBClaimChanged |
| +--- a/internal/host/claimproduction.go |
| ++++ b/internal/host/claimproduction.go |
| +@@ -363,11 +363,18 @@ |
| + if err != nil { |
| + return park.View{}, err |
| + } |
| +- v, err := p.Parks.View(ctx, park.HomeView(o, actor.HomeRegion)) |
| ++ if o.Claim == nil { |
| ++ return park.View{}, park.ErrUnsafe |
| ++ } |
| ++ home, err := park.BoundHome(actor.HomeRegion, o.Claim.Op.Intent["home_region"]) |
| + if err != nil { |
| + return park.View{}, err |
| + } |
| +- v.HomeRegion = actor.HomeRegion |
| ++ v, err := p.Parks.View(ctx, park.HomeView(o, home)) |
| ++ if err != nil { |
| ++ return park.View{}, err |
| ++ } |
| ++ v.HomeRegion = home |
| + v.ArchivedIncarnations = claimArchivedLocations(o.Claim) |
| + v.RefusedLocations = map[string]error{} |
| + if _, err := claimLegacyIncarnation(o.Claim); err != nil { |
| +--- a/internal/host/claimrecoveryreceipt.go |
| ++++ b/internal/host/claimrecoveryreceipt.go |
| +@@ -9,6 +9,7 @@ |
| + "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| - "github.com/carlosframework/platform/internal/hostguard" |
| - "github.com/carlosframework/platform/internal/moves" |
| + "github.com/carlosframework/platform/internal/hostevidence" |
| + "github.com/carlosframework/platform/internal/park" |
| - "github.com/carlosframework/platform/internal/registry" |
| "strconv" |
| ) |
| -@@ -72,7 +73,7 @@ |
| - if err != nil { |
| + |
| +@@ -34,7 +35,7 @@ |
| + stamp, err := strconv.ParseUint(c.Op.Journal[hostclaim.JournalTargetStamp], 10, 64) |
| + if err != nil || stamp == 0 || r.V != 1 || r.Kind != "stopped" || r.Host != o.Host || r.Box != p.Box || r.OperationID != c.Op.ID || |
| + r.Incarnation != c.Holder.Incarnation || r.WriterSession != p.Session || r.InstallationStamp != stamp || |
| +- r.Region != c.Op.Intent["to"] || r.Enrollment != c.Op.Intent["target_enrollment"] || r.Phase != hostclaim.PhaseReleasing || |
| ++ !park.SameRegion(c.Op.Intent["home_region"], r.Region, c.Op.Intent["to"]) || r.Enrollment != c.Op.Intent["target_enrollment"] || r.Phase != hostclaim.PhaseReleasing || |
| + key != stopEvidenceKey(o.Host, c.Op.ID, p.Box, r.ID) || r.FenceID == "" || r.ObligationID == "" || r.ClaimGen == 0 || |
| + r.EvidenceAt.IsZero() || r.RequestLimitNS != int64(hostbox.RequestLimit) || len(r.CgroupIdentities) == 0 || |
| + !stopDigest(r.InventorySHA256) || !stopDigest(r.LocalJournalSHA256) || !stopDigest(r.ProducerDrainDigest) { |
| +--- a/internal/host/movesabort.go |
| ++++ b/internal/host/movesabort.go |
| +@@ -4,6 +4,7 @@ |
| + "context" |
| + "errors" |
| + "fmt" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "log" |
| + "os" |
| + "sort" |
| +@@ -234,7 +235,7 @@ |
| + } |
| + } |
| + scope := moves.AbortScope(host, id, obs.Claim.Holder.Incarnation, bound) |
| +- if _, err := hostclaim.Update(ctx, s, obs, hostclaim.Abort(id, rec.Move.To, scope, obligationID)); err != nil { |
| ++ if _, err := hostclaim.Update(ctx, s, obs, hostclaim.Abort(id, park.PlacementRegion(rec.Move.To), scope, obligationID)); err != nil { |
| + return false, fmt.Errorf("abort the claim: %w", err) |
| + } |
| + log.Printf("moves: %s: the claim aborts move %s; the staging lane in %s is queued for its park there", host, id, rec.Move.To) |
| +--- a/internal/host/movescut.go |
| ++++ b/internal/host/movescut.go |
| +@@ -8,6 +8,7 @@ |
| + "encoding/json" |
| + "errors" |
| + "fmt" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "io" |
| + "log" |
| + "os" |
| +@@ -1291,7 +1292,7 @@ |
| + } |
| + } |
| + scope := moves.StagingScope(rec.Host, id, obs.Claim.Holder.Incarnation, bound) |
| +- if _, err := hostclaim.Update(ctx, s, obs, hostclaim.Revert(id, cfg.Box, rec.Move.To, &scope, obligationID)); err != nil { |
| ++ if _, err := hostclaim.Update(ctx, s, obs, hostclaim.Revert(id, cfg.Box, park.PlacementRegion(rec.Move.To), &scope, obligationID)); err != nil { |
| + return fmt.Errorf("revert the claim: %w", err) |
| + } |
| + log.Printf("moves: %s: the claim reverts to this box; the staging lane in %s is queued for its park", rec.Host, rec.Move.To) |
| +--- a/internal/host/movesfinal.go |
| ++++ b/internal/host/movesfinal.go |
| +@@ -387,12 +387,12 @@ |
| + if err := f.sourceOf(obs); err != nil { |
| return err |
| } |
| -- if verified.Box() != p.Box || verified.Region() != op.Intent["to"] { |
| -+ if verified.Box() != p.Box || !park.SameRegion(op.Intent["home_region"], verified.Region(), op.Intent["to"]) { |
| - return hostclaim.ErrFence |
| +- if o.Op != f.g.ID || o.Region != cfg.Region { |
| ++ if o.Op != f.g.ID || !park.PlacementMatches(cfg.ParkHomeRegion, cfg.Region, o.Region) { |
| + return fmt.Errorf("%w: not this finalization's chore", park.ErrUnsafe) |
| } |
| return nil |
| + } |
| +- return park.Engine{Store: vs, Claims: f.s, Bucket: cfg.OwnBucket, Region: cfg.Region, Leases: cfg.ParkLeases, |
| ++ return park.Engine{HomeRegion: cfg.ParkHomeRegion, Store: vs, Claims: f.s, Bucket: cfg.OwnBucket, Region: park.PlacementRegion(cfg.Region), Leases: cfg.ParkLeases, |
| + Now: f.now, NewID: obligationID, View: cfg.ParkView, Authorize: movesArmAuthorize(cfg.ParkAuthorize, own), After: cfg.parkFault} |
| + } |
| + |
| +@@ -406,7 +406,7 @@ |
| + var chore *hostclaim.Obligation |
| + for i := range obs.Claim.Obligations { |
| + o := &obs.Claim.Obligations[i] |
| +- if o.Op == id && o.Kind == "park-replica" && o.Region == cfg.Region && o.Park != nil && !isStagingLocation(o.Park.Location) { |
| ++ if o.Op == id && o.Kind == "park-replica" && park.PlacementMatches(cfg.ParkHomeRegion, cfg.Region, o.Region) && o.Park != nil && !isStagingLocation(o.Park.Location) { |
| + chore = o |
| + } |
| + } |
| +--- a/internal/host/movesinstall.go |
| ++++ b/internal/host/movesinstall.go |
| +@@ -479,7 +479,7 @@ |
| + return false, err |
| + } |
| + scope := moves.ResidueScope(holder, *ir.rec.Move.FlippedAt) |
| +- if obs, err = hostclaim.Update(ctx, ir.s, obs, hostclaim.QueueResidueParks(id, cfg.Region, scope, gens, obligationID)); err != nil { |
| ++ if obs, err = hostclaim.Update(ctx, ir.s, obs, hostclaim.QueueResidueParks(id, park.PlacementRegion(cfg.Region), scope, gens, obligationID)); err != nil { |
| + return false, fmt.Errorf("queue the residue parks: %w", err) |
| + } |
| + if err := ir.fault("after-residue-queued"); err != nil { |
| +@@ -488,7 +488,7 @@ |
| + eng := ir.engine(vs) |
| + pending := 0 |
| + for _, o := range obs.Claim.Obligations { |
| +- if o.Op != id || o.Kind != "park-generation" || o.Region != cfg.Region { |
| ++ if o.Op != id || o.Kind != "park-generation" || !park.PlacementMatches(cfg.ParkHomeRegion, cfg.Region, o.Region) { |
| + continue |
| + } |
| + w := park.Work{Host: host, ID: o.ID, Op: id} |
| +--- a/internal/host/moveslandcommit.go |
| ++++ b/internal/host/moveslandcommit.go |
| +@@ -4,6 +4,7 @@ |
| + "context" |
| + "errors" |
| + "fmt" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "log" |
| + |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| +@@ -162,7 +163,7 @@ |
| + if err != nil { |
| + return err |
| + } |
| +- if _, err := hostclaim.Update(ctx, l.s, obs, hostclaim.KeepWriterSession(id, hostclaim.Commit(id, l.rec.Move.From, obs.Claim.Op.Intent["source"], cfg.Region, source, staging, obligationID))); err != nil { |
| ++ if _, err := hostclaim.Update(ctx, l.s, obs, hostclaim.KeepWriterSession(id, hostclaim.Commit(id, park.PlacementRegion(l.rec.Move.From), obs.Claim.Op.Intent["source"], park.PlacementRegion(cfg.Region), source, staging, obligationID))); err != nil { |
| + return fmt.Errorf("move %s: the claim's commit: %w", id, err) |
| + } |
| + log.Printf("moves: %s: the claim of landing %s is committed", l.rec.Host, id) |
| +--- a/internal/host/moveslandpark.go |
| ++++ b/internal/host/moveslandpark.go |
| +@@ -135,7 +135,7 @@ |
| + // check (the arming requires all three). |
| + func (ir *installRun) engine(vs lease.VersionedStore) park.Engine { |
| + cfg := ir.cfg() |
| +- return park.Engine{Store: vs, Claims: ir.s, Bucket: cfg.OwnBucket, Region: cfg.Region, Leases: cfg.ParkLeases, |
| ++ return park.Engine{HomeRegion: cfg.ParkHomeRegion, Store: vs, Claims: ir.s, Bucket: cfg.OwnBucket, Region: park.PlacementRegion(cfg.Region), Leases: cfg.ParkLeases, |
| + Now: cfg.Now, NewID: obligationID, View: cfg.ParkView, Authorize: movesArmAuthorize(cfg.ParkAuthorize, ir.authorizeObligation), After: cfg.parkFault} |
| + } |
| + |
| +--- a/internal/host/movespass.go |
| ++++ b/internal/host/movespass.go |
| +@@ -605,11 +605,12 @@ |
| + // registry's; RemoveVerifyEntry is |
| + // dbverify.RemoveHost; WriteGen is hostbox.WriteGenerationMarker, the |
| + // one <dbPath>.gen writer (ruling G2). |
| +- Guard hostguard.Guard |
| +- Local park.Local |
| +- ParkLeases park.Leases |
| +- ParkView func(context.Context, hostclaim.Observed) (park.View, error) |
| +- ParkAuthorize func(context.Context, hostclaim.Observed, hostclaim.Obligation) error |
| ++ Guard hostguard.Guard |
| ++ Local park.Local |
| ++ ParkHomeRegion string |
| ++ ParkLeases park.Leases |
| ++ ParkView func(context.Context, hostclaim.Observed) (park.View, error) |
| ++ ParkAuthorize func(context.Context, hostclaim.Observed, hostclaim.Obligation) error |
| + // ParkRunHeld and ParkLocalAuthorize are claim Plan 12's pieces of the |
| + // MoveParks capability (movesClaimPlan12, movessources.go): RunHeld runs |
| + // a chore under a lease the caller holds (M5: the landing's residue park |
| +--- a/internal/host/movesphase1.go |
| ++++ b/internal/host/movesphase1.go |
| +@@ -6,6 +6,7 @@ |
| + "encoding/json" |
| + "errors" |
| + "fmt" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "io" |
| + "log" |
| + "os" |
| +@@ -318,12 +319,12 @@ |
| + // K12 and T9: the target region and box are in touched before the |
| + // first staging write. |
| + targetBox := obs.Claim.Op.Intent["target"] |
| +- if !containsString(obs.Claim.Touched.Regions, rec.Move.To) || (targetBox != "" && !containsString(obs.Claim.Touched.Boxes, targetBox)) { |
| ++ if !containsString(obs.Claim.Touched.Regions, park.PlacementRegion(rec.Move.To)) || (targetBox != "" && !containsString(obs.Claim.Touched.Boxes, targetBox)) { |
| + var boxes []string |
| + if targetBox != "" { |
| + boxes = []string{targetBox} |
| + } |
| +- if _, err := hostclaim.Update(ctx, s, obs, hostclaim.Touch(id, []string{rec.Move.To}, boxes)); err != nil { |
| ++ if _, err := hostclaim.Update(ctx, s, obs, hostclaim.Touch(id, []string{park.PlacementRegion(rec.Move.To)}, boxes)); err != nil { |
| + return nil, nil, fmt.Errorf("touch %s in the host claim: %w", rec.Move.To, err) |
| + } |
| + } |
| +--- a/internal/host/movespolicy.go |
| ++++ b/internal/host/movespolicy.go |
| +@@ -437,7 +437,7 @@ |
| + if rec == nil || rec.Host != c.Host || rec.Move == nil || rec.Move.ID != s.Op || rec.Move.Incarnation != c.Holder.Incarnation { |
| + return refuse("the record is not this move", nil) |
| + } |
| +- if !park.SameRegion(c.Op.Intent["home_region"], park.PlacementRegion(rec.Move.From), s.Region) { |
| ++ if !park.PlacementMatches(c.Op.Intent["home_region"], rec.Move.From, s.Region) { |
| + return refuse("this region is not the move's source region", nil) |
| + } |
| + var ( |
| +--- a/internal/host/movesprobeowners.go |
| ++++ b/internal/host/movesprobeowners.go |
| +@@ -229,7 +229,7 @@ |
| + return v, hostbox.ErrChanged |
| + } |
| + mv := rec.Move |
| +- if mv.ID != v.op || mv.Incarnation != v.holder.Incarnation || !park.SameRegion(c.Op.Intent["home_region"], park.PlacementRegion(mv.To), m.Region) || mv.Inputs == nil || mv.Inputs.Release == "" { |
| ++ if mv.ID != v.op || mv.Incarnation != v.holder.Incarnation || !park.PlacementMatches(c.Op.Intent["home_region"], mv.To, m.Region) || mv.Inputs == nil || mv.Inputs.Release == "" { |
| + return v, hostbox.ErrChanged |
| + } |
| + if v.committed { |
| +@@ -393,7 +393,7 @@ |
| + if n := len(h.Admissions); n > 0 { |
| + return h.Admissions[n-1].Region |
| + } |
| +- return from |
| ++ return park.PlacementRegion(from) |
| + } |
| + |
| + func (m *movesProbeOwners) request(host string, h *hostclaim.Holder, home string) genselect.Request { |
| +--- a/internal/host/movesrecover.go |
| ++++ b/internal/host/movesrecover.go |
| +@@ -5,6 +5,7 @@ |
| + "encoding/json" |
| + "errors" |
| + "fmt" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "log" |
| + "strconv" |
| + "time" |
| +@@ -672,7 +673,7 @@ |
| + return false, err |
| + } |
| + scope := moves.ReturnLaneScope(host, r.rb, obs.Claim.Holder.Incarnation, seal.Bound()) |
| +- lane = hostclaim.QueueReturnLanePark(id, cfg.Region, scope, obligationID) |
| ++ lane = hostclaim.QueueReturnLanePark(id, park.PlacementRegion(cfg.Region), scope, obligationID) |
| + } |
| + switch obs.Claim.Op.Phase { |
| + case hostclaim.PhaseProbing: |
| +--- a/internal/host/movesrevert.go |
| ++++ b/internal/host/movesrevert.go |
| +@@ -162,7 +162,7 @@ |
| + // bucket. In production armMovesParks sets the three only with claim Plan |
| + // 12, so this answers false there until it lands. |
| + func revertLaneArmed(cfg moveCfg) bool { |
| +- return cfg.Box != "" && cfg.Region != "" && cfg.OwnBucket != "" && parkHooksArmed(cfg) && cfg.Versioned != nil |
| ++ return cfg.Box != "" && park.PlacementRegion(cfg.Region) != "" && cfg.OwnBucket != "" && parkHooksArmed(cfg) && cfg.Versioned != nil |
| + } |
| + |
| + // restatusHold is moveCfg.RestatusHold in production: an op's own hold |
| +@@ -899,7 +899,7 @@ |
| + lane := moves.ScopeLocation(host, id) |
| + var chores []hostclaim.Obligation |
| + for _, o := range obs.Claim.Obligations { |
| +- if o.Op == id && o.Kind == "park-replica" && o.Region == cfg.Region && o.Park != nil && o.Park.Location == lane { |
| ++ if o.Op == id && o.Kind == "park-replica" && park.PlacementMatches(cfg.ParkHomeRegion, cfg.Region, o.Region) && o.Park != nil && o.Park.Location == lane { |
| + chores = append(chores, o) |
| + } |
| + } |
| +@@ -917,12 +917,12 @@ |
| + own := func(_ context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error { |
| + c := obs.Claim |
| + if obs.Absent || c == nil || !inOp(c.Op) || (!committed && c.Op.Exec == cfg.Box) || |
| +- o.Op != id || o.Kind != "park-replica" || o.Region != cfg.Region || o.Park == nil || o.Park.Location != lane { |
| ++ o.Op != id || o.Kind != "park-replica" || !park.PlacementMatches(cfg.ParkHomeRegion, cfg.Region, o.Region) || o.Park == nil || o.Park.Location != lane { |
| + return fmt.Errorf("%w: not this move's lane park", park.ErrUnsafe) |
| + } |
| + return nil |
| + } |
| +- e := park.Engine{Store: vs, Claims: s, Bucket: cfg.OwnBucket, Region: cfg.Region, Leases: cfg.ParkLeases, |
| ++ e := park.Engine{HomeRegion: cfg.ParkHomeRegion, Store: vs, Claims: s, Bucket: cfg.OwnBucket, Region: park.PlacementRegion(cfg.Region), Leases: cfg.ParkLeases, |
| + Now: w.now, NewID: obligationID, View: cfg.ParkView, Authorize: movesArmAuthorize(cfg.ParkAuthorize, own), After: cfg.parkFault} |
| + for _, o := range chores { |
| + err := e.Run(ctx, park.Work{Host: host, ID: o.ID, Op: id}) |
| +--- a/internal/host/movesrollback.go |
| ++++ b/internal/host/movesrollback.go |
| +@@ -556,7 +556,7 @@ |
| + } |
| + holder := obs.Claim.Holder |
| + scope := moves.TargetLocationScope(*holder, report.CheckedAt) |
| +- if _, err := hostclaim.Update(ctx, r.s, obs, hostclaim.TargetReleased(r.g.ID, cfg.Box, cfg.Region, evidence, scope, obligationID)); err != nil { |
| ++ if _, err := hostclaim.Update(ctx, r.s, obs, hostclaim.TargetReleased(r.g.ID, cfg.Box, park.PlacementRegion(cfg.Region), evidence, scope, obligationID)); err != nil { |
| + return fmt.Errorf("the claim half of the release: %w", err) |
| + } |
| + log.Printf("moves: %s: the target gave execution up for rollback %s, citing %s", r.rec.Host, r.rb, evidence) |
| +@@ -1103,7 +1103,7 @@ |
| + return false, err |
| + } |
| + scope := moves.StagingScope(host, id, obs.Claim.Holder.Incarnation, staged.Bound()) |
| +- if _, err := hostclaim.Update(ctx, r.s, obs, hostclaim.QueueStagingPark(id, cfg.Region, scope, obligationID)); err != nil { |
| ++ if _, err := hostclaim.Update(ctx, r.s, obs, hostclaim.QueueStagingPark(id, park.PlacementRegion(cfg.Region), scope, obligationID)); err != nil { |
| + return false, fmt.Errorf("queue the move's lane park: %w", err) |
| + } |
| + if err := mark(rbStepLaneQueued, ""); err != nil { |
| +@@ -1121,7 +1121,7 @@ |
| + e := r.engine(vs) |
| + waiting := 0 |
| + for _, o := range obs.Claim.Obligations { |
| +- if o.Op != id || o.Kind != "park-replica" || o.Region != cfg.Region || o.Park == nil { |
| ++ if o.Op != id || o.Kind != "park-replica" || !park.PlacementMatches(cfg.ParkHomeRegion, cfg.Region, o.Region) || o.Park == nil { |
| + continue |
| + } |
| + work := park.Work{Host: host, ID: o.ID, Op: id} |
| +@@ -1158,7 +1158,7 @@ |
| + } |
| + return r.authorizeChore(obs.Claim, o) |
| + } |
| +- return park.Engine{Store: vs, Claims: r.s, Bucket: cfg.OwnBucket, Region: cfg.Region, Leases: cfg.ParkLeases, |
| ++ return park.Engine{HomeRegion: cfg.ParkHomeRegion, Store: vs, Claims: r.s, Bucket: cfg.OwnBucket, Region: park.PlacementRegion(cfg.Region), Leases: cfg.ParkLeases, |
| + Now: r.now, NewID: obligationID, View: cfg.ParkView, Authorize: movesArmAuthorize(cfg.ParkAuthorize, own), After: cfg.parkFault} |
| + } |
| + |
| +@@ -1173,7 +1173,7 @@ |
| + return fmt.Errorf("%w: not this rollback's chore", park.ErrUnsafe) |
| + } |
| + switch { |
| +- case o.Kind == "park-replica" && o.Region == cfg.Region: |
| ++ case o.Kind == "park-replica" && park.PlacementMatches(cfg.ParkHomeRegion, cfg.Region, o.Region): |
| + case o.Kind == "park-local" && o.Box == cfg.Box: |
| + default: |
| + return fmt.Errorf("%w: not this target's chore", park.ErrUnsafe) |
| +--- a/internal/host/movessources.go |
| ++++ b/internal/host/movessources.go |
| +@@ -404,6 +404,10 @@ |
| + } |
| + return "", fmt.Errorf("%w: the record does not carry move %s", park.ErrUnsafe, c.Op.ID) |
| + } |
| ++ from, to := park.PlacementRegion(m.From), park.PlacementRegion(m.To) |
| ++ if from == "" || to == "" { |
| ++ return "", park.ErrUnsafe |
| ++ } |
| + contradicts := func(want ...string) error { |
| + if !known { |
| + return nil |
| +@@ -417,38 +421,38 @@ |
| + } |
| + switch c.Op.Kind { |
| + case hostclaim.KindMoving, hostclaim.KindReverting: |
| +- if err := contradicts(m.From); err != nil { |
| ++ if err := contradicts(from); err != nil { |
| + return "", err |
| + } |
| +- return m.From, nil |
| ++ return from, nil |
| + case hostclaim.KindLanding: |
| +- if err := contradicts(m.From, m.To); err != nil { |
| ++ if err := contradicts(from, to); err != nil { |
| + return "", err |
| + } |
| +- if known && admitted == m.To { |
| +- if region == m.From && c.Op.Phase != hostclaim.PhaseCommitted && m.State != provision.MoveCommitted { |
| ++ if known && admitted == to { |
| ++ if region == from && c.Op.Phase != hostclaim.PhaseCommitted && m.State != provision.MoveCommitted { |
| + // J2/F4 (Paul's #236 ruling): the source's live set, its |
| + // files live set among it, stays protected on the source |
| + // until the record commits. Seen from the source's region the |
| + // source stays authoritative, so the engine protects there |
| + // too (and refuses a park of the location, whose current |
| + // generation the source's marker does not name). |
| +- return m.From, nil |
| ++ return from, nil |
| + } |
| +- return m.To, nil |
| ++ return to, nil |
| + } |
| + if c.Op.Phase == hostclaim.PhaseCommitted || m.State == provision.MoveCommitted { |
| + return "", fmt.Errorf("%w: move %s committed with no admission in %s", park.ErrUnsafe, m.ID, m.To) |
| + } |
| +- return m.From, nil |
| ++ return from, nil |
| + default: // restoring |
| +- if err := contradicts(m.From, m.To); err != nil { |
| ++ if err := contradicts(from, to); err != nil { |
| + return "", err |
| + } |
| + if known { |
| + return admitted, nil |
| + } |
| +- return m.From, nil |
| ++ return from, nil |
| + } |
| + } |
| + |
| +@@ -599,6 +603,7 @@ |
| + if p12 == nil { |
| + return &ClaimDependencyError{Contracts: movesPlan12Contracts} |
| + } |
| ++ cfg.ParkHomeRegion = engine.HomeRegion |
| + cfg.ParkLeases, cfg.ParkView, cfg.ParkAuthorize = engine.Leases, engine.View, engine.Authorize |
| + cfg.ParkRunHeld = func(ctx context.Context, e park.Engine, w park.Work, held park.HeldLease) error { |
| + return e.RunHeld(ctx, w, held) |
| +--- a/internal/hostclaim/relocating.go |
| ++++ b/internal/hostclaim/relocating.go |
| +@@ -18,7 +18,8 @@ |
| + // source and exact fence reference. A snapshot of an undecided record is not |
| + // an implementation of this interface. While the claim is moving the source |
| + // is its "source" and the region is the record's; once relocating they are |
| +-// its "source_box" and "region". The returned region is the record's. |
| ++// its "source_box" and "region". The returned region is the verified engine |
| ++// identity of the record placement, never an empty placement code. |
| + type PreFlipGuard interface { |
| + CheckMovingBeforeFlip(context.Context, Observed, string, FenceRef) (string, error) |
| + } |
| +--- a/internal/hostops/lifecycle_record.go |
| ++++ b/internal/hostops/lifecycle_record.go |
| +@@ -7,6 +7,7 @@ |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/release" |
| + ) |
| +@@ -84,11 +85,11 @@ |
| + return "", ErrIdentity |
| + } |
| + var from, to string |
| +- if json.Unmarshal(before["region"], &from) != nil || json.Unmarshal(next["region"], &to) != nil { |
| ++ if (len(before["region"]) != 0 && json.Unmarshal(before["region"], &from) != nil) || (len(next["region"]) != 0 && json.Unmarshal(next["region"], &to) != nil) || !provision.ValidRegion(from) || !provision.ValidRegion(to) { |
| + return "", ErrIdentity |
| + } |
| + if from != to { |
| +- flip := o.Claim.Op.Kind == hostclaim.KindMoving && o.Claim.Op.Phase == "sealed" && a.State == "cutting" && a.CutPhase == "archived" && a.FlippedAt == nil && a.CommittedAt == nil && a.Rollback == nil && b.State == "landing" && b.CutPhase == "flipped" && b.FlippedAt != nil && b.LandingDeadline != nil && b.LandingDeadline.After(*b.FlippedAt) && b.Rollback == nil && from == b.From && to == b.To && to == o.Claim.Op.Intent["to"] |
| ++ flip := o.Claim.Op.Kind == hostclaim.KindMoving && o.Claim.Op.Phase == "sealed" && a.State == "cutting" && a.CutPhase == "archived" && a.FlippedAt == nil && a.CommittedAt == nil && a.Rollback == nil && b.State == "landing" && b.CutPhase == "flipped" && b.FlippedAt != nil && b.LandingDeadline != nil && b.LandingDeadline.After(*b.FlippedAt) && b.Rollback == nil && from == b.From && to == b.To && park.PlacementMatches(o.Claim.Op.Intent["home_region"], to, o.Claim.Op.Intent["to"]) |
| + restore := o.Claim.Op.Kind == hostclaim.KindLanding && o.Claim.Op.Phase != "committed" && a.State == "landing" && a.CommittedAt == nil && a.Rollback == nil && b.State == "rolling_back" && b.CommittedAt == nil && b.Rollback != nil && b.Rollback.ID != "" && b.Rollback.RequestedAt != nil && b.Rollback.TargetPhase == "releasing" && b.Rollback.SourcePhase == "waiting" && b.Rollback.ReleaseDeadline != nil && b.Rollback.ReleaseDeadline.Equal(b.Rollback.RequestedAt.Add(15*time.Minute)) && from == b.To && to == b.From |
| + // The rollback region is the original record's Move.From (a.From == |
| + // b.From above), never claim input: the claim carries no source region. |
| +--- a/internal/hostops/move_enrollment.go |
| ++++ b/internal/hostops/move_enrollment.go |
| +@@ -151,7 +151,7 @@ |
| + return nil, ErrIdentity |
| + } |
| + c := moving.Claim |
| +- if c.Op.Kind != hostclaim.KindMoving || c.Op.Phase != hostclaim.PhaseMoving || p.ID != c.Op.ID || p.Incarnation != c.Holder.Incarnation || park.PlacementRegion(p.To) != c.Op.Intent["to"] || |
| ++ if c.Op.Kind != hostclaim.KindMoving || c.Op.Phase != hostclaim.PhaseMoving || p.ID != c.Op.ID || p.Incarnation != c.Holder.Incarnation || !park.PlacementMatches(c.Op.Intent["home_region"], p.To, c.Op.Intent["to"]) || |
| + c.Op.Intent["admission_id"] != h.id || c.Op.Intent["app_id"] != h.appID { |
| + return nil, ErrIdentity |
| + } |
| +--- a/internal/hostops/move_fence_release.go |
| ++++ b/internal/hostops/move_fence_release.go |
| +@@ -160,7 +160,7 @@ |
| + return err |
| + } |
| + m := rec.Move |
| +- if m == nil || m.ID != g.o.OpID || m.Incarnation != g.o.Incarnation || m.Rollback == nil || !hostclaim.LostMoveTargetParked(o.Claim, m.To) { |
| ++ if m == nil || m.ID != g.o.OpID || m.Incarnation != g.o.Incarnation || m.Rollback == nil || !hostclaim.LostMoveTargetParked(o.Claim, park.PlacementRegion(m.To)) { |
| + return ErrPending |
| + } |
| + if m.Rollback.TargetParkedAt != nil { |
| +--- a/internal/hostops/move_loss_owner.go |
| ++++ b/internal/hostops/move_loss_owner.go |
| +@@ -314,7 +314,7 @@ |
| + // rollback, phase, Dirty and return lane. |
| + func recordLossIntent(in *moveLossIntent, rec provision.Record, etag string, seq uint64, op hostclaim.Op, claimETag string, prior *supersededLoss) error { |
| + m := rec.Move |
| +- if m == nil || m.ID != in.Move || m.Incarnation != in.Incarnation || park.PlacementRegion(m.To) != in.TargetRegion || park.PlacementRegion(m.From) == "" || m.From == m.To { |
| ++ if m == nil || m.ID != in.Move || m.Incarnation != in.Incarnation || !park.PlacementMatches(op.Intent["home_region"], m.To, in.TargetRegion) || park.PlacementRegion(m.From) == "" || m.From == m.To { |
| + return hostresolve.ErrResolve |
| + } |
| + if m.State == provision.MoveCommitted || m.CommittedAt != nil { |
| +--- a/internal/hostops/move_recovery_bridge.go |
| ++++ b/internal/hostops/move_recovery_bridge.go |
| +@@ -5,6 +5,7 @@ |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "encoding/json" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "strconv" |
| + "time" |
| + |
| +@@ -72,7 +73,9 @@ |
| + // CheckMovingBeforeFlip reads the move's own intent while the claim is |
| + // moving ("source", "to") and the conversion's once relocating |
| + // ("source_box", "region", "move_to_region"). The region it returns is the |
| +-// record's; ConvertMovingToRelocating compares it with the fence binding. |
| ++// verified roster identity for the record placement; the home code stays |
| ++// empty in the decision. ConvertMovingToRelocating compares the returned |
| ++// identity with the unmodified fence binding. |
| + // ConvertMovingToRelocating calls it again inside its transition, so the |
| + // admission reread here is the last read before the one claim CAS. |
| + func (g movingRecordDecision) CheckMovingBeforeFlip(ctx context.Context, o hostclaim.Observed, opID string, ref hostclaim.FenceRef) (string, error) { |
| +@@ -105,7 +108,11 @@ |
| + if d.MovingIdentity != moving || c.Gen < d.ClaimGen { |
| + return "", hostclaim.ErrConflict |
| + } |
| +- source, region, to = c.Op.Intent["source"], rec.Region, c.Op.Intent["to"] |
| ++ region = park.PlacementRegion(rec.Region) |
| ++ if rec.Region == "" && c.Op.Intent["home_region"] != "" { |
| ++ region = c.Op.Intent["home_region"] |
| ++ } |
| ++ source, to = c.Op.Intent["source"], c.Op.Intent["to"] |
| + case hostclaim.KindRelocating: |
| + convertedGen, err := strconv.ParseUint(c.Op.Journal["conversion_claim_gen"], 10, 64) |
| + raw, e := json.Marshal(d) |
| +@@ -125,7 +132,7 @@ |
| + return "", err |
| + } |
| + if source == "" || region == "" || d.DecidedAt.IsZero() || d.Fence != string(raw) || d.Enrollment != ref.Enrollment || d.Enrollment != c.Op.Intent["source_enrollment"] || d.SourceBox != source || |
| +- d.Region != region || rec.Region != d.Region || rec.Move.From != d.Region || to == "" || rec.Move.To != to || rec.Move.FlippedAt != nil || rec.Move.CommittedAt != nil || rec.Move.Rollback != nil { |
| ++ !park.PlacementMatches(c.Op.Intent["home_region"], d.Region, region) || rec.Region != d.Region || rec.Move.From != d.Region || !park.PlacementMatches(c.Op.Intent["home_region"], rec.Move.To, to) || rec.Move.FlippedAt != nil || rec.Move.CommittedAt != nil || rec.Move.Rollback != nil { |
| + return "", ErrIdentity |
| + } |
| + switch d.PreviousState { |
| +@@ -136,10 +143,10 @@ |
| + if g.placement == nil { |
| + return "", ErrContract |
| + } |
| +- if err := g.placement.CheckOneBoxRegion(ctx, source, d.Region); err != nil { |
| +- return "", err |
| +- } |
| +- return d.Region, nil |
| ++ if err := g.placement.CheckOneBoxRegion(ctx, source, region); err != nil { |
| ++ return "", err |
| ++ } |
| ++ return region, nil |
| + } |
| + |
| + // ConversionDecisionDigest binds the converted claim to the exact record |
| +@@ -242,10 +249,10 @@ |
| + return err |
| + } |
| + region := rec.Region |
| +- if region == "" || region != fence.Region() || rec.Move == nil || rec.Move.From != region { |
| ++ if !park.PlacementMatches(c.Op.Intent["home_region"], region, fence.Region()) || rec.Move == nil || rec.Move.From != region { |
| + return hostclaim.ErrFence |
| + } |
| +- if err := placement.CheckOneBoxRegion(ctx, source, region); err != nil { |
| ++ if err := placement.CheckOneBoxRegion(ctx, source, fence.Region()); err != nil { |
| + return err |
| + } |
| + raw, err := json.Marshal(ref) |
| +--- a/internal/hostops/move_recovery_grant.go |
| ++++ b/internal/hostops/move_recovery_grant.go |
| +@@ -5,6 +5,7 @@ |
| + "crypto/rand" |
| + "encoding/json" |
| + "errors" |
| ++ "github.com/carlosframework/platform/internal/park" |
| + "strconv" |
| + "sync" |
| + "sync/atomic" |
| +@@ -418,7 +419,7 @@ |
| + return err |
| + } |
| + n := 0 |
| +- transition = hostclaim.Commit(g.o.OpID, m.From, c.Op.Intent["source"], m.To, source, staging, func() string { n++; return id + strconv.Itoa(n) }) |
| ++ transition = hostclaim.Commit(g.o.OpID, park.PlacementRegion(m.From), c.Op.Intent["source"], park.PlacementRegion(m.To), source, staging, func() string { n++; return id + strconv.Itoa(n) }) |
| + default: |
| + return ErrIdentity |
| + } |
| +@@ -477,10 +478,10 @@ |
| + } |
| + return hostclaim.VerifyOperationFence(ctx, g.w.raw, authority, c.Op, c.Op.Intent["source_box"]) |
| + } |
| +- if c.Op.Kind != hostclaim.KindMoving || c.Op.Intent["source_enrollment"] != ref.Enrollment || fence.Box() != c.Op.Intent["source"] || fence.Region() != rec.Region { |
| ++ if c.Op.Kind != hostclaim.KindMoving || c.Op.Intent["source_enrollment"] != ref.Enrollment || fence.Box() != c.Op.Intent["source"] || !park.PlacementMatches(c.Op.Intent["home_region"], rec.Region, fence.Region()) { |
| + return hostclaim.ErrFence |
| + } |
| +- if err := placement.CheckOneBoxRegion(ctx, fence.Box(), rec.Region); err != nil { |
| ++ if err := placement.CheckOneBoxRegion(ctx, fence.Box(), fence.Region()); err != nil { |
| + return err |
| + } |
| + if rec.Move == nil || rec.Move.ID != g.o.OpID || rec.Move.Incarnation != h.Incarnation { |
| +--- a/internal/park/region_names.go |
| ++++ b/internal/park/region_names.go |
| +@@ -39,3 +39,25 @@ |
| + } |
| + |
| + func (v View) sameRegion(a, b string) bool { return SameRegion(v.HomeRegion, a, b) } |
| ++ |
| ++// PlacementMatches compares a record code with a nonempty engine identity. |
| ++func PlacementMatches(home, code, engine string) bool { |
| ++ return SameRegion(home, PlacementRegion(code), engine) |
| ++} |
| ++ |
| ++// BoundHome joins the verified local binding and the admitted operation's |
| ++// binding for history reads. It never changes an engine's ownership. |
| ++func BoundHome(local, operation string) (string, error) { |
| ++ for _, home := range []string{local, operation} { |
| ++ if home != "" && PlacementRegion(home) != home { |
| ++ return "", ErrUnsafe |
| ++ } |
| ++ } |
| ++ if local != "" && operation != "" && local != operation { |
| ++ return "", ErrUnsafe |
| ++ } |
| ++ if local != "" { |
| ++ return local, nil |
| ++ } |
| ++ return operation, nil |
| ++} |
| +--- a/internal/park/remote.go |
| ++++ b/internal/park/remote.go |
| +@@ -329,7 +329,11 @@ |
| + if v.ClaimETag != obs.ETag || !e.OwnsRegion(v.Region) { |
| + return obs, v, ErrChanged |
| + } |
| +- v.HomeRegion = e.HomeRegion |
| ++ home, err := BoundHome(e.HomeRegion, obs.Claim.Op.Intent["home_region"]) |
| ++ if err != nil { |
| ++ return obs, v, err |
| ++ } |
| ++ v.HomeRegion = home |
| + return obs, v, nil |
| + } |
| + func (e Engine) selected(ctx context.Context, w Work, o hostclaim.Obligation, held HeldLease, vs []lease.ObjectVersion) (hostclaim.Observed, View, []lease.ObjectVersion, error) { |
| +--- a/internal/provision/move_relocation.go |
| ++++ b/internal/provision/move_relocation.go |
| +@@ -44,7 +44,7 @@ |
| + return err |
| + } |
| + var ref struct{ Enrollment, Key, SHA256 string } |
| +- if len(movingIdentity) != 64 || len(holderIdentity) != 64 || claimGen == 0 || claimETag == "" || box == "" || enrollment == "" || region == "" || now.IsZero() || |
| ++ if len(movingIdentity) != 64 || len(holderIdentity) != 64 || claimGen == 0 || claimETag == "" || box == "" || enrollment == "" || !ValidRegion(region) || now.IsZero() || |
| + json.Unmarshal([]byte(fence), &ref) != nil || ref.Enrollment != enrollment || ref.Key != "control/box-fences/"+enrollment+".json" || len(ref.SHA256) != 64 || |
| + rec.Region != region || rec.Move.From != region { |
| + return fmt.Errorf("%w: the relocation decision does not match this move", ErrMoveConflict) |
| ``` |
| ## Task H: Classify the census changes |
| -Run `go test ./internal/provision ./internal/console -run 'TestPlan9B8|TestClaimAuthoritySourceCensus' -count=1` before applying this patch. The record census must report these nine changed units; the console authority census must report only the changed VerifyMoveTargetFence argument. Then apply the patch and rerun the same command. |
| +Rev 2 changes four additional inventoried units and revises productionClaimRegional.View's earlier digest. Keep the same classes and site-kind sets: |
| + |
| +| Unit | Class | Reviewed rev 2 change | |
| +| --- | --- | --- | |
| +| productionClaimRegional.View | key-site | Bind remote history to the admitted operation; preserve local ownership. | |
| +| movesWorker.sourceRound | nonrecord | Map the target placement before touched-region checks and Touch; the existing staging write is unchanged. | |
| +| conditionalRecordStore.putLifecycle | record-writer | Decode omitted home placement fields; compare flip target with the bound engine identity. All record and admission guards remain. | |
| +| MoveRecoveryGrant.StampLostMoveTargetParked | record-writer | Map the record target before the engine predicate. | |
| +| MoveRecoveryGrant.Convert | record-writer | Compare the placement with the verified fence through the bound alias; retain roster identity for the one-box authority check. | |
| + |
| +The original nine units below retain their stated classifications. There are thirteen changed census units in total, not fourteen: productionClaimRegional.View is in both rounds. No class or site-kind set is widened. |
| + |
| + |
| +Run `go test ./internal/provision ./internal/console -run 'TestPlan9B8|TestClaimAuthoritySourceCensus' -count=1` before applying this patch. The record census must report the thirteen classified changed units; the console authority census must report only the changed VerifyMoveTargetFence argument. Then apply the patch and rerun the same command. |
| | Unit | Classification retained | Reason for changed source | |
| | --- | --- | --- | |
| @@ -2073,21 +3178,25 @@ No class, site-kind set, raw sink or authority call is added or removed. The dig |
| ```diff |
| --- a/internal/provision/plan9_b8_census_test.go |
| +++ b/internal/provision/plan9_b8_census_test.go |
| -@@ -474,11 +474,11 @@ |
| +@@ -474,13 +474,13 @@ |
| var plan9B8CensusInventory = []censusEntry{ |
| {"internal/hostbox/parkleases.go", "ReplicaParkLeaseSet", "key-site", "lit:instances", "0ca8d9b743402fda9d15cc215464423b35e21c1c2b47f91294d76188b7d43fb6"}, |
| {"internal/host/claimretirementcensus.go", "ClaimRetirementCensus", "key-site", "lit:instances", "abc0bd8c5f5d328abfac8821f42f8f23954723ed6506747f33b0197824e2611f"}, |
| - {"internal/host/claimproduction.go", "productionClaimRegional.View", "key-site", "lit:instances", "61ff9ff47054b6a96cb24467c7aaf41c112b7c5129987af5ce27e0b7f862348e"}, |
| - {"internal/hostops/move_loss_admission.go", "moveLossOwner.ValidateLossRecovery", "key-site", "key:RecordKey", "ea2aa35a069ffdc846ecb208c33940245fc865795188818e86ea7a93135dd6a9"}, |
| -+ {"internal/host/claimproduction.go", "productionClaimRegional.View", "key-site", "lit:instances", "b0c6bac0beb2cbff790b646a7d978a3f22e6ecc7bac78b63402e73fe5c6e9776"}, |
| ++ {"internal/host/claimproduction.go", "productionClaimRegional.View", "key-site", "lit:instances", "85fa91931c06b2fb1f8e53512a219190fcb146b9919b1e625b8562c299209840"}, |
| + {"internal/hostops/move_loss_admission.go", "moveLossOwner.ValidateLossRecovery", "key-site", "key:RecordKey", "1ad4af4595c764fc5051e50c2b187472cb20ce73c3b3e8245b8527228f326f5f"}, |
| {"internal/host/claimretirementcensus.go", "claimArchivedLocations", "key-site", "lit:instances", "9ba8ea4ecb5ad63621c1322467b9a80f8f5703896ba1fc3f7cb1addd56418950"}, |
| {"internal/host/claimretirementcensus.go", "claimLegacyIncarnation", "key-site", "lit:instances", "31a14ea105c67e00a24f06b9500a221ee5bfc47402b9c33cecfde807e51db530"}, |
| - {"internal/hostops/move_fence_release.go", "MoveRecoveryGrant.ReleaseFencedTarget", "record-writer", "key:WriteRecord,key:writeRecord", "031a3a8169a92775bff8a1a1b08cfe82a5a613156a1bce45dc9cfcb34c56befe"}, |
| +- {"internal/hostops/move_fence_release.go", "MoveRecoveryGrant.StampLostMoveTargetParked", "record-writer", "key:WriteRecord", "b082db5634a2f4d9a5b45cc4a9cb725964e9bb24b7aeb53a6346167d7737227c"}, |
| +- {"internal/hostops/move_recovery_grant.go", "MoveRecoveryGrant.Convert", "record-writer", "key:WriteRecord", "794963a24a0a50d980505f83ac912973a10267b21f74bd41c56584ab9b0b25a1"}, |
| + {"internal/hostops/move_fence_release.go", "MoveRecoveryGrant.ReleaseFencedTarget", "record-writer", "key:WriteRecord,key:writeRecord", "fc0dda0802a3cc0cf8cc33d2d68aa9ae2eb49a90e20720607fec613b822f4f7f"}, |
| - {"internal/hostops/move_fence_release.go", "MoveRecoveryGrant.StampLostMoveTargetParked", "record-writer", "key:WriteRecord", "b082db5634a2f4d9a5b45cc4a9cb725964e9bb24b7aeb53a6346167d7737227c"}, |
| - {"internal/hostops/move_recovery_grant.go", "MoveRecoveryGrant.Convert", "record-writer", "key:WriteRecord", "794963a24a0a50d980505f83ac912973a10267b21f74bd41c56584ab9b0b25a1"}, |
| ++ {"internal/hostops/move_fence_release.go", "MoveRecoveryGrant.StampLostMoveTargetParked", "record-writer", "key:WriteRecord", "911ab90f0786d646b494a62c2d061852c35273dc538408636ebf2ec613fa2198"}, |
| ++ {"internal/hostops/move_recovery_grant.go", "MoveRecoveryGrant.Convert", "record-writer", "key:WriteRecord", "bbb8c3aea28bcd7b3c45e5c4df3951509bb4e21158cdb67cea91a4486f3381da"}, |
| // The recovery pass's move log writer: one create-only entry of a move's |
| + // log under its report prefix, for a host whose claim still names the |
| + // app; never a record (Plan 6 Task 3 fix round 3, ruling 1). |
| @@ -612,11 +612,11 @@ |
| {"internal/console/apiinstancemove.go", "Server.registerMovesPhase1Routes", "key-site", "lit:instances", "5af589d288ba22856f631481c40cc001aac342ea74cee6ee0464ec77d0320bbc"}, |
| {"internal/console/apiinstances.go", "Server.instanceManifest", "key-site", "lit:instances", "e6160ffe84abda9163527ccb33e2ffff2109b35908adb2f3a754534325658fdf"}, |
| @@ -2102,7 +3211,12 @@ No class, site-kind set, raw sink or authority call is added or removed. The dig |
| {"internal/console/apistatusinstances.go", "Server.handleAPIStatusInstances", "key-site", "lit:instances", "2b4b9a2bea5f2fc5727869a5dcb8ae96c020e72571621f19898b195ca3b61aaa"}, |
| {"internal/console/apisystemupdate.go", "Server.mirrorSystemArtifact", "nonrecord", "sel:Put", "76a8dcb558e3723ad5774788da37c22234c229dc3257ded5a6c7b04746c69c2f"}, |
| {"internal/console/apiupdates.go", "Server.handleAPIUpdatesPut", "record-writer", "key:UpdateRecord", "53044a537795302e7ffd8fa72799b7a77e7bed277f00ad17ed1fd465b7f7cfaa"}, |
| -@@ -709,14 +709,14 @@ |
| +@@ -705,18 +705,18 @@ |
| + {"internal/host/movesmeasure.go", "measureS3", "nonrecord", "sel:Put", "3ae9fc093b453e19a5b19be0929533b30a0e2a3e998ba8dc766f41287db2caca"}, |
| + {"internal/host/movespass.go", "legacyMoveRound", "record-writer", "key:SetRecordMove", "1e13a3ea099d45318d3b64b067448c87dc4a136cd909348f526e4583ac00150a"}, |
| + {"internal/host/movesphase1.go", "writeMoveLedger", "nonrecord", "sel:Put", "597a70ac06ceb4e9eb52bb9f61b28a4937e703fa4a0916f64bd95d9594f0556e"}, |
| +- {"internal/host/movesphase1.go", "movesWorker.sourceRound", "nonrecord", "sel:PutIfAbsentVersioned", "c5bc2e6cb57b819ae1da485d37093c9a9ed3cdc9e9e16e8a8e312fd5ad2e2fe3"}, |
| ++ {"internal/host/movesphase1.go", "movesWorker.sourceRound", "nonrecord", "sel:PutIfAbsentVersioned", "810e5a9abac7dea9220f83eae924cea99b0eca0f002764c88ec35de26a356dff"}, |
| {"internal/host/movesphase1.go", "movesWorker.refreshCrossRate", "nonrecord", "sel:CopyVersioned", "7e03e680bd61be26dddcf168cacd2427bb535571dfd326c4a3130ebe65ea1968"}, |
| {"internal/host/movesphase1.go", "movesWorker.writeSourceReport", "nonrecord", "sel:PutReplace", "370407cca2c1b91f6e5e7f135238f04c0dce9d7986e6e1ee9bebbf88cad0b9e0"}, |
| {"internal/host/movesphase1.go", "movesWorker.decideReady", "record-writer", "key:SetRecordMove", "d29c22eafacfb894b96cd0c40ec447b6ab02f46ea087201685627861f992085d"}, |
| @@ -2119,6 +3233,15 @@ No class, site-kind set, raw sink or authority call is added or removed. The dig |
| {"internal/hostbox/requeststore.go", "RequestStore.CopyVersion", "versioned-transport", "def:CopyVersion,sel:CopyVersion", "3a50049ce7ca7f2cbc770bef46218944cdc7000205a86c20e4932f0b510da28f"}, |
| {"internal/hostbox/requeststore.go", "RequestStore.PutIfAbsent", "versioned-transport", "def:PutIfAbsent,sel:PutIfAbsent", "3666cbe5d2b91c1c652ffb4459c10a374fbd92e325e4e5c574fa3525d0ce2ffe"}, |
| {"internal/hostclaim/adopt.go", "Adopt", "key-site", "lit:instances", "24c06b99cce6c638acb19826985cfadd3c736fd0ace7289bcf7ac3dfe784f05a"}, |
| +@@ -746,7 +746,7 @@ |
| + {"internal/hostops/lifecycle_grant.go", "authorizeSignedControl", "reference", "str:Put", "cf012aebc7b4e0ea8dfef48ea3817a7466f1dabacd7cf1e5be07b2302bf183ba"}, |
| + {"internal/hostops/lifecycle_record.go", "type lifecycleRecordKey", "key-site", "key:lifecycleRecordKey", "09ab1fd62d0be032bd735d1f60488cdb19128f8573b00bbd7a70f582208da75d"}, |
| + {"internal/hostops/lifecycle_record.go", "setLifecycleRecordMove", "record-writer", "key:SetRecordMove,key:lifecycleRecordKey", "37691dfd5158c644b12e28383243f92a7868e075eade79be79e7149a18f8d420"}, |
| +- {"internal/hostops/lifecycle_record.go", "conditionalRecordStore.putLifecycle", "record-writer", "key:putSequencedRecord", "03ac054bfed5f28429eaaefca22ab940ad71bace1897064e1c770cec361bcc03"}, |
| ++ {"internal/hostops/lifecycle_record.go", "conditionalRecordStore.putLifecycle", "record-writer", "key:putSequencedRecord", "75f03db96d509d340fc3edf62a5ff60957ab75d08d6586fea6e342c5a9c3eb8d"}, |
| + {"internal/hostops/mail_gate.go", "ControlWriter.AcquireMailGate", "nonrecord", "sel:Put", "f37147b214f176da0565268aba714f15216f3bdd9c5f279d6c404098a499b482"}, |
| + {"internal/hostops/mail_gate.go", "ControlWriter.ReleaseMailGate", "nonrecord", "sel:Put", "aea8bc583a0a28cd61e8c81c6d466b02e4fb6a086ba921f01fdd7dfdb286191c"}, |
| + {"internal/hostops/mail_gate.go", "ControlWriter.RecordMailGateMember", "nonrecord", "sel:Put", "ed87514c0cfd15664765deb5aa0051c37b14ecebf0ba1a77459da4f8b7e7465f"}, |
| @@ -847,8 +847,8 @@ |
| {"internal/objstore/sidecar.go", "claimRawPutSidecar", "nonrecord", "sel:Put", "233e3d808dd462ff83aecdac5176b180556b948c552aa26f6e0b778df91a24c7"}, |
| {"internal/ops/litestream.go", "FleetDBsWithSkipped", "key-site", "lit:instances", "f62291df0685db15529c4946dca358a29aba66fc31e81aab299de579e37f60f1"}, |
| @@ -2165,7 +3288,7 @@ Apply this to the separately identified rev 6.32 design spec, after checking its |
| 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 |
| -@@ -1123,6 +1125,57 @@ |
| +@@ -1123,6 +1125,76 @@ |
| 5. Execute Plan 4’s exact-version park. |
| 6. Acknowledge with the chore’s own operation ID, updating `quiet_after` in the same CAS. |
| @@ -2219,6 +3342,25 @@ Apply this to the separately identified rev 6.32 design spec, after checking its |
| +remain required. Roll out the alias-aware readers and quiet checks on all |
| +home workers before allowing home moves; earlier readers cannot service |
| +`@home` chores safely. The home name must remain stable across that rollout. |
| ++ |
| ++The operation's verified home binding also projects remote admission history |
| ++for a named worker's park view. It does not confer home ownership on that |
| ++worker. Every moves engine retains the local binding supplied by MoveParks; |
| ++observation reads retain the operation binding for mixed-name live chains. |
| ++ |
| ++For an unnamed console home, legacy database admission names are recognized |
| ++only from the configured home bucket's HibernateRegion fallback. Admission |
| ++contents cannot establish an alias. Named placements and an explicit home |
| ++name do not use that fallback. An unresolved console region still refuses |
| ++startup. Signed stop receipts keep their roster region and exact bytes; only |
| ++the comparison with the operation target uses the bound alias. |
| ++ |
| ++Before a flip, recovery compares record placement codes through the shared |
| ++rule and returns the source's roster identity to the claim conversion. Both |
| ++recovery entry points keep the decision's Region, Move.From, Move.To and the |
| ++record Region as placement codes, including an omitted empty Region field. |
| ++The roster, enrollment, signature, fence, record sequence and admission checks |
| ++remain intact. No empty claim or engine identity is accepted. |
| + |
| Regional ownership remains `region:<r>`. Any eligible enrolled worker with access to that region may perform the chore under the aggregate leases. Local chores remain bound to the original machine enrollment. |
| @@ -2242,6 +3384,7 @@ Run on the exact replayed tree, using the shared cache and flags from bootstrap: |
| ```bash |
| go test ./internal/park ./internal/hostbox ./internal/hostops ./internal/console ./internal/host -run TestPlan13 -count=1 |
| +go test ./internal/console -run TestDoorPlanUnnamedHomeRegion -count=1 |
| go test ./internal/provision ./internal/console -run 'TestPlan9B8|TestClaimAuthoritySourceCensus' -count=1 |
| go vet ./... |
| GOOS=darwin GOARCH=amd64 go build ./... |
| @@ -2267,7 +3410,7 @@ Never include GOCACHE in cleanup. |
| | Item | Disposition | |
| | --- | --- | |
| | UI text, CLI help, emails, member-facing errors | No additions or changes. Existing error values and refusal strings are reused. | |
| -| HomeRegion, HomePlacementCode, SameRegion, QuietUntil, HolderRegionView, HomeView, OwnsRegion, ClaimMoveRosterPlaces, MoveFenceRegions | Internal API identifiers; no product copy. | |
| +| HomeRegion, HomePlacementCode, SameRegion, QuietUntil, HolderRegionView, HomeView, OwnsRegion, PlacementMatches, BoundHome, ClaimMoveRosterPlaces, MoveFenceRegions, ParkHomeRegion, dbClaimHomeRegion | Internal API identifiers; no product copy. | |
| | `op.intent.home_region` | Internal additive operation-map key; no top-level wire field or strict-reader schema change. | |
| | `source_region: "@home"` | Existing internal loss-intent field with a nonempty engine value; never shown as a placement code. | |
| | Code comments, tests, this plan, report and rev 6.33 amendment | Implementation instructions for the controller. No new diagnostic wording is proposed for shipping. | |