moves: env report and pin version 2, fail closed on v1 (home part 2, 2b r3)
A v1 env report has no instance layer names, so an absent Instance cannot tell an older producer, which never pinned a box variable, from a newer one with none instance-set. The report is now version 2 (moves.EnvReportVersion) and its digest a v2 pin (env2:sha256:...). T3 refuses a v1 source report before comparing (MV-N513, pending), and the probe owners refuse a v1 pin in Select, Materialize and the committed wake's checks, through their existing refusals. No v1 digest can equal a v2 pin, so an older target refuses a newer pin too. This is the report's version; moves.ProtocolVersion is unchanged (R5). Witnesses, named to named and named to home: a v1 source report is refused at T3; a v1 pin with the instance's HOSTNAME removed after the pin (the target inheriting another) is refused at the probe; a v1 target's digest never matches a v2 pin, whether the target adds or loses an instance HOSTNAME. Census: the home-part2 overlay's rows re-frozen again at main's dispositions, new seal, ruling owed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 files changed,
+278
−41
approved_moves_copy_test.go+1 −1docs/superpowers/plans/2026-10-06-moves-phase1-copy.md+6 −0internal/host/claimmoveaccess_test.go+2 −2internal/host/movesenvskew_test.go+165 −0internal/host/movesprobeowners.go+6 −4internal/host/movesreport.go+28 −19internal/moves/envreport.go+29 −5internal/moves/envreport_test.go+31 −1internal/provision/plan14_root_reads_test.go+1 −1internal/provision/testdata/plan14-root-reads-home-part2.tsv+9 −8
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.014s
? 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 306.947s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.705s
ok github.com/carlosframework/platform/cmd/moveprobe 93.765s
ok github.com/carlosframework/platform/cmd/zddprobe 0.824s
ok github.com/carlosframework/platform/internal/activator 249.071s
ok github.com/carlosframework/platform/internal/agentsock 0.025s
ok github.com/carlosframework/platform/internal/alerting 0.016s
ok github.com/carlosframework/platform/internal/analytics 0.041s
ok github.com/carlosframework/platform/internal/appconfig 2.828s
ok github.com/carlosframework/platform/internal/apperr 0.116s
ok github.com/carlosframework/platform/internal/applog 0.189s
ok github.com/carlosframework/platform/internal/awsinv 0.015s
ok github.com/carlosframework/platform/internal/boxchoice 0.007s
ok github.com/carlosframework/platform/internal/boxid 0.958s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.011s
ok github.com/carlosframework/platform/internal/caps 0.015s
ok github.com/carlosframework/platform/internal/certwarm 0.020s
ok github.com/carlosframework/platform/internal/cgroup 0.839s
ok github.com/carlosframework/platform/internal/changelog 0.387s
ok github.com/carlosframework/platform/internal/clamd 0.057s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 398.710s
? 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 5.818s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.009s
ok github.com/carlosframework/platform/internal/dbverify 0.007s
ok github.com/carlosframework/platform/internal/dnsrec 0.003s
ok github.com/carlosframework/platform/internal/docsite 0.007s
ok github.com/carlosframework/platform/internal/domains 2.525s
ok github.com/carlosframework/platform/internal/doorkey 0.009s
ok github.com/carlosframework/platform/internal/economics 0.007s
ok github.com/carlosframework/platform/internal/edge 26.916s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.009s
ok github.com/carlosframework/platform/internal/fleetapi 0.133s
ok github.com/carlosframework/platform/internal/gate 1.657s
ok github.com/carlosframework/platform/internal/genselect 0.027s
ok github.com/carlosframework/platform/internal/geo 0.013s
ok github.com/carlosframework/platform/internal/geoip 0.030s
ok github.com/carlosframework/platform/internal/host 140.060s
ok github.com/carlosframework/platform/internal/hostactors 0.549s
ok github.com/carlosframework/platform/internal/hostbox 2.348s
ok github.com/carlosframework/platform/internal/hostclaim 0.627s
ok github.com/carlosframework/platform/internal/hostevidence 0.007s
ok github.com/carlosframework/platform/internal/hostexec 5.122s
ok github.com/carlosframework/platform/internal/hostfence 0.047s
ok github.com/carlosframework/platform/internal/hostfiles 10.474s
ok github.com/carlosframework/platform/internal/hostfloor 0.012s
ok github.com/carlosframework/platform/internal/hostguard 0.988s
ok github.com/carlosframework/platform/internal/hostops 1.741s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.003s
ok github.com/carlosframework/platform/internal/hostquarantine 0.002s
ok github.com/carlosframework/platform/internal/hostrelocate 0.559s
ok github.com/carlosframework/platform/internal/hostresolve 0.157s
ok github.com/carlosframework/platform/internal/hostwrite 0.002s
ok github.com/carlosframework/platform/internal/infraview 0.004s
ok github.com/carlosframework/platform/internal/instancectl 4.174s
ok github.com/carlosframework/platform/internal/instanceexec 2.725s
ok github.com/carlosframework/platform/internal/instancefs 0.016s
ok github.com/carlosframework/platform/internal/instancekey 0.003s
ok github.com/carlosframework/platform/internal/instanceupdates 0.017s
ok github.com/carlosframework/platform/internal/layout 1.592s
ok github.com/carlosframework/platform/internal/lease 9.819s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.006s
ok github.com/carlosframework/platform/internal/ledger 0.014s
ok github.com/carlosframework/platform/internal/mail 0.224s
ok github.com/carlosframework/platform/internal/maintenance 0.092s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.533s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.095s
ok github.com/carlosframework/platform/internal/metrics 0.006s
ok github.com/carlosframework/platform/internal/moves 21.413s
ok github.com/carlosframework/platform/internal/moves/archive 0.849s
ok github.com/carlosframework/platform/internal/objstore 0.202s
ok github.com/carlosframework/platform/internal/ops 0.213s
ok github.com/carlosframework/platform/internal/park 0.264s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 16.180s
ok github.com/carlosframework/platform/internal/publicrelease 0.007s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 4.990s
ok github.com/carlosframework/platform/internal/relay 4.238s
ok github.com/carlosframework/platform/internal/release 20.395s
ok github.com/carlosframework/platform/internal/rollout 3.723s
ok github.com/carlosframework/platform/internal/rolloutwords 0.005s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/rootparticipation 0.008s
ok github.com/carlosframework/platform/internal/rootstate 0.029s
ok github.com/carlosframework/platform/internal/runs 0.598s
ok github.com/carlosframework/platform/internal/s3compat 0.009s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.557s
ok github.com/carlosframework/platform/internal/seeds 0.011s
ok github.com/carlosframework/platform/internal/sim/hostlife 10.189s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.495s
ok github.com/carlosframework/platform/internal/status 0.043s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.215s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.003s
ok github.com/carlosframework/platform/internal/updatewords 0.004s
ok github.com/carlosframework/platform/internal/usage 0.004s
ok github.com/carlosframework/platform/internal/version 0.005s
ok github.com/carlosframework/platform/internal/vet 1.606s
=== 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
linux/arm64 carlos binary: 46399650 bytes (ceiling 52000000)
--- 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.023s
=== RUN TestT4StorageProbeMinio
2026/10/15 17:30:22 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEC4326380CEC2, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok github.com/carlosframework/platform/internal/host 0.021s
minio contract: PASS
diff --git a/approved_moves_copy_test.go b/approved_moves_copy_test.go| index df5132644..b237e0260 100644 |
| --- a/approved_moves_copy_test.go |
| +++ b/approved_moves_copy_test.go |
| @@ -89,7 +89,7 @@ var movesCopyIDs = []string{ |
| "MV-N440", "MV-N441", "MV-N442", "MV-N443", |
| "MV-N450", "MV-N451", "MV-N454", "MV-N455", "MV-N456", |
| "MV-N500", |
| - "MV-N512", |
| + "MV-N512", "MV-N513", |
| } |
| // copyRow is one row of the record. The File cell is the file the sentence is |
diff --git a/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md b/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md| index 89da8c052..50bc8524d 100644 |
| --- a/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md |
| +++ b/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md |
| @@ -456,3 +456,9 @@ Plan `docs/superpowers/plans/2026-10-15-moves-home-region-part2.md` (amadan #236 |
| | id | file | text | status | |
| |---|---|---|---| |
| | MV-N512 | internal/moves/envreport.go (a T3 kind inside MV-N129's list) | region-present | pending | |
| + |
| +Task 2b's row (fix round 3) is T3's refusal of a version 1 env report. A v1 report carries no instance layer names, so it cannot say whether a box variable such as HOSTNAME came from the instance's files or the box: a target on this release refuses it before comparing anything, fail closed, until the source runs this release too. |
| + |
| +| id | file | text | status | |
| +|---|---|---|---| |
| +| MV-N513 | internal/host/movesreport.go | the source reported its launch environment in an older format | pending | |
diff --git a/internal/host/claimmoveaccess_test.go b/internal/host/claimmoveaccess_test.go| index b26eccdae..234e124e5 100644 |
| --- a/internal/host/claimmoveaccess_test.go |
| +++ b/internal/host/claimmoveaccess_test.go |
| @@ -216,7 +216,7 @@ func (m *plan10InputOwner) InputPolicy(ctx context.Context, c *hostclaim.Claim, |
| func TestPlan10CommittedInputDispatch(t *testing.T) { |
| reader := &mpReader{rec: mpRecord()} |
| reader.rec.Move.State = provision.MoveCommitted |
| - reader.rec.Move.Inputs.EnvReport = "report" |
| + reader.rec.Move.Inputs.EnvReport = "env2:sha256:report" // a v2 pin (moves.CurrentEnvPin) |
| owner := &plan10InputOwner{movesProbeOwners: &movesProbeOwners{Records: reader.read}} |
| serving := 0 |
| legacy := func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { |
| @@ -307,7 +307,7 @@ func TestPlan10CommittedColdAdmissionInputs(t *testing.T) { |
| t.Fatal(err) |
| } |
| rec := &provision.Record{Host: recoveryHost, Move: &provision.MoveState{ID: "m1", Incarnation: o.Claim.Holder.Incarnation, State: provision.MoveCommitted, Inputs: mpPins()}} |
| - rec.Move.Inputs.EnvReport = "report" |
| + rec.Move.Inputs.EnvReport = "env2:sha256:report" // a v2 pin (moves.CurrentEnvPin) |
| read := MovesRecordReader(func(context.Context, string, string, string) (*provision.Record, error) { return rec, nil }) |
| owner := &plan10InputOwner{movesProbeOwners: &movesProbeOwners{Records: read}} |
| bindings := ClaimOwnerBindings{InputPolicy: func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { |
diff --git a/internal/host/movesenvskew_test.go b/internal/host/movesenvskew_test.go| new file mode 100644 |
| index 000000000..4e310556a |
| --- /dev/null |
| +++ b/internal/host/movesenvskew_test.go |
| @@ -0,0 +1,165 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "encoding/json" |
| + "fmt" |
| + "os" |
| + "path/filepath" |
| + "sort" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/activator" |
| + "github.com/carlosframework/platform/internal/moves" |
| + "github.com/carlosframework/platform/internal/provision" |
| +) |
| + |
| +// Version skew of the env report and pin (home part 2, Task 2b fix round |
| +// 3). A v1 report carries no instance layer names, so it cannot say |
| +// whether a box variable was the instance's or the box's: an older |
| +// producer and a newer one with no instance-set box variables look the |
| +// same. A v2 participant therefore refuses a v1 report at T3 and a v1 pin |
| +// at the probe, and a v2 pin is in a form no v1 participant's digest can |
| +// equal. The older participant is emulated here by its own rule: f4f51ba6's |
| +// digest, every variable but the agent-owned and box variables, in the |
| +// "sha256:" form. |
| + |
| +// v1EnvDigest is a v1 participant's digest of a report's variables. |
| +func v1EnvDigest(r moves.EnvReport) string { |
| + var names []string |
| + for k := range r.Vars { |
| + if !moves.AgentOwnedVars[k] && !moves.BoxVars[k] { |
| + names = append(names, k) |
| + } |
| + } |
| + sort.Strings(names) |
| + h := sha256.New() |
| + for _, k := range names { |
| + fmt.Fprintf(h, "%s=%s\n", k, r.Vars[k]) |
| + } |
| + return "sha256:" + hex.EncodeToString(h.Sum(nil)) |
| +} |
| + |
| +// asV1Source rewrites the source's published report as a v1 source writes |
| +// it: version 1, no instance layer names, the v1 digest. |
| +func asV1Source(t *testing.T, h *phase1Harness) { |
| + t.Helper() |
| + ctx := context.Background() |
| + rec := h.read(t, p1Host) |
| + r := h.sourceReport(t) |
| + r.Env.V, r.Env.Instance = 1, nil |
| + r.Env.Digest = v1EnvDigest(r.Env) |
| + body, err := json.Marshal(r) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := moves.PutReplace(ctx, h.s, moves.SourceReportKey(moves.ReportPrefix(rec.Account, rec.App, rec.Host, rec.Move.ID)), body); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +const t3OldReportWant = "the source reported its launch environment in an older format" |
| + |
| +// TestT3RefusesAnOlderSourceReport: a v1 source with a v2 target. The |
| +// target refuses the source's v1 report at T3, before comparing, named to |
| +// named and named to home. |
| +func TestT3RefusesAnOlderSourceReport(t *testing.T) { |
| + for name, opts := range map[string][]p1Option{ |
| + "named to named": {realLaunchEnv}, |
| + "named to home": {intoHome, realLaunchEnv}, |
| + } { |
| + t.Run(name, func(t *testing.T) { |
| + h := newPhase1Harness(t, opts...) |
| + h.start(t, "manual") |
| + h.pass(t) |
| + asV1Source(t, h) |
| + h.targetPass(t, h.targetCfg()) |
| + r := h.targetReport(t) |
| + if t3 := r.Checks[moves.CheckEnv]; t3.OK || t3.Reason != t3OldReportWant { |
| + t.Fatalf("T3 on a v1 source report: %+v", t3) |
| + } |
| + if r.Inputs.EnvReport != "" { |
| + t.Fatalf("a refused T3 bound the source's env report: %q", r.Inputs.EnvReport) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +// TestProbeRefusesAnOlderPin: a v1 source with a v2 target, and the |
| +// instance's HOSTNAME removed after the pin. The source's .env set |
| +// HOSTNAME=a, which a v1 pin leaves out; after the pin the target's .env |
| +// loses it and the target inherits HOSTNAME=b from its box, which the v2 |
| +// rule also leaves out. The two digests would agree on a changed env; the |
| +// v2 target refuses the v1 pin instead, named to named and named to home. |
| +func TestProbeRefusesAnOlderPin(t *testing.T) { |
| + for name, opts := range map[string][]p1Option{ |
| + "named to named": {realLaunchEnv}, |
| + "named to home": {intoHome, realLaunchEnv}, |
| + } { |
| + t.Run(name, func(t *testing.T) { |
| + h := newCloseHarness(t, append([]p1Option{sourceEnvFile("APP_MODE=prod\nHOSTNAME=a\n")}, opts...)...) |
| + p := h.productionProbe(t) |
| + v1 := v1EnvDigest(h.sourceReport(t).Env) |
| + if _, err := provision.SetRecordMove(context.Background(), h.s, "bab", "app", p1Host, func(rec *provision.Record) error { |
| + rec.Move.Inputs.EnvReport = v1 |
| + return nil |
| + }); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(h.tdir, p1Host+".env"), []byte("APP_MODE=prod\n"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Setenv("HOSTNAME", "b") |
| + h.land(t) |
| + if len(p.admissions) != 0 { |
| + t.Fatalf("a v1 pin admitted a changed env: %+v", p.admissions) |
| + } |
| + if m := h.read(t, p1Host).Move; m.Reason != provision.RollbackConfigChanged || m.State == provision.MoveCommitted { |
| + t.Fatalf("the landing on a v1 pin: state %s reason %q", m.State, m.Reason) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +// TestOlderTargetCannotMatchANewerPin: a v2 source with a v1 target. A v1 |
| +// target judges its own launch by the v1 digest; whatever its env, that |
| +// digest never equals a v2 pin, so the older target refuses, fail closed. |
| +// Both directions of a box variable changing after the pin: |
| +// - the source's HOSTNAME is inherited (left out of the pin) and the |
| +// target's .env adds HOSTNAME=b; |
| +// - the source's .env sets HOSTNAME=a (pinned) and the target's .env |
| +// loses it, inheriting HOSTNAME=b. |
| +func TestOlderTargetCannotMatchANewerPin(t *testing.T) { |
| + for name, c := range map[string]struct { |
| + srcFile, tgtFile string |
| + }{ |
| + "the target adds an instance HOSTNAME": {"APP_MODE=prod\n", "APP_MODE=prod\nHOSTNAME=b\n"}, |
| + "the target loses the instance HOSTNAME": {"APP_MODE=prod\nHOSTNAME=a\n", "APP_MODE=prod\n"}, |
| + } { |
| + t.Run(name, func(t *testing.T) { |
| + t.Setenv("HOSTNAME", "a") |
| + h := newCloseHarness(t, sourceEnvFile(c.srcFile), intoHome, realLaunchEnv) |
| + pin := h.read(t, p1Host).Move.Inputs.EnvReport |
| + if !moves.CurrentEnvPin(pin) { |
| + t.Fatalf("the source's pin is not v2: %q", pin) |
| + } |
| + if err := os.WriteFile(filepath.Join(h.tdir, p1Host+".env"), []byte(c.tgtFile), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Setenv("HOSTNAME", "b") |
| + env, _, err := (&activator.ExecBackend{DataDir: h.tdir, Region: h.dst}).LaunchEnvironment(h.route) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + key, err := moves.ReportKeyFromSecrets(h.cfg.SecretsKey) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if got := v1EnvDigest(moves.ReportEnv(key, env)); got == pin { |
| + t.Fatalf("a v1 target's digest of a changed env matches the v2 pin %q", pin) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/movesprobeowners.go b/internal/host/movesprobeowners.go| index 73d970fb8..1fcd9d717 100644 |
| --- a/internal/host/movesprobeowners.go |
| +++ b/internal/host/movesprobeowners.go |
| @@ -640,7 +640,9 @@ func (m *movesProbeOwners) Select(ctx context.Context, rt registry.Route, o host |
| if l.Version != pins.Release { |
| return ClaimLaunchInputs{}, probeInputsChanged() |
| } |
| - if pins.EnvReport == "" || envDigest(m.EnvKey, l.Env, l.Instance) != pins.EnvReport { |
| + // A v1 pin is refused whatever it hashes: it cannot tell an instance's |
| + // box variable from the box's (moves.EnvReportVersion). |
| + if !moves.CurrentEnvPin(pins.EnvReport) || envDigest(m.EnvKey, l.Env, l.Instance) != pins.EnvReport { |
| return ClaimLaunchInputs{}, probeInputsChanged() |
| } |
| return ClaimLaunchInputs{Roles: []string{"main"}, Identity: launchIdentity(pins), Baseline: plan, Admission: admission, |
| @@ -822,7 +824,7 @@ func (m *movesProbeOwners) Materialize(ctx context.Context, o hostclaim.Observed |
| if err := m.Release(ctx, rt, pins.Release, heldIdentityWork{Work: released, ID: hostwrite.Identity{Release: pins.Release}}); err != nil { |
| return err |
| } |
| - if envDigest(m.EnvKey, in.Env, in.Instance) != pins.EnvReport { |
| + if !moves.CurrentEnvPin(pins.EnvReport) || envDigest(m.EnvKey, in.Env, in.Instance) != pins.EnvReport { |
| return probeInputsChanged() |
| } |
| m.recordFence(o.Host, h.Session, work) |
| @@ -1450,7 +1452,7 @@ func (m *movesProbeOwners) selectCommitted(ctx context.Context, rt registry.Rout |
| // on the launch itself: its environment's report and its release. |
| pins := v.rec.Move.Inputs |
| id := committedLaunchIdentity(m.EnvKey, l.Env, l.Instance, l.Version) |
| - if pins.EnvReport == "" || id != pinnedLaunchIdentity(pins) { |
| + if !moves.CurrentEnvPin(pins.EnvReport) || id != pinnedLaunchIdentity(pins) { |
| return ClaimLaunchInputs{}, fmt.Errorf("%w: the launch is not the move's pinned inputs", hostbox.ErrChanged) |
| } |
| admission, err := m.committedAdmission(o, v, gen, plan) |
| @@ -1541,7 +1543,7 @@ func (m *movesProbeOwners) InputPolicy(ctx context.Context, c *hostclaim.Claim, |
| rec.Move.State != provision.MoveCommitted || rec.Move.Inputs == nil || rec.Move.Inputs.Release == "" { |
| return hostbox.ErrChanged |
| } |
| - if rec.Move.Inputs.EnvReport == "" || id != pinnedLaunchIdentity(rec.Move.Inputs) { |
| + if !moves.CurrentEnvPin(rec.Move.Inputs.EnvReport) || id != pinnedLaunchIdentity(rec.Move.Inputs) { |
| return fmt.Errorf("%w: the committed launch is not the move's pinned inputs", hostbox.ErrChanged) |
| } |
| return nil |
diff --git a/internal/host/movesreport.go b/internal/host/movesreport.go| index 31cc0fdf3..1d35140c4 100644 |
| --- a/internal/host/movesreport.go |
| +++ b/internal/host/movesreport.go |
| @@ -42,25 +42,29 @@ const ( |
| t2Differs = "this box's secrets key is not the console's" |
| t3NoSource = "the source has not published a readiness report yet" // MV-N62's text |
| t3NoCompose = "this box could not compose the launch environment of the held row" |
| - t5NoArchive = "the source has published no archive yet" // MV-N73 |
| - t5Unreadable = "this box could not read the archive the source published" |
| - t5Refused = "the archive does not open with this box's key or does not match its manifest" |
| - t5NotPinned = "the source staged its archive without a fixed version id" |
| - t6Unmeasured = "this box could not measure its free disk space" |
| - t7NoRelease = "the source has not reported its release yet" // MV-N76 |
| - t7Waiting = "waiting for the root stage pass" // MV-N75 |
| - t7Failed = "this box could not stage the source's release" |
| - t7NotProven = "this box requires a proof for the source's release and has not accepted one" |
| - t7NotStaged = "the release is not staged on this box yet" // MV-N455: T7 under the claim, the version store not writable (heldReleaseStaged) |
| - t9NotThisMove = "the host claim is not held by this move" |
| - t9Incarnation = "the host claim names another incarnation of this instance" |
| - t9NoMoveJSON = "the staging lane has no move.json yet" |
| - t9OtherMove = "the staging lane holds another move's move.json" // MV-N9, aligned |
| - t10NoBucket = "the source region publishes no data bucket" |
| - t10Failed = "this box cannot write the return lane in the source's bucket" |
| - t11NoReplica = "no replica has been staged yet" |
| - t11Unmeasured = "the database size could not be measured" // MV-N69 |
| - t13NotArmed = "this box cannot land the move or roll it back yet" |
| + // t3OldReport is MV-N513: the source's env report is version 1, whose |
| + // pin cannot tell an instance's box variable from the box's |
| + // (moves.EnvReportVersion). |
| + t3OldReport = "the source reported its launch environment in an older format" |
| + t5NoArchive = "the source has published no archive yet" // MV-N73 |
| + t5Unreadable = "this box could not read the archive the source published" |
| + t5Refused = "the archive does not open with this box's key or does not match its manifest" |
| + t5NotPinned = "the source staged its archive without a fixed version id" |
| + t6Unmeasured = "this box could not measure its free disk space" |
| + t7NoRelease = "the source has not reported its release yet" // MV-N76 |
| + t7Waiting = "waiting for the root stage pass" // MV-N75 |
| + t7Failed = "this box could not stage the source's release" |
| + t7NotProven = "this box requires a proof for the source's release and has not accepted one" |
| + t7NotStaged = "the release is not staged on this box yet" // MV-N455: T7 under the claim, the version store not writable (heldReleaseStaged) |
| + t9NotThisMove = "the host claim is not held by this move" |
| + t9Incarnation = "the host claim names another incarnation of this instance" |
| + t9NoMoveJSON = "the staging lane has no move.json yet" |
| + t9OtherMove = "the staging lane holds another move's move.json" // MV-N9, aligned |
| + t10NoBucket = "the source region publishes no data bucket" |
| + t10Failed = "this box cannot write the return lane in the source's bucket" |
| + t11NoReplica = "no replica has been staged yet" |
| + t11Unmeasured = "the database size could not be measured" // MV-N69 |
| + t13NotArmed = "this box cannot land the move or roll it back yet" |
| ) |
| func t1Below(min int) string { |
| @@ -510,6 +514,11 @@ func (w *movesWorker) checkEnv(ctx context.Context, s release.Store, rec provisi |
| for _, name := range shadowed { |
| notes = append(notes, t3Shadowed(name)) |
| } |
| + // A v1 report cannot say which box variables the instance supplied, so |
| + // it is refused before any comparison (fail closed). |
| + if !moves.CurrentEnvReport(source.Env) { |
| + return held, moves.Check{Reason: t3OldReport}, notes |
| + } |
| // A move into home names the home's code, "", on the record: T3's home |
| // rule follows the record, never this box's region, so a second |
| // placementless box stays T8's failure and is not T3's pass. |
diff --git a/internal/moves/envreport.go b/internal/moves/envreport.go| index c4eec53e2..dc95fd89a 100644 |
| --- a/internal/moves/envreport.go |
| +++ b/internal/moves/envreport.go |
| @@ -123,7 +123,7 @@ func (r EnvReport) instanceSupplied(name string) bool { |
| // the target's composition of the same instance hash the same |
| // (PortableDigest). Raw values never appear in the result. |
| func ReportLaunch(key []byte, env map[string]string, instance map[string]bool) EnvReport { |
| - r := EnvReport{V: 1, Vars: make(map[string]string, len(env))} |
| + r := EnvReport{V: EnvReportVersion, Vars: make(map[string]string, len(env))} |
| for k, v := range env { |
| r.Vars[k] = macVar(key, k, v) |
| if BoxVars[k] && instance[k] { |
| @@ -156,7 +156,7 @@ func LocalDigest(key []byte, env map[string]string) string { |
| for k, v := range env { |
| vars[k] = macVar(key, k, v) |
| } |
| - return digestOf(vars, func(string) bool { return true }) |
| + return "sha256:" + digestOf(vars, func(string) bool { return true }) |
| } |
| func macVar(key []byte, name, value string) string { |
| @@ -167,11 +167,35 @@ func macVar(key []byte, name, value string) string { |
| return hex.EncodeToString(mac.Sum(nil)) |
| } |
| -// portableDigest hashes the reported variables Pinned keeps. |
| +// portableDigest hashes the reported variables Pinned keeps, in the v2 |
| +// pin's form. |
| func (r EnvReport) portableDigest() string { |
| - return digestOf(r.Vars, func(k string) bool { return Pinned(k, r.instanceSupplied(k)) }) |
| + return envPinPrefix + digestOf(r.Vars, func(k string) bool { return Pinned(k, r.instanceSupplied(k)) }) |
| } |
| +// EnvReportVersion is the env report's and the env pin's contract |
| +// (home part 2, Task 2b fix round 3). Version 2 names the instance's layer |
| +// names (Instance) and pins a box variable only when the instance supplied |
| +// it (Pinned). A version 1 report carries no layer names, so an absent |
| +// Instance cannot tell an older producer, whose box variables were never |
| +// pinned, from a newer one with none instance-set: a v2 participant |
| +// refuses a v1 report at T3 and a v1 pin at the probe (CurrentEnvReport, |
| +// CurrentEnvPin), and a v2 pin is in a form (envPinPrefix) no v1 |
| +// digest, "sha256:<hex>", can equal. This is the report's version, not |
| +// ProtocolVersion. |
| +const EnvReportVersion = 2 |
| + |
| +// envPinPrefix marks a v2 env pin. |
| +const envPinPrefix = "env2:sha256:" |
| + |
| +// CurrentEnvPin reports whether pin is a v2 env pin, the only form this |
| +// release checks a launch against. |
| +func CurrentEnvPin(pin string) bool { return strings.HasPrefix(pin, envPinPrefix) } |
| + |
| +// CurrentEnvReport reports whether r is a v2 env report: its version and |
| +// its digest's form both say so. |
| +func CurrentEnvReport(r EnvReport) bool { return r.V == EnvReportVersion && CurrentEnvPin(r.Digest) } |
| + |
| func digestOf(vars map[string]string, keep func(string) bool) string { |
| names := make([]string, 0, len(vars)) |
| for k := range vars { |
| @@ -184,7 +208,7 @@ func digestOf(vars map[string]string, keep func(string) bool) string { |
| for _, k := range names { |
| fmt.Fprintf(h, "%s=%s\n", k, vars[k]) |
| } |
| - return "sha256:" + hex.EncodeToString(h.Sum(nil)) |
| + return hex.EncodeToString(h.Sum(nil)) |
| } |
| // Class is T3's variable class by name. Every BoxOwned variable has a |
diff --git a/internal/moves/envreport_test.go b/internal/moves/envreport_test.go| index 22e3d005a..307457202 100644 |
| --- a/internal/moves/envreport_test.go |
| +++ b/internal/moves/envreport_test.go |
| @@ -334,7 +334,7 @@ func TestReportEnvIsDeterministicAndDigestCoversNamesAndValues(t *testing.T) { |
| key := []byte("0123456789abcdef0123456789abcdef") |
| a := ReportEnv(key, map[string]string{"A": "1", "B": "2"}) |
| b := ReportEnv(key, map[string]string{"B": "2", "A": "1"}) |
| - if a.Digest != b.Digest || a.V != 1 || !strings.HasPrefix(a.Digest, "sha256:") { |
| + if a.Digest != b.Digest || a.V != EnvReportVersion || !CurrentEnvReport(a) || !strings.HasPrefix(a.Digest, "env2:sha256:") { |
| t.Fatalf("%v %v", a, b) |
| } |
| // the mac binds the name: the same value under another name differs |
| @@ -383,3 +383,33 @@ func TestReportKeyFromSecrets(t *testing.T) { |
| } |
| } |
| } |
| + |
| +// TestEnvReportVersionTwo (fix round 3): a report is version 2 and its |
| +// digest is a v2 pin. A v1 report, or a v1 pin in either v1 form (main's |
| +// whole-env digest, f4f51ba6's digest without box variables), is not |
| +// current, and no v1 form of the same env equals the v2 pin. The local |
| +// digest keeps its own form. |
| +func TestEnvReportVersionTwo(t *testing.T) { |
| + key := []byte("0123456789abcdef0123456789abcdef") |
| + env := map[string]string{"A": "1", "HOSTNAME": "box"} |
| + r := ReportEnv(key, env) |
| + if r.V != 2 || !CurrentEnvReport(r) || !CurrentEnvPin(r.Digest) { |
| + t.Fatalf("not a v2 report: %+v", r) |
| + } |
| + old := r |
| + old.V = 1 |
| + if CurrentEnvReport(old) { |
| + t.Fatal("a v1 report is current") |
| + } |
| + for _, pin := range []string{"", "sha256:" + strings.TrimPrefix(r.Digest, "env2:sha256:"), LocalDigest(key, env), "report"} { |
| + if CurrentEnvPin(pin) { |
| + t.Errorf("%q is a current pin", pin) |
| + } |
| + if pin == r.Digest { |
| + t.Errorf("a v1 form %q equals the v2 pin", pin) |
| + } |
| + } |
| + if !strings.HasPrefix(LocalDigest(key, env), "sha256:") { |
| + t.Fatal("the local digest changed form") |
| + } |
| +} |
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go| index bafb1d2fb..503ef88a1 100644 |
| --- a/internal/provision/plan14_root_reads_test.go |
| +++ b/internal/provision/plan14_root_reads_test.go |
| @@ -750,7 +750,7 @@ func TestPlan14RootReadCensus(t *testing.T) { |
| if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-home-part2.tsv"), &inv, true); err != nil { |
| t.Fatal(err) |
| } |
| - seal = "b7e249599ad4f2a667887c7ceb82b5452abff8ace919608cd03ece04cd48b628" |
| + seal = "8d9a063305974175e1486b3a0b5e61ba298bbff6a637ecf77503c0ca2447015d" |
| errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...) |
| errs = append(errs, checkRootReadDeclarations(got, inv)...) |
| if len(errs) > 0 { |
diff --git a/internal/provision/testdata/plan14-root-reads-home-part2.tsv b/internal/provision/testdata/plan14-root-reads-home-part2.tsv| index c44fff582..b01fc351b 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-home-part2.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-home-part2.tsv |
| @@ -3,6 +3,7 @@ |
| # Env digests, by where they are compared. Across boxes, the move-portable env: the source pin writeSourceReport sr.Env.Digest (host/movesphase1.go:875) copied to the target report's Inputs.EnvReport (host/movesreport.go:276) and checked on the target by the probe owners' Select and Materialize (host/movesprobeowners.go envDigest) and by the committed wake (committedLaunchIdentity against pinnedLaunchIdentity, selectCommitted and InputPolicy); the cut's currentInputs (host/movescut.go), compared on the source with that pin. Same box, the whole local env: the recovery's recoveryInputs (host/movescut.go, recorded through host/movesrecoveryprobe.go:139-146 on the recovering box) checked by the recovery owners' Select and Materialize (host/movesrecoveryowners.go localEnvDigest). Same box, full composed env kept and unchanged: ClaimLaunchDigest (host/claimoperations.go:70, host/claimowners.go:400), compared only with the attempt's own box, boot and controller (host/movesprobeattempts.go:312 and :353, host/relocationattempts.go:265, host/claimlaunch_authority.go:89-96, host/claimwakestop.go:162, activator/servingretry.go:188 and :222, activator/rolelaunch_linux.go:37 and :135, activator/rolelaunch_root_linux.go:41); snapshot.Identity.Config (host/claimmoveaccess.go:191), which the moves owners never read (they set their own identity from the pins). movesReadLaunch (host/moveswakesource.go:207) discards the launch source's EnvReport, not its identity. |
| # Re-frozen units, every one at main's disposition (class, task and basis unchanged; kinds and digest refreshed). Authority rows, read set unchanged (only the source offsets moved): host/claimmoveaccess.go claimMoveLaunchSource (authority C,G,J1; it now calls LaunchLayers and returns the instance's layer names) and host/movespass.go armPhase1 (authority B2,C; its LaunchEnv hook now returns the layer names). Legacy rows: activator ExecBackend.LaunchEnvironment (now a wrapper of LaunchLayers) and the new ExecBackend.LaunchLayers (LaunchEnvironment's composition and reads, moved; LaunchEnvironment's disposition); host movesWorker.writeSourceReport (ReportLaunch with the layer names); movesProbeOwners.Select, Materialize, selectCommitted and materializeCommitted (envDigest with the layer names); movesRecoveryOwners.Select and Materialize (localEnvDigest). No new read and no root decision. Nine frozen declarations are added. |
| # ClaimLaunchDigest (host/claimoperations.go, fix round 3): envelope only, it reads nothing and is no root-read unit. It covers ClaimLaunchInputs.Instance, sorted, only when non-empty, so every launch without the instance's layer names keeps main's digest byte for byte (TestClaimLaunchDigestCoversInstanceOnlyWhenSet pins main's bytes). Its comparisons stay same-box (listed above). |
| +# Fix round 3: the env report and pin are version 2 (moves.EnvReportVersion, envPinPrefix "env2:sha256:"). T3 (checkEnv) refuses a v1 source report and the probe owners (Select, Materialize, selectCommitted, InputPolicy) refuse a v1 pin, fail closed; movesProbeOwners.Select, Materialize and selectCommitted are re-frozen again at main's dispositions, read sets unchanged. The rows below replace this overlay's earlier, unruled rows for the same units. |
| @legacy 849ce111c61c3dcedd80595348d0a26c67fb47462d497fba63d52d2c94e1d5d0 internal/activator/backend_exec.go ExecBackend.LaunchEnvironment legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route whole:Route@139 e8853e15b427b80a701b5e10b565f7d4f0b778e0c8185da63266e2408f6b4113 |
| internal/activator/backend_exec.go ExecBackend.LaunchEnvironment legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route whole:Route@139 e8853e15b427b80a701b5e10b565f7d4f0b778e0c8185da63266e2408f6b4113 |
| @legacy 1c98a7ecd93fe84486af8cbc09bd1cb31915c69c16785c3fd3bcdd6d21eb6849 internal/activator/backend_exec.go ExecBackend.LaunchLayers legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:Socket@689,whole:Route@179,whole:Route@685 a82c0f1740d9242da29248be3a009ab34a528df105e3a269b91b49e40c839261 |
| @@ -11,18 +12,18 @@ internal/host/claimmoveaccess.go claimMoveLaunchSource authority C,G,J1 root-rou |
| internal/host/movespass.go armPhase1 authority B2,C rev3=census-ruling-seeded-instances;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's body with the S1 probe hook refusing a row whose import gate is set; main's B2,C authority retained registry:ImportGate@8279,whole:Route@8266,whole:Route@8408,whole:Route@8757,whole:Route@8898 b24834b54b93a21e8ba0757fa8f61a9a5eee2ea3c8fa85d634ef4ce4aba1c85d |
| @legacy 6080eef9badd1adefd1a5c4e7a6dd3c64999b8cf58eb016adbe4e72ab8efbff2 internal/host/movesphase1.go movesWorker.writeSourceReport legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input file:Stat@753 585c0c5bb9b8c20d5a5d77dc08e49720268951ec30bbc1175db9c83900c817f2 |
| internal/host/movesphase1.go movesWorker.writeSourceReport legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input file:Stat@753 585c0c5bb9b8c20d5a5d77dc08e49720268951ec30bbc1175db9c83900c817f2 |
| -@legacy 31e120a3a0d3d9bf8367d682d37bf828ff0d38de82120df7bf90ddf65160def0 internal/host/movesprobeowners.go movesProbeOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Agent landing materialization adds C8 payload establishment and recovery delegation; generic payload rendering remains legacy. registry:DBPath@1041,whole:Route@1214,whole:Route@2099 411a6da5dd4d7da78124b299c081289c97466b4d54e63e32f7ec1635b5651541 |
| -internal/host/movesprobeowners.go movesProbeOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Agent landing materialization adds C8 payload establishment and recovery delegation; generic payload rendering remains legacy. registry:DBPath@1041,whole:Route@1214,whole:Route@2099 411a6da5dd4d7da78124b299c081289c97466b4d54e63e32f7ec1635b5651541 |
| -@legacy 1efd44545eb606c4c5b1fb04db18062c4aed2d7bc20d0b8b42e9c89cd0b6531c internal/host/movesprobeowners.go movesProbeOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1400,registry:DBPath@3028,whole:Route@206,whole:Route@2284,whole:Route@465,whole:Route@765 5a606e96b50c37070313f637d2c4d7f9453dc6f780834e6195bb975099b680fd |
| -internal/host/movesprobeowners.go movesProbeOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1400,registry:DBPath@3028,whole:Route@206,whole:Route@2284,whole:Route@465,whole:Route@765 5a606e96b50c37070313f637d2c4d7f9453dc6f780834e6195bb975099b680fd |
| +@legacy ee65a41f912901bc0645265455b564ab7bfd2dc08875dcade50bec796444bb69 internal/host/movesprobeowners.go movesProbeOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Agent landing materialization adds C8 payload establishment and recovery delegation; generic payload rendering remains legacy. registry:DBPath@1041,whole:Route@1214,whole:Route@2099 6b21920ef49548fba1bfee767e496851451918eadde1ecc85f2a264eb91d3027 |
| +internal/host/movesprobeowners.go movesProbeOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Agent landing materialization adds C8 payload establishment and recovery delegation; generic payload rendering remains legacy. registry:DBPath@1041,whole:Route@1214,whole:Route@2099 6b21920ef49548fba1bfee767e496851451918eadde1ecc85f2a264eb91d3027 |
| +@legacy d3d37e02c781782a73bc9044cff03e90440ba490259914550017a0fa0763842c internal/host/movesprobeowners.go movesProbeOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1400,registry:DBPath@3175,whole:Route@206,whole:Route@2284,whole:Route@465,whole:Route@765 3bd1707831d739695d687fb3074db3f2911bec36fbcd55efadd0f2bec59478cb |
| +internal/host/movesprobeowners.go movesProbeOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1400,registry:DBPath@3175,whole:Route@206,whole:Route@2284,whole:Route@465,whole:Route@765 3bd1707831d739695d687fb3074db3f2911bec36fbcd55efadd0f2bec59478cb |
| @legacy 45cda5b26dc120bbd9f580493ce79219ce21501f3de390c29a859dad6801c5aa internal/host/movesprobeowners.go movesProbeOwners.materializeCommitted legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Committed landing materialization establishes C8 files before start check; payload delivery remains legacy. registry:DBPath@629,whole:Route@616,whole:Route@712 f0a7780b71f42a05595eec24ddb3654d621205a403b6d18c0cba4391209b9554 |
| internal/host/movesprobeowners.go movesProbeOwners.materializeCommitted legacy accepted-legacy accepted-legacy;rev3=merge-ruling-2;basis=Committed landing materialization establishes C8 files before start check; payload delivery remains legacy. registry:DBPath@629,whole:Route@616,whole:Route@712 f0a7780b71f42a05595eec24ddb3654d621205a403b6d18c0cba4391209b9554 |
| -@legacy e6ac718f1c049c2b9e937669afb730f4152ab18cd25b571967a0b78982e5749f internal/host/movesprobeowners.go movesProbeOwners.selectCommitted legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1647,whole:Route@176,whole:Route@611 af0e6f216a63265245cc5cb1db23aedf9dd8d6731383c90de4d0e8b2b53fc401 |
| -internal/host/movesprobeowners.go movesProbeOwners.selectCommitted legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1647,whole:Route@176,whole:Route@611 af0e6f216a63265245cc5cb1db23aedf9dd8d6731383c90de4d0e8b2b53fc401 |
| +@legacy c270bb7c3c88c9fc63421868b5bccff36fe89e341723b982f7314792e821f5d6 internal/host/movesprobeowners.go movesProbeOwners.selectCommitted legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1663,whole:Route@176,whole:Route@611 31c9975dbc5307ccdaa70d7b0821b08b717fa716716fac938095d8d01f512357 |
| +internal/host/movesprobeowners.go movesProbeOwners.selectCommitted legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1663,whole:Route@176,whole:Route@611 31c9975dbc5307ccdaa70d7b0821b08b717fa716716fac938095d8d01f512357 |
| @legacy 7ba24c0aa59e9f30cdf08fdd9e7acb056ab11672ae774772df2c7bb3bbb15ac3 internal/host/movesrecoveryowners.go movesRecoveryOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@577,whole:Route@1566,whole:Route@750 a87029143a13dc05d5b873b6b4f099515be737f9e0188586fac5fe91719ccd81 |
| internal/host/movesrecoveryowners.go movesRecoveryOwners.Materialize legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@577,whole:Route@1566,whole:Route@750 a87029143a13dc05d5b873b6b4f099515be737f9e0188586fac5fe91719ccd81 |
| @legacy 98403405dd4f1aa000814d268c71477fd7b3f54fe1921270f8ad3ffc41ec7c54 internal/host/movesrecoveryowners.go movesRecoveryOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1753,registry:DBPath@792,whole:Route@1008,whole:Route@514 35686d5e12ca3008cfefe0e8a15fcfc8d1e7c120adf94619a55fa948f4554921 |
| internal/host/movesrecoveryowners.go movesRecoveryOwners.Select legacy accepted-legacy accepted-legacy;rev3=J1;basis=root-route registry:DBPath@1753,registry:DBPath@792,whole:Route@1008,whole:Route@514 35686d5e12ca3008cfefe0e8a15fcfc8d1e7c120adf94619a55fa948f4554921 |
| @coverage internal/activator 4b4b243c37f98df546ac893baaf98a80ae805705492dcd0792f532d41976766c |
| -@coverage internal/host e4e29379586d2c258da32909370f909a2134a87226968785bdfea4babe6f789e |
| -@coverage internal/moves 14b012bafa4965182c173593563004a1d872436fdfcf03d55bd0b49a0bde85d2 |
| +@coverage internal/host 3699333bae7ea249340acd261f3de74ed3969bb5fcdb4cad988a0b38a2473553 |
| +@coverage internal/moves d395a0a343cbdea42a899ed2fdb2fce7c352f5e6f3a31c2795ccd84a28eebfef |