WIP claim Plan 14 Part 3 checkpoint 3 (not for merge)
Seeds floor (unpublished): storage, legacy denies, acknowledgement, selectors; G2 non-empty historic migration with signed stop import, startup verification, predecessor-gated re-admission; reboot phase retry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
35 files changed,
+1269
−78
internal/console/seedfloor.go+2 −1internal/console/seedfloor_test.go+15 −0internal/edgesfile/snapshot.go+2 −0internal/host/claimconstruction.go+4 −0internal/host/edgesfile.go+2 −0internal/host/movesfloor.go+5 −0internal/host/plan14_root_migration_test.go+60 −0internal/host/plan9fix4_claimjournal_migration_linux_test.go+1 −0internal/host/relocationsourcehold_linux_test.go+1 −0internal/host/rootmigration_bootstrap_root_linux_test.go+70 −2internal/host/rootmigration_evidence.go+64 −0internal/host/rootmigration_historic_linux_test.go+148 −0internal/host/rootmigration_production_linux.go+25 −21internal/host/rootmigration_reboot_root_linux_test.go+150 −0internal/host/rootmigration_startup.go+127 −0internal/host/rootmigration_stopped_linux.go+46 −0internal/host/rootmovesfloor.go+5 −2internal/host/rootmovesfloor_boot.go+22 −0internal/host/rootmovesfloor_boot_root_linux_test.go+36 −3internal/host/rootmovesfloor_crash_root_linux_test.go+10 −4internal/host/rootmovesfloor_refresh.go+4 −4internal/host/rootmovesfloor_source.go+29 −6internal/host/rootseedsfloor.go+142 −0internal/host/rootseedsfloor_root_linux_test.go+84 −0internal/host/rootseedsfloor_test.go+44 −0internal/host/seedsfloor.go+8 −0internal/host/systemapply.go+15 −5internal/hostguard/guard.go+1 −1internal/hostguard/plan14_root_fences_test.go+22 −2internal/hostguard/root.go+1 −1internal/hostguard/rootmigration.go+21 −0internal/hostguard/rootmigration_test.go+45 −0internal/hostguard/rootview.go+1 −1internal/provision/testdata/plan14-root-reads-p3.tsv+56 −25internal/release/systempointer.go+1 −0
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 303.951s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.858s
ok github.com/carlosframework/platform/cmd/moveprobe 92.429s
ok github.com/carlosframework/platform/cmd/zddprobe 0.740s
ok github.com/carlosframework/platform/internal/activator 249.905s
ok github.com/carlosframework/platform/internal/agentsock 0.020s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.024s
ok github.com/carlosframework/platform/internal/appconfig 2.580s
ok github.com/carlosframework/platform/internal/apperr 0.094s
ok github.com/carlosframework/platform/internal/applog 0.153s
ok github.com/carlosframework/platform/internal/awsinv 0.005s
ok github.com/carlosframework/platform/internal/boxchoice 0.005s
ok github.com/carlosframework/platform/internal/boxid 0.930s
? 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.009s
ok github.com/carlosframework/platform/internal/caps 0.012s
ok github.com/carlosframework/platform/internal/certwarm 0.004s
ok github.com/carlosframework/platform/internal/cgroup 0.754s
ok github.com/carlosframework/platform/internal/changelog 0.324s
ok github.com/carlosframework/platform/internal/clamd 0.058s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 394.074s
? 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.857s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.010s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.003s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.394s
ok github.com/carlosframework/platform/internal/doorkey 0.007s
ok github.com/carlosframework/platform/internal/economics 0.004s
ok github.com/carlosframework/platform/internal/edge 26.857s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.008s
ok github.com/carlosframework/platform/internal/fleetapi 0.134s
ok github.com/carlosframework/platform/internal/gate 1.655s
ok github.com/carlosframework/platform/internal/genselect 0.016s
ok github.com/carlosframework/platform/internal/geo 0.018s
ok github.com/carlosframework/platform/internal/geoip 0.030s
ok github.com/carlosframework/platform/internal/historyreadback 0.153s
ok github.com/carlosframework/platform/internal/host 146.806s
ok github.com/carlosframework/platform/internal/hostactors 0.490s
ok github.com/carlosframework/platform/internal/hostbox 2.332s
ok github.com/carlosframework/platform/internal/hostclaim 0.578s
ok github.com/carlosframework/platform/internal/hostevidence 0.007s
ok github.com/carlosframework/platform/internal/hostexec 5.146s
ok github.com/carlosframework/platform/internal/hostfence 0.040s
ok github.com/carlosframework/platform/internal/hostfiles 10.483s
ok github.com/carlosframework/platform/internal/hostfloor 0.019s
ok github.com/carlosframework/platform/internal/hostguard 1.027s
ok github.com/carlosframework/platform/internal/hostops 1.622s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.002s
ok github.com/carlosframework/platform/internal/hostquarantine 0.002s
ok github.com/carlosframework/platform/internal/hostrelocate 0.494s
ok github.com/carlosframework/platform/internal/hostresolve 0.142s
ok github.com/carlosframework/platform/internal/hostwrite 0.002s
ok github.com/carlosframework/platform/internal/infraview 0.005s
ok github.com/carlosframework/platform/internal/instancectl 4.190s
ok github.com/carlosframework/platform/internal/instanceexec 2.718s
ok github.com/carlosframework/platform/internal/instancefs 0.006s
ok github.com/carlosframework/platform/internal/instancekey 0.007s
ok github.com/carlosframework/platform/internal/instanceupdates 0.006s
ok github.com/carlosframework/platform/internal/layout 1.541s
ok github.com/carlosframework/platform/internal/lease 9.810s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.008s
ok github.com/carlosframework/platform/internal/ledger 0.024s
ok github.com/carlosframework/platform/internal/mail 0.188s
ok github.com/carlosframework/platform/internal/maintenance 0.083s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.558s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.068s
ok github.com/carlosframework/platform/internal/metrics 0.007s
ok github.com/carlosframework/platform/internal/moves 21.363s
ok github.com/carlosframework/platform/internal/moves/archive 0.794s
ok github.com/carlosframework/platform/internal/objstore 0.205s
ok github.com/carlosframework/platform/internal/ops 0.186s
ok github.com/carlosframework/platform/internal/park 0.231s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 17.937s
ok github.com/carlosframework/platform/internal/publicrelease 0.006s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 5.388s
ok github.com/carlosframework/platform/internal/relay 7.244s
ok github.com/carlosframework/platform/internal/release 22.260s
ok github.com/carlosframework/platform/internal/rollout 3.920s
ok github.com/carlosframework/platform/internal/rolloutwords 0.003s
ok github.com/carlosframework/platform/internal/rootaws 0.008s
ok github.com/carlosframework/platform/internal/rootparticipation 0.007s
ok github.com/carlosframework/platform/internal/rootstate 0.027s
ok github.com/carlosframework/platform/internal/runs 0.559s
ok github.com/carlosframework/platform/internal/s3compat 0.006s
ok github.com/carlosframework/platform/internal/safefile 0.005s
ok github.com/carlosframework/platform/internal/schedule 2.500s
ok github.com/carlosframework/platform/internal/seeds 0.012s
ok github.com/carlosframework/platform/internal/sim/hostlife 10.149s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.787s
ok github.com/carlosframework/platform/internal/status 0.037s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.305s
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.007s
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.002s
ok github.com/carlosframework/platform/internal/vet 1.640s
=== 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: 47251618 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: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
minio: downloaded, digest verified
mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
mc: downloaded, 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 15:55:50 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEBF09CB14E2EE, 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/internal/console/seedfloor.go b/internal/console/seedfloor.go| index 4708e8e05..cd1977414 100644 |
| --- a/internal/console/seedfloor.go |
| +++ b/internal/console/seedfloor.go |
| @@ -77,7 +77,8 @@ func (s *Server) seedRegionReady(ctx context.Context, region string, oneBox bool |
| continue |
| } |
| prevOK := f.PrevRelease == "none" || f.PrevSeedsAllowedFor == floor.MinRelease |
| - if f.SeedsFloorMinRelease != floor.MinRelease || !prevOK { |
| + rootRefused := (f.MovesFloorState != "" || f.MovesFloorGeneration != 0 || f.SeedsFloorState != "" || f.SeedsFloorGeneration != 0) && (f.SeedsFloorState != "accepted" || f.SeedsFloorGeneration == 0) |
| + if rootRefused || f.SeedsFloorMinRelease != floor.MinRelease || !prevOK { |
| below = append(below, f.Box) |
| } |
| } |
diff --git a/internal/console/seedfloor_test.go b/internal/console/seedfloor_test.go| index fa06a4e18..11c2ac2df 100644 |
| --- a/internal/console/seedfloor_test.go |
| +++ b/internal/console/seedfloor_test.go |
| @@ -352,3 +352,18 @@ func TestSeedsFloorDoorIsForSystemOperators(t *testing.T) { |
| t.Fatalf("floor = %+v, %v", f, err) |
| } |
| } |
| + |
| +func TestPlan14SeedsFloorMixedFleet(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + publishSeedsFloorFx(t, fx, "v0.27.0") |
| + seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "nested", false) |
| + for _, extra := range []string{``, `,"moves_floor_state":"accepted"`, `,"moves_floor_state":"accepted","seeds_floor_state":"quarantined","seeds_floor_generation":1`, `,"moves_floor_state":"accepted","seeds_floor_state":"accepted","seeds_floor_generation":1`} { |
| + putSeedEdges(t, fx, "kass-2", `{"v":1,"box":"kass-2","eip":"1.2.3.4","hosts":{},"seeds_protocol":1,"seeds_floor_min_release":"v0.27.0","prev_release":"none"`+extra+`}`) |
| + putSeedHeartbeat(t, fx, "kass-2", "nested", false, time.Now()) |
| + ref := fx.srv.seedRegionReady(t.Context(), "", false) |
| + accepted := extra == "" || strings.Contains(extra, `"seeds_floor_state":"accepted"`) |
| + if (ref == nil) != accepted { |
| + t.Fatal("mixed fleet", extra, ref) |
| + } |
| + } |
| +} |
diff --git a/internal/edgesfile/snapshot.go b/internal/edgesfile/snapshot.go| index 7efff95fb..159505101 100644 |
| --- a/internal/edgesfile/snapshot.go |
| +++ b/internal/edgesfile/snapshot.go |
| @@ -78,6 +78,8 @@ type PlacementView struct { |
| PrevAllowedFor string `json:"prev_allowed_for"` |
| // The seeds floor (seeded-instances spec §3.3), read only by the |
| // console's seedRegionReady. |
| + SeedsFloorState string `json:"seeds_floor_state,omitempty"` |
| + SeedsFloorGeneration uint64 `json:"seeds_floor_generation,omitempty"` |
| SeedsProtocol int `json:"seeds_protocol"` |
| SeedsFloorMinRelease string `json:"seeds_floor_min_release"` |
| PrevSeedsAllowedFor string `json:"prev_seeds_allowed_for"` |
diff --git a/internal/host/claimconstruction.go b/internal/host/claimconstruction.go| index b347bacfa..95749026e 100644 |
| --- a/internal/host/claimconstruction.go |
| +++ b/internal/host/claimconstruction.go |
| @@ -220,6 +220,10 @@ func OpenProductionClaimAssembly(ctx context.Context, store release.Store, r Cla |
| return nil, errors.Join(refreshErr, hostwrite.ErrUnavailable) |
| } |
| actor = fresh |
| + if err := verifyRootMigrationStartup(ctx, rootFloorIdentity{Deployment: actor.ControlBucket, Box: actor.Box, Enrollment: actor.Enrollment}, journals.Inventory); err != nil { |
| + launches.Close() |
| + return nil, err |
| + } |
| stop, err = owners.claimEvidenceCoordinator(ctx, store, r.Mode, actor) |
| if err != nil { |
| launches.Close() |
diff --git a/internal/host/edgesfile.go b/internal/host/edgesfile.go| index d8689a643..4f6e753cc 100644 |
| --- a/internal/host/edgesfile.go |
| +++ b/internal/host/edgesfile.go |
| @@ -310,6 +310,8 @@ type edgesFile struct { |
| // (a build that cannot place a seed). SeedsFloorMinRelease is the |
| // armed seeds floor's min_release (absent: not armed), and |
| // PrevSeedsAllowedFor the floor carlos.prev was judged at or above. |
| + SeedsFloorState string `json:"seeds_floor_state,omitempty"` |
| + SeedsFloorGeneration uint64 `json:"seeds_floor_generation,omitempty"` |
| SeedsProtocol int `json:"seeds_protocol"` |
| SeedsFloorMinRelease string `json:"seeds_floor_min_release,omitempty"` |
| PrevSeedsAllowedFor string `json:"prev_seeds_allowed_for,omitempty"` |
diff --git a/internal/host/movesfloor.go b/internal/host/movesfloor.go| index 7c38d9b0e..ed25680bb 100644 |
| --- a/internal/host/movesfloor.go |
| +++ b/internal/host/movesfloor.go |
| @@ -458,6 +458,11 @@ func rootJudgePrev(reg *registry.Registry) error { |
| } |
| func rootJudgePrevWithSource(reg *registry.Registry, source rootFloorIdentitySource) error { |
| + if id, required, err := source.read(); err != nil { |
| + return err |
| + } else if required { |
| + return errors.Join(rootJudgeMovesPrevWithSource(reg, source), rootRefreshAndJudgeSeedsPrev(id)) |
| + } |
| return errors.Join(rootJudgeMovesPrevWithSource(reg, source), rootJudgeSeedsPrev()) |
| } |
diff --git a/internal/host/plan14_root_migration_test.go b/internal/host/plan14_root_migration_test.go| index c2045b0ed..d69f8a6f6 100644 |
| --- a/internal/host/plan14_root_migration_test.go |
| +++ b/internal/host/plan14_root_migration_test.go |
| @@ -218,3 +218,63 @@ func TestPlan14RootMigrationCensusCrashResume(t *testing.T) { |
| } |
| } |
| } |
| + |
| +func TestPlan14RootMigrationStartupVerification(t *testing.T) { |
| + m, _, _ := migrationFixture(t) |
| + root := hostguard.NewRootAuthority(&rollbackDisk{dir: t.TempDir()}) |
| + if err := m.run(t.Context()); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := verifyRootMigrationState(t.Context(), m.state, m.execution, m.identity, root); err != nil { |
| + t.Fatal("matching startup", err) |
| + } |
| + raw, err := m.state.Read("migration.json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, attack := range []string{"missing", "pending", "enrollment", "schema", "census"} { |
| + t.Run(attack, func(t *testing.T) { |
| + if err := m.state.Write("migration.json", raw); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := m.census(true); err != nil { |
| + t.Fatal(err) |
| + } |
| + var r rootMigrationRecord |
| + if json.Unmarshal(raw, &r) != nil { |
| + t.Fatal("fixture") |
| + } |
| + switch attack { |
| + case "missing": |
| + if err := m.state.Remove("migration.json"); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "pending": |
| + r.Phase = "prepared" |
| + case "enrollment": |
| + r.Identity.Enrollment = "other" |
| + case "schema": |
| + r.Schema++ |
| + case "census": |
| + if err := m.census(false); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if attack != "missing" { |
| + if err := m.write("migration.json", r); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if verifyRootMigrationState(t.Context(), m.state, m.execution, m.identity, root) == nil { |
| + t.Fatal("unsafe startup") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func fixtureWithoutRootMigrationCensus(t *testing.T) { |
| + t.Helper() |
| + original := openRootMigrationParticipation |
| + openRootMigrationParticipation = func() (rootparticipation.Ledger, bool, error) { return rootparticipation.Ledger{}, false, nil } |
| + t.Cleanup(func() { openRootMigrationParticipation = original }) |
| +} |
diff --git a/internal/host/plan9fix4_claimjournal_migration_linux_test.go b/internal/host/plan9fix4_claimjournal_migration_linux_test.go| index 55b3cee61..1b3a2832b 100644 |
| --- a/internal/host/plan9fix4_claimjournal_migration_linux_test.go |
| +++ b/internal/host/plan9fix4_claimjournal_migration_linux_test.go |
| @@ -96,6 +96,7 @@ func fix4JournalFixture(t *testing.T) (release.Store, ClaimRuntimeResources, fix |
| } |
| func TestPlan9Fix4RootAssemblyIgnoresReplacedLegacyDirectory(t *testing.T) { |
| + fixtureWithoutRootMigrationCensus(t) |
| for _, mode := range []string{"instance-control", "adopt"} { |
| for _, replacement := range []string{"missing", "renamed", "symlink", "file"} { |
| t.Run(mode+"/"+replacement, func(t *testing.T) { |
diff --git a/internal/host/relocationsourcehold_linux_test.go b/internal/host/relocationsourcehold_linux_test.go| index acd30ff27..1a127d5ce 100644 |
| --- a/internal/host/relocationsourcehold_linux_test.go |
| +++ b/internal/host/relocationsourcehold_linux_test.go |
| @@ -12,6 +12,7 @@ import ( |
| // It sets FenceClosed and HoldRow with CARLOS_REGION_HA on, and neither |
| // with it off, so reconcile there is main's. |
| func TestProductionClaimAssemblySetsTheSourceHoldOnlyWithRegionHA(t *testing.T) { |
| + fixtureWithoutRootMigrationCensus(t) |
| for _, v := range []string{"", "0", "1"} { |
| on := v == "1" |
| t.Run("CARLOS_REGION_HA="+v, func(t *testing.T) { |
diff --git a/internal/host/rootmigration_bootstrap_root_linux_test.go b/internal/host/rootmigration_bootstrap_root_linux_test.go| index 23f13c1fe..336774729 100644 |
| --- a/internal/host/rootmigration_bootstrap_root_linux_test.go |
| +++ b/internal/host/rootmigration_bootstrap_root_linux_test.go |
| @@ -15,14 +15,18 @@ import ( |
| "time" |
| "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| "github.com/carlosframework/platform/internal/hostops" |
| "github.com/carlosframework/platform/internal/moves" |
| + "github.com/carlosframework/platform/internal/registry" |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/rootparticipation" |
| "github.com/carlosframework/platform/internal/rootstate" |
| ) |
| -func TestPlan14RootMigrationBootstrapRootRunner(t *testing.T) { |
| +func TestPlan14RootMigrationBootstrapRootRunner(t *testing.T) { testRootMigrationBootstrap(t, false) } |
| +func TestPlan14RootMigrationHistoricRootRunner(t *testing.T) { testRootMigrationBootstrap(t, true) } |
| +func testRootMigrationBootstrap(t *testing.T, historic bool) { |
| if os.Getenv("PLAN14_P3_BOOTSTRAP_CHILD") == "" { |
| unit := "/usr/lib/systemd/system/carlos-edge.service" |
| for _, p := range []string{unit, "/etc/systemd/system/carlos-edge.service", "/run/systemd/system/carlos-edge.service"} { |
| @@ -55,7 +59,7 @@ func TestPlan14RootMigrationBootstrapRootRunner(t *testing.T) { |
| defer release() |
| // The existing generation census supports a controlled all-root namespace; |
| // E owns the wider running-tenant/foreign-namespace production integration. |
| - cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", os.Args[0], "-test.run=^TestPlan14RootMigrationBootstrapRootRunner$", "-test.v") |
| + cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", os.Args[0], "-test.run=^"+t.Name()+"$", "-test.v") |
| cmd.Env = append(os.Environ(), "PLAN14_P3_BOOTSTRAP_CHILD=1") |
| if out, err := cmd.CombinedOutput(); err != nil { |
| t.Fatal(err, string(out)) |
| @@ -64,6 +68,14 @@ func TestPlan14RootMigrationBootstrapRootRunner(t *testing.T) { |
| } |
| return |
| } |
| + for _, path := range []string{"/run/carlos", "/run/carlos/hostguard"} { |
| + err := os.Mkdir(path, 0755) |
| + if err == nil { |
| + t.Cleanup(func() { os.RemoveAll(path) }) |
| + } else if !os.IsExist(err) { |
| + t.Fatal(err) |
| + } |
| + } |
| rootMigrationQuiesce = func(context.Context) (func(), error) { return func() {}, nil } |
| for _, path := range []string{"/var/lib/carlos-claims", "/var/lib/carlos-release"} { |
| if err := os.Mkdir(path, 0700); err != nil { |
| @@ -100,6 +112,11 @@ func TestPlan14RootMigrationBootstrapRootRunner(t *testing.T) { |
| key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize)) |
| t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(key.Public().(ed25519.PublicKey))) |
| plan := hostops.BackfillPlan{V: 1, ID: "migration", Phase: "collecting", DirectoryRevision: 1, CreatedAt: time.Now(), Hosts: map[string]hostops.BackfillHost{}} |
| + var target hostguard.FenceTarget |
| + var historicBytes []byte |
| + if historic { |
| + target, historicBytes = migrationHistoricFixture(t, store, r, owners.ClaimOwnerBindings, actor, &plan) |
| + } |
| raw, err := release.SignRootEnvelope(release.RootSigner(key), plan) |
| if err != nil { |
| t.Fatal(err) |
| @@ -131,6 +148,26 @@ func TestPlan14RootMigrationBootstrapRootRunner(t *testing.T) { |
| oldOwner := claimQuiesceOwner |
| claimQuiesceOwner = func() (int, int, error) { return 65534, 65534, nil } |
| defer func() { claimQuiesceOwner = oldOwner }() |
| + if historic { |
| + held, err := os.Open(filepath.Join(r.DataDir, "historic-data")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + err = migrateRootClaimAuthority(t.Context(), store, r, owners.ClaimOwnerBindings, actor, service, journal) |
| + held.Close() |
| + if err == nil { |
| + t.Fatal("migration completed with an open historic payload") |
| + } |
| + if err := r.Registry.UpsertTimer(registry.Timer{Host: target.Host, Name: "unrecorded", Kind: "cron", Spec: "0 0 * * *", Path: "/extra"}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := migrateRootClaimAuthority(t.Context(), store, r, owners.ClaimOwnerBindings, actor, service, journal); err == nil { |
| + t.Fatal("unrecorded preclaim timer accepted") |
| + } |
| + if err := r.Registry.DeleteTimer(target.Host, "unrecorded"); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| if err := migrateRootClaimAuthority(t.Context(), store, r, owners.ClaimOwnerBindings, actor, service, journal); err != nil { |
| t.Fatal("production bootstrap", err) |
| } |
| @@ -158,4 +195,35 @@ func TestPlan14RootMigrationBootstrapRootRunner(t *testing.T) { |
| if err := migrateRootClaimAuthority(t.Context(), store, r, owners.ClaimOwnerBindings, actor, service, journal); err != nil { |
| t.Fatal("completed migration replay", err) |
| } |
| + if err := verifyRootMigrationStartup(t.Context(), id, service.Inventory); err != nil { |
| + t.Fatal("production startup", err) |
| + } |
| + if historic { |
| + fences, err := openClaimRootAuthority() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer fences.Close() |
| + root := hostguard.NewRootAuthority(fences) |
| + g := hostguard.Guard{Root: root, DataDir: r.DataDir, Claims: hostguard.StoreReader{Store: store}} |
| + receipt, err := g.FenceReceiptFor(t.Context(), target) |
| + if err != nil || !bytes.Equal(receipt.Bytes, historicBytes) { |
| + t.Fatal("historic bytes changed", err) |
| + } |
| + view, err := root.View(t.Context(), target.Host, hostguard.Captured{Incarnation: target.Incarnation, Stamp: target.Stamp}) |
| + if err != nil || view.Installation.Artifacts.Timers != "refresh" || len(view.MigrationDenies) != 1 { |
| + t.Fatal("historic root inventory", view, err) |
| + } |
| + if err := os.RemoveAll(filepath.Join(r.DataDir, ".hostclaims")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := migrateRootClaimAuthority(t.Context(), store, r, owners.ClaimOwnerBindings, actor, service, journal); err != nil { |
| + t.Fatal("legacy erased retry", err) |
| + } |
| + again, err := g.FenceReceiptFor(t.Context(), target) |
| + if err != nil || !bytes.Equal(again.Bytes, historicBytes) || !again.EvidenceAt.Equal(receipt.EvidenceAt) { |
| + t.Fatal("historic replay renewed evidence", err) |
| + } |
| + } |
| + |
| } |
diff --git a/internal/host/rootmigration_evidence.go b/internal/host/rootmigration_evidence.go| new file mode 100644 |
| index 000000000..b27243cc6 |
| --- /dev/null |
| +++ b/internal/host/rootmigration_evidence.go |
| @@ -0,0 +1,64 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "strings" |
| + |
| + "github.com/carlosframework/platform/internal/hostactors" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostevidence" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +func readMigrationStop(ctx context.Context, store release.Store, directory hostactors.Directory, launches *hostbox.LaunchJournal, inventory *hostbox.StopInventoryDB, launch hostbox.LaunchInventory) (StopResult, error) { |
| + target := launch.Target |
| + in, err := inventory.ReadExactStopInventory(ctx, target) |
| + if err != nil { |
| + return StopResult{}, err |
| + } |
| + journal, err := launches.ReadStopKernelJournal(ctx, target) |
| + if err != nil { |
| + return StopResult{}, err |
| + } |
| + if err := legacyJournalProof(store, directory)(ctx, launch, in, journal); err != nil { |
| + return StopResult{}, err |
| + } |
| + roster, err := directory.Read(ctx) |
| + if err != nil { |
| + return StopResult{}, err |
| + } |
| + actor, ok := roster.Roster.Boxes[target.Box] |
| + if !ok || actor.Enrollment != target.Enrollment { |
| + return StopResult{}, hostbox.ErrLegacyJournalProof |
| + } |
| + prefix := strings.TrimSuffix(stopEvidenceKey(target.Host, target.Op, target.Box, ""), ".json") |
| + keys, err := store.List(ctx, prefix) |
| + if err != nil { |
| + return StopResult{}, err |
| + } |
| + wanted := StopReceipt{Host: target.Host, Incarnation: target.Incarnation, InstallationStamp: target.Stamp, WriterSession: target.Session, Box: target.Box, Enrollment: target.Enrollment, BootID: target.BootID, ControllerID: target.ControllerID, OperationID: target.Op, Region: target.Region, DirectoryRevision: in.DirectoryRevision, InventorySHA256: digestStop(in), LocalJournalSHA256: digestStop(journal)} |
| + for _, key := range keys { |
| + if !strings.HasPrefix(key, prefix) || !strings.HasSuffix(key, ".json") { |
| + continue |
| + } |
| + raw, _, err := store.Get(ctx, key) |
| + if err != nil { |
| + return StopResult{}, err |
| + } |
| + signature, _, err := store.Get(ctx, key+".sig") |
| + if err != nil { |
| + continue |
| + } |
| + if hostevidence.Verify(actor.CensusPublicKey, key, raw, signature) != nil { |
| + continue |
| + } |
| + var p StopReceipt |
| + if json.Unmarshal(raw, &p) != nil || p.V != 1 || p.Kind != "stopped" || p.ID == "" || p.FenceID == "" || p.ClaimGen == 0 || p.ObservedBootID == "" || p.EvidenceAt.IsZero() || len(p.CgroupIdentities) == 0 || p.RequestLimitNS != int64(hostbox.RequestLimit) || key != prefix+p.ID+".json" || legacyJournalProofMatch(p) != legacyJournalProofMatch(wanted) || p.Phase != target.Phase || p.ObligationID != target.ObligationID { |
| + continue |
| + } |
| + target.FenceID, target.ClaimGen, target.ClaimETag = p.FenceID, p.ClaimGen, p.ClaimETag |
| + return StopResult{Target: target, Key: key, Digest: digestRawStop(raw), Bytes: raw, EvidenceAt: p.EvidenceAt}, nil |
| + } |
| + return StopResult{}, hostbox.ErrLegacyJournalProof |
| +} |
diff --git a/internal/host/rootmigration_historic_linux_test.go b/internal/host/rootmigration_historic_linux_test.go| new file mode 100644 |
| index 000000000..5259aabd0 |
| --- /dev/null |
| +++ b/internal/host/rootmigration_historic_linux_test.go |
| @@ -0,0 +1,148 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostevidence" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/hostops" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +func migrationHistoricFixture(t *testing.T, store release.Store, r ClaimRuntimeResources, owners ClaimOwnerBindings, actor hostbox.Actor, plan *hostops.BackfillPlan) (hostguard.FenceTarget, []byte) { |
| + t.Helper() |
| + ctx := t.Context() |
| + at := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC) |
| + rec := provision.Record{Schema: 1, Host: "historic.example", Account: "bab", App: "app", Channel: "stable", Region: "ie", CreatedAt: at} |
| + raw, _ := json.Marshal(rec) |
| + key := provision.RecordKey(rec.Account, rec.App, rec.Host) |
| + tag, err := store.Put(ctx, key, raw, "") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + plan.Hosts[rec.Host] = hostops.BackfillHost{Key: key, ETag: tag} |
| + inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, at) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + claim := hostclaim.Claim{V: 2, Host: rec.Host, Gen: 1, Holder: &hostclaim.Holder{Family: "instance", Key: key, Account: rec.Account, App: rec.App, CreatedAt: at, Incarnation: inc, Location: "instances/" + rec.Host, Generation: "generation", Stamp: 1, Session: 9, WriterBox: actor.Box, Admissions: []hostclaim.AdmissionRecord{{Region: "ie", Session: 9, Box: actor.Box, Generation: "generation", Mode: "cold", Covered: true}}}, Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: actor.Box, Participants: []string{actor.Box}}} |
| + raw, _ = json.Marshal(claim) |
| + ckey, _ := hostclaim.Key(rec.Host) |
| + ctag, err := store.Put(ctx, ckey, raw, "") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := hostclaim.Read(ctx, store, rec.Host); err != nil { |
| + t.Fatal("claim fixture", err) |
| + } |
| + target := hostguard.FenceTarget{Host: rec.Host, Incarnation: inc, Stamp: 1, Session: 9, Box: actor.Box, Enrollment: actor.Enrollment, Region: "ie", DirectoryRevision: "1", BootID: "historic-boot", ControllerID: "historic-controller", ClaimGen: 1, ClaimETag: ctag, FenceID: "historic-fence"} |
| + if err := r.Registry.UpsertTimer(registry.Timer{Host: rec.Host, Name: "refresh", Kind: "cron", Spec: "0 0 * * *", Path: "/refresh"}); err != nil { |
| + t.Fatal(err) |
| + } |
| + legacy := filepath.Join(r.DataDir, ".hostclaims") |
| + launches, err := hostbox.OpenLaunchJournal(filepath.Join(legacy, "launches.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + launch := hostbox.LaunchInventory{Target: target, ControllerPID: 999999, ProcessStart: "1", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "historic-session", Device: 1, Inode: 2}}} |
| + if err := launches.Create(ctx, launch); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := launches.CloseWriters(ctx, target); err != nil { |
| + t.Fatal(err) |
| + } |
| + journal, err := launches.ReadStopKernelJournal(ctx, target) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := launches.Close(); err != nil { |
| + t.Fatal(err) |
| + } |
| + path := filepath.Join(r.DataDir, "historic-data") |
| + if err := os.WriteFile(path, []byte("retained tenant bytes"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + var st unix.Stat_t |
| + if err := unix.Lstat(path, &st); err != nil { |
| + t.Fatal(err) |
| + } |
| + in := hostbox.StopInventory{Complete: true, Host: rec.Host, Incarnation: inc, InstallationStamp: 1, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "1", Entries: []hostbox.StopInventoryEntry{{Kind: "path", ArtifactID: "historic-data", ManagedRoot: "data", RelativePath: "historic-data", Ownership: "historic-owner", Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: st.Mode & unix.S_IFMT}, {Kind: "timer", ArtifactID: "timer", ManagedRoot: "timers", RelativePath: rec.Host + "/refresh", Ownership: "historic-owner", Materialization: "materialized"}}} |
| + inventory, err := hostbox.OpenStopInventoryDB(filepath.Join(legacy, "inventory.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := inventory.WriteStopInventory(ctx, in); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := inventory.Close(); err != nil { |
| + t.Fatal(err) |
| + } |
| + receipt := StopReceipt{V: 1, ID: "historic-stop", Kind: "stopped", Host: target.Host, Incarnation: inc, InstallationStamp: 1, WriterSession: 9, Box: actor.Box, Enrollment: actor.Enrollment, Region: "ie", BootID: target.BootID, ObservedBootID: target.BootID, ControllerID: target.ControllerID, DirectoryRevision: "1", ClaimGen: 1, ClaimETag: ctag, FenceID: target.FenceID, EvidenceAt: at, RequestLimitNS: int64(hostbox.RequestLimit), InventorySHA256: digestStop(in), LocalJournalSHA256: digestStop(journal), CgroupIdentities: []string{"historic-session"}} |
| + raw, _ = json.Marshal(receipt) |
| + if err := hostevidence.Publish(ctx, store, owners.EvidenceKey, stopEvidenceKey(target.Host, "", actor.Box, receipt.ID), raw); err != nil { |
| + t.Fatal(err) |
| + } |
| + return target, raw |
| +} |
| + |
| +func TestPlan14RootMigrationHistoricEvidence(t *testing.T) { |
| + store, r, owners, _ := fix4JournalFixture(t) |
| + actor, err := owners.Identity.Verify(t.Context()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + plan := hostops.BackfillPlan{Hosts: map[string]hostops.BackfillHost{}} |
| + target, raw := migrationHistoricFixture(t, store, r, owners.ClaimOwnerBindings, actor, &plan) |
| + j, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "root.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer j.Close() |
| + service, err := hostbox.NewJournalService(j) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := service.MigrateLegacy(t.Context(), filepath.Join(r.DataDir, ".hostclaims"), actor.Box, actor.Enrollment, func(context.Context) (func(), error) { return func() {}, nil }, legacyJournalProof(store, owners.RetirementAuthority.Directory)); err != nil { |
| + t.Fatal("signed legacy import", err) |
| + } |
| + launch, err := j.Read(t.Context(), target) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + result, err := readMigrationStop(t.Context(), store, owners.RetirementAuthority.Directory, j, service.Inventory, launch) |
| + if err != nil || string(result.Bytes) != string(raw) { |
| + t.Fatal("exact historic evidence", err) |
| + } |
| + signatureKey := result.Key + ".sig" |
| + validSignature, signatureETag, err := store.Get(t.Context(), signatureKey) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + badETag, err := store.Put(t.Context(), signatureKey, []byte("forged"), signatureETag) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := readMigrationStop(t.Context(), store, owners.RetirementAuthority.Directory, j, service.Inventory, launch); err == nil { |
| + t.Fatal("forged historic signature accepted") |
| + } |
| + if _, err := store.Put(t.Context(), signatureKey, validSignature, badETag); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := readMigrationStop(t.Context(), store, owners.RetirementAuthority.Directory, j, service.Inventory, launch); err != nil { |
| + t.Fatal("valid evidence after refusal", err) |
| + } |
| + launch.Target.Session++ |
| + if _, err := readMigrationStop(t.Context(), store, owners.RetirementAuthority.Directory, j, service.Inventory, launch); err == nil { |
| + t.Fatal("foreign session evidence") |
| + } |
| +} |
diff --git a/internal/host/rootmigration_production_linux.go b/internal/host/rootmigration_production_linux.go| index 655f48b1c..7dfe54830 100644 |
| --- a/internal/host/rootmigration_production_linux.go |
| +++ b/internal/host/rootmigration_production_linux.go |
| @@ -22,8 +22,6 @@ import ( |
| var rootMigrationQuiesce = quiesceLegacyClaimJournals |
| -const rootMigrationDir = "/var/lib/carlos-claims/authority/migration" |
| - |
| func migrateRootClaimAuthority(ctx context.Context, store release.Store, r ClaimRuntimeResources, owners ClaimOwnerBindings, actor hostbox.Actor, journals *hostbox.JournalService, launches *hostbox.LaunchJournal) error { |
| return withClaimRootRelease(ctx, true, func() (rootstate.Store, error) { return rootstate.Open(rootReleasePrivate, true, false) }, func() error { |
| state, err := rootstate.Open(rootMigrationDir, true, false) |
| @@ -76,7 +74,10 @@ func migrateRootClaimAuthority(ctx context.Context, store release.Store, r Claim |
| if err != nil { |
| return err |
| } |
| - artifacts, err := rootInventoryArtifacts(item.Installation, canonical, in, revision) |
| + prior := item.Installation |
| + // The signed complete inventory plus the offline physical census establishes this history. |
| + prior.Artifacts.Schema = 1 |
| + artifacts, err := rootInventoryArtifacts(prior, canonical, in, revision) |
| if err != nil { |
| return err |
| } |
| @@ -100,8 +101,15 @@ func migrateRootClaimAuthority(ctx context.Context, store release.Store, r Claim |
| if target.Host != in.Host || target.Incarnation != in.Incarnation || target.Stamp != in.Stamp || target.Session != item.Session { |
| continue |
| } |
| + if err := verifyMigrationStopped(ctx, launches, inventory, l, roots); err != nil { |
| + return err |
| + } |
| + result, err := readMigrationStop(ctx, store, owners.RetirementAuthority.Directory, launches, inventory, l) |
| + if err != nil { |
| + return err |
| + } |
| + target = result.Target |
| target.ClaimETag = item.ClaimETag |
| - target.FenceID = "migration-" + generationInventoryDigest(item) |
| target, err = guard.BeginFence(ctx, target) |
| if err != nil { |
| return err |
| @@ -116,10 +124,10 @@ func migrateRootClaimAuthority(ctx context.Context, store release.Store, r Claim |
| } else if !errors.Is(err, hostguard.ErrNoFenceReceipt) { |
| return err |
| } |
| - result, err := coordinator.RecoverStopped(ctx, target) |
| - if err != nil { |
| + if err := guard.CompleteFence(ctx, target, result.Bytes, result.EvidenceAt, func(context.Context, hostguard.FenceTarget, []byte, time.Time) error { return nil }); err != nil { |
| return err |
| } |
| + result.Target = target |
| if err := coordinator.Publish(ctx, result); err != nil { |
| return err |
| } |
| @@ -127,6 +135,9 @@ func migrateRootClaimAuthority(ctx context.Context, store release.Store, r Claim |
| } |
| return ErrClaimStopInventory |
| }, ready: func(ctx context.Context, s rootMigrationSnapshot) error { |
| + if err := verifyRootMigrationRetained(ctx, root, s); err != nil { |
| + return err |
| + } |
| var all []hostbox.StopInventory |
| for _, item := range s.Items { |
| in, revision, err := inventory.Snapshot(ctx, item.Installation.Host, hostguard.Captured{Incarnation: item.Installation.Incarnation, Stamp: item.Installation.Stamp}) |
| @@ -142,6 +153,9 @@ func migrateRootClaimAuthority(ctx context.Context, store release.Store, r Claim |
| if revision == 0 || retained.InventoryRevision != revision || !in.Complete { |
| return ErrClaimStopInventory |
| } |
| + if err := (ClaimRootHTTP{Reconcile: hostbox.ReconcileAdapter{Registry: r.Registry}}).verifyInventoryTimers(in); err != nil { |
| + return err |
| + } |
| all = append(all, in) |
| } |
| if err := hostbox.CheckMigrationRoots(ctx, roots, claimGlobalSharedPaths(r), all); err != nil { |
| @@ -153,25 +167,15 @@ func migrateRootClaimAuthority(ctx context.Context, store release.Store, r Claim |
| if _, _, err := productionGenerationKernelCensus(ctx); err != nil { |
| return err |
| } |
| - floors, err := openRootMovesFloors(r.OptDir, rootFloorIdentity{Deployment: id.Deployment, Box: id.Box, Enrollment: id.Enrollment}, false) |
| - if err != nil { |
| - return err |
| - } |
| - defer floors.close() |
| - p, err := floors.projection() |
| - if err != nil { |
| - return err |
| - } |
| - for _, path := range []string{systemBinaryPath(r.OptDir), filepath.Join(systemGuardDir(r.OptDir), "carlos")} { |
| - b, err := readRootFloorBinary(r.OptDir, path) |
| + floorID := rootFloorIdentity{Deployment: id.Deployment, Box: id.Box, Enrollment: id.Enrollment} |
| + for _, dir := range []string{rootFloorDir(r.OptDir), rootSeedsFloorDir(r.OptDir)} { |
| + f, err := openRootProtocolFloors(dir, floorID, true) |
| if err != nil { |
| return err |
| } |
| - if !b.supports(p.Floor) { |
| - return errRootFloorHistory |
| - } |
| + f.close() |
| } |
| - return nil |
| + return verifyRootMigrationFloors(r.OptDir, rootFloorIdentity{Deployment: id.Deployment, Box: id.Box, Enrollment: id.Enrollment}) |
| }, publish: func(ctx context.Context, report hostops.CensusReport) error { |
| return hostops.PublishCensusReport(ctx, store, owners.RetirementAuthority.Directory, report, owners.EvidenceKey) |
| }} |
diff --git a/internal/host/rootmigration_reboot_root_linux_test.go b/internal/host/rootmigration_reboot_root_linux_test.go| new file mode 100644 |
| index 000000000..d92ddd015 |
| --- /dev/null |
| +++ b/internal/host/rootmigration_reboot_root_linux_test.go |
| @@ -0,0 +1,150 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/hostops" |
| + "github.com/carlosframework/platform/internal/rootparticipation" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +const migrationRebootDir = "/var/lib/plan14-p3-migration-reboot" |
| + |
| +func rebootMigration(t *testing.T) (rootMigration, *hostguard.RootAuthority) { |
| + t.Helper() |
| + open := func(name string) rootstate.Store { |
| + s, e := rootstate.Open(filepath.Join(migrationRebootDir, name), true, false) |
| + if e != nil { |
| + t.Fatal(e) |
| + } |
| + t.Cleanup(func() { s.Close() }) |
| + return s |
| + } |
| + state, execution, fences := open("migration"), open("execution"), open("fences") |
| + root := hostguard.NewRootAuthority(fences) |
| + m := rootMigration{state: state, execution: execution, identity: rootMigrationIdentity{"deploy", "box", "en", 1}, quiesce: func(context.Context) (func(), error) { return func() {}, nil }} |
| + m.observe = func(context.Context) (rootMigrationSnapshot, error) { |
| + raw, e := state.Read("snapshot.json") |
| + var s rootMigrationSnapshot |
| + if e == nil { |
| + e = json.Unmarshal(raw, &s) |
| + } |
| + return s, e |
| + } |
| + m.deny = func(ctx context.Context, item rootMigrationItem) error { |
| + return root.RetainMigrationDeny(ctx, item.Installation, item.Session) |
| + } |
| + m.recover = func(ctx context.Context, item rootMigrationItem) error { |
| + return verifyRootMigrationRetained(ctx, root, rootMigrationSnapshot{Items: []rootMigrationItem{item}}) |
| + } |
| + m.ready = func(ctx context.Context, s rootMigrationSnapshot) error { |
| + return verifyRootMigrationRetained(ctx, root, s) |
| + } |
| + m.publish = func(context.Context, hostops.CensusReport) error { return nil } |
| + return m, root |
| +} |
| +func TestRootRebootPlan14MigrationPhase1(t *testing.T) { |
| + if err := os.Mkdir(migrationRebootDir, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + m, root := rebootMigration(t) |
| + _, s, _ := migrationFixture(t) |
| + item := rootMigrationItem{Installation: hostguard.RootInstallation{Host: "a.example", Incarnation: "inc", Stamp: 1, Account: "bab", App: "app", Record: "record", Box: "box", Enrollment: "en", Migration: 1, InventoryRevision: 1, Artifacts: hostguard.InstallationArtifacts{Schema: 1}}, Session: 9, ClaimETag: "claim"} |
| + s.Items = []rootMigrationItem{item} |
| + if err := m.write("snapshot.json", s); err != nil { |
| + t.Fatal(err) |
| + } |
| + // This phase-engine witness starts with a root-verified completed stop. The |
| + // separate historic runner exercises its signed import and physical census. |
| + if err := root.RetainMigrationDeny(t.Context(), item.Installation, item.Session); err != nil { |
| + t.Fatal(err) |
| + } |
| + target := hostguard.FenceTarget{Host: "a.example", Incarnation: "inc", Stamp: 1, Session: 9, Box: "box", Enrollment: "en", BootID: "old", ControllerID: "controller", ClaimETag: "claim", DirectoryRevision: "7", FenceID: "fence"} |
| + fences, err := rootstate.Open(filepath.Join(migrationRebootDir, "fences"), false, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer fences.Close() |
| + raw, err := fences.Read("a.example.json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var record map[string]any |
| + if json.Unmarshal(raw, &record) != nil { |
| + t.Fatal("record") |
| + } |
| + // Encode the typed completed receipt as pre-existing authority for this crash fixture. |
| + var state map[string]any |
| + encoded, _ := json.Marshal(record["State"]) |
| + if json.Unmarshal(encoded, &state) != nil { |
| + t.Fatal("state") |
| + } |
| + state["fence_journal"] = []hostguard.FenceReceipt{{Target: target, Bytes: []byte("root verified receipt"), EvidenceAt: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)}} |
| + record["State"] = state |
| + raw, _ = json.Marshal(record) |
| + if err := fences.Write("a.example.json", raw); err != nil { |
| + t.Fatal(err) |
| + } |
| + boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := m.state.Write("boot", boot); err != nil { |
| + t.Fatal(err) |
| + } |
| + cuts := 0 |
| + interrupted := errors.New("completion parent sync interrupted") |
| + m.state = rootstate.WithWriteFault(m.state, func(name, stage string, op func() error) error { |
| + if filepath.Base(name) == "migration.json" && stage == "parent-sync" { |
| + cuts++ |
| + if cuts == 4 { |
| + return interrupted |
| + } |
| + } |
| + return op() |
| + }) |
| + if err := m.run(t.Context()); !errors.Is(err, interrupted) { |
| + t.Fatal("crash boundary", cuts, err) |
| + } |
| + if _, err := (rootparticipation.Ledger{State: m.execution}).Census(); err == nil { |
| + t.Fatal("unsynced completion allowed grants") |
| + } |
| +} |
| +func TestRootRebootPlan14MigrationPhase2(t *testing.T) { |
| + if _, err := os.Stat(migrationRebootDir); err != nil { |
| + t.Fatal(err) |
| + } |
| + defer os.RemoveAll(migrationRebootDir) |
| + m, root := rebootMigration(t) |
| + before, err := m.state.Read("boot") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + now, err := os.ReadFile("/proc/sys/kernel/random/boot_id") |
| + if err != nil || strings.TrimSpace(string(before)) == strings.TrimSpace(string(now)) { |
| + t.Fatal("no real reboot", err) |
| + } |
| + if err := m.run(t.Context()); err != nil { |
| + t.Fatal("reboot resume", err) |
| + } |
| + if err := verifyRootMigrationState(t.Context(), m.state, m.execution, m.identity, root); err != nil { |
| + t.Fatal("startup verification", err) |
| + } |
| + v, err := root.View(t.Context(), "a.example", hostguard.Captured{Incarnation: "inc", Stamp: 1}) |
| + if err != nil || len(v.MigrationDenies) != 1 || v.MigrationDenies[0].Session != 9 || len(v.Receipts) != 1 || string(v.Receipts[0].Bytes) != "root verified receipt" { |
| + t.Fatal("reboot lost evidence", v, err) |
| + } |
| + if err := m.run(t.Context()); err != nil { |
| + t.Fatal("completed reboot retry", err) |
| + } |
| +} |
diff --git a/internal/host/rootmigration_startup.go b/internal/host/rootmigration_startup.go| new file mode 100644 |
| index 000000000..fce08ca96 |
| --- /dev/null |
| +++ b/internal/host/rootmigration_startup.go |
| @@ -0,0 +1,127 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rootparticipation" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +const rootMigrationDir = "/var/lib/carlos-claims/authority/migration" |
| + |
| +var openRootMigrationParticipation = rootparticipation.OpenForUpdate |
| + |
| +func verifyRootMigrationRetained(ctx context.Context, root *hostguard.RootAuthority, snapshot rootMigrationSnapshot) error { |
| + views, err := root.MigrationInstallations(ctx) |
| + if err != nil { |
| + return err |
| + } |
| + byHost := map[string]hostguard.FenceView{} |
| + for _, v := range views { |
| + byHost[v.Host] = v |
| + } |
| + for _, item := range snapshot.Items { |
| + v, ok := byHost[item.Installation.Host] |
| + if !ok { |
| + return errRootMigrationBusy |
| + } |
| + in := item.Installation |
| + denied, stopped := false, false |
| + for _, d := range v.MigrationDenies { |
| + if d.Incarnation == in.Incarnation && d.Stamp >= in.Stamp && d.Session >= item.Session { |
| + denied = true |
| + } |
| + } |
| + for _, r := range v.Receipts { |
| + if r.Target.Host == in.Host && r.Target.Incarnation == in.Incarnation && r.Target.Stamp == in.Stamp && r.Target.Session == item.Session && r.Target.Box == in.Box && r.Target.Enrollment == in.Enrollment && len(r.Bytes) > 0 && !r.EvidenceAt.IsZero() { |
| + stopped = true |
| + } |
| + } |
| + if !denied || !stopped || v.Installation.InventoryRevision == 0 || v.Installation.Artifacts.Schema != 1 { |
| + return errRootMigrationBusy |
| + } |
| + } |
| + return nil |
| +} |
| +func verifyRootMigrationState(ctx context.Context, state, execution rootstate.Store, id rootMigrationIdentity, root *hostguard.RootAuthority) error { |
| + c, err := (rootparticipation.Ledger{State: execution}).Census() |
| + if err != nil { |
| + return err |
| + } |
| + if c.Box != id.Box || c.Enrollment != id.Enrollment || c.Migration != id.Generation || !c.Complete || c.NeverEnrolled { |
| + return errRootMigrationBusy |
| + } |
| + raw, err := state.Read("migration.json") |
| + if err != nil { |
| + return err |
| + } |
| + var r rootMigrationRecord |
| + if rootparticipation.Decode(raw, &r) != nil || r.Schema != 1 || r.Identity != id || r.Phase != "complete" || r.Snapshot.preflight(id) != nil { |
| + return errRootMigrationBusy |
| + } |
| + return verifyRootMigrationRetained(ctx, root, r.Snapshot) |
| +} |
| +func verifyRootMigrationStartup(ctx context.Context, id rootFloorIdentity, inventory *hostbox.StopInventoryDB) error { |
| + l, required, err := openRootMigrationParticipation() |
| + if l.State != nil { |
| + defer l.State.Close() |
| + } |
| + if err != nil || !required { |
| + return err |
| + } |
| + c, err := l.Census() |
| + if err != nil { |
| + return err |
| + } |
| + state, err := rootstate.Open(rootMigrationDir, false, false) |
| + if err != nil { |
| + return err |
| + } |
| + defer state.Close() |
| + fences, err := openClaimRootAuthority() |
| + if err != nil { |
| + return err |
| + } |
| + defer fences.Close() |
| + root := hostguard.NewRootAuthority(fences) |
| + if err := verifyRootMigrationState(ctx, state, l.State, rootMigrationIdentity{Deployment: id.Deployment, Box: id.Box, Enrollment: id.Enrollment, Generation: c.Migration}, root); err != nil { |
| + return err |
| + } |
| + views, err := root.MigrationInstallations(ctx) |
| + if err != nil { |
| + return err |
| + } |
| + for _, v := range views { |
| + if inventory == nil { |
| + return errRootMigrationBusy |
| + } |
| + in, revision, err := inventory.Snapshot(ctx, v.Host, hostguard.Captured{Incarnation: v.Installation.Incarnation, Stamp: v.Installation.Stamp}) |
| + if err != nil || !in.Complete || revision == 0 || revision != v.Installation.InventoryRevision || v.Installation.Box != id.Box || v.Installation.Enrollment != id.Enrollment { |
| + return errRootMigrationBusy |
| + } |
| + } |
| + return verifyRootMigrationFloors(release.OptDir(), id) |
| +} |
| + |
| +func verifyRootMigrationFloors(opt string, id rootFloorIdentity) error { |
| + bound, err := readRootProtocolBound(rootFloorDir(opt), id) |
| + if err != nil { |
| + return err |
| + } |
| + for _, path := range []string{systemBinaryPath(opt), systemGuardDir(opt) + "/carlos"} { |
| + b, err := readRootFloorBinary(opt, path) |
| + if err != nil { |
| + return err |
| + } |
| + if !b.supports(bound) { |
| + return errRootFloorHistory |
| + } |
| + if err := rootSeedsAllowsBinary(opt, id, path); err != nil { |
| + return err |
| + } |
| + } |
| + return nil |
| +} |
diff --git a/internal/host/rootmigration_stopped_linux.go b/internal/host/rootmigration_stopped_linux.go| new file mode 100644 |
| index 000000000..0defc4a04 |
| --- /dev/null |
| +++ b/internal/host/rootmigration_stopped_linux.go |
| @@ -0,0 +1,46 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "os" |
| + "strings" |
| +) |
| + |
| +func verifyMigrationStopped(ctx context.Context, launches *hostbox.LaunchJournal, inventory *hostbox.StopInventoryDB, launch hostbox.LaunchInventory, roots map[string]string) error { |
| + if os.Geteuid() != 0 { |
| + return ErrClaimStopUnavailable |
| + } |
| + if err := verifyStopKernelMounts("/sys/fs/cgroup", "/proc"); err != nil { |
| + return err |
| + } |
| + journal, err := launches.ReadStopKernelJournal(ctx, launch.Target) |
| + if err != nil { |
| + return err |
| + } |
| + if !journal.WritersClosed || len(journal.ProducerIDs) != 0 || journal.PendingRoles || !journal.Settled { |
| + return ErrClaimStopInventory |
| + } |
| + raw, err := os.ReadFile("/proc/sys/kernel/random/boot_id") |
| + if err != nil { |
| + return err |
| + } |
| + boot := strings.TrimSpace(string(raw)) |
| + if boot == launch.Target.BootID { |
| + _, err = scanStopCgroups(ctx, "/sys/fs/cgroup", journal.Cgroups) |
| + } else { |
| + _, err = scanRebootCgroups(ctx, "/sys/fs/cgroup", journal.Cgroups, launch.Target.BootID, boot) |
| + } |
| + if err != nil { |
| + return err |
| + } |
| + in, err := inventory.ReadExactStopInventory(ctx, launch.Target) |
| + if err != nil { |
| + return err |
| + } |
| + paths, err := (LinuxStopKernel{ManagedRoots: roots}).stopDescriptorPaths(in) |
| + if err != nil { |
| + return err |
| + } |
| + return scanStopDescriptors(ctx, "/proc", journal.Files, paths) |
| +} |
diff --git a/internal/host/rootmovesfloor.go b/internal/host/rootmovesfloor.go| index 2e9adcf9f..993dccac7 100644 |
| --- a/internal/host/rootmovesfloor.go |
| +++ b/internal/host/rootmovesfloor.go |
| @@ -50,13 +50,16 @@ type rootMovesFloors struct { |
| func rootFloorDir(opt string) string { return filepath.Join(opt, "system", "guard", "moves") } |
| func openRootMovesFloors(opt string, id rootFloorIdentity, create bool) (rootMovesFloors, error) { |
| + return openRootProtocolFloors(rootFloorDir(opt), id, create) |
| +} |
| +func openRootProtocolFloors(dir string, id rootFloorIdentity, create bool) (rootMovesFloors, error) { |
| f := rootMovesFloors{identity: id} |
| var err error |
| - f.public, err = rootstate.Open(rootFloorDir(opt), create, true) |
| + f.public, err = rootstate.Open(dir, create, true) |
| if err != nil { |
| return f, err |
| } |
| - f.private, err = rootstate.Open(filepath.Join(rootFloorDir(opt), "private"), create, false) |
| + f.private, err = rootstate.Open(filepath.Join(dir, "private"), create, false) |
| if err != nil { |
| f.public.Close() |
| return rootMovesFloors{}, err |
diff --git a/internal/host/rootmovesfloor_boot.go b/internal/host/rootmovesfloor_boot.go| index 3bad40e3a..203709984 100644 |
| --- a/internal/host/rootmovesfloor_boot.go |
| +++ b/internal/host/rootmovesfloor_boot.go |
| @@ -68,6 +68,9 @@ func rootFloorAllowsPrev(opt string) error { |
| return err |
| } |
| defer f.close() |
| + if err := rootSeedsAllowsBinary(opt, p.Identity, systemPrevPath(opt)); err != nil { |
| + return err |
| + } |
| return rootFloorPrevVerdict(rootFloorJudge{floors: f, prev: systemPrevPath(opt), binary: func(path string) (rootFloorBinary, error) { return readRootFloorBinary(opt, path) }}) |
| } |
| @@ -84,6 +87,16 @@ func withRootFloorGuardCandidate(ctx context.Context, opt string, id rootFloorId |
| return err |
| } |
| defer unlock() |
| + seeds, err := openRootProtocolFloors(rootSeedsFloorDir(opt), id, true) |
| + if err != nil { |
| + return err |
| + } |
| + defer seeds.close() |
| + unlockSeeds, err := seeds.private.Lock(ctx, "floor.lock") |
| + if err != nil { |
| + return err |
| + } |
| + defer unlockSeeds() |
| j := rootFloorJudge{floors: f, current: systemBinaryPath(opt), prev: systemPrevPath(opt), binary: func(path string) (rootFloorBinary, error) { return readRootFloorBinary(opt, path) }} |
| candidate, err := rootFloorBootCandidate(ctx, j, journal, func(ctx context.Context, path string) error { |
| _, err := productionSysOps().run(ctx, systemVersionWait, path, "system", "floor-allows-prev", "--root", opt) |
| @@ -92,6 +105,15 @@ func withRootFloorGuardCandidate(ctx context.Context, opt string, id rootFloorId |
| if err != nil { |
| return err |
| } |
| + if err := rootSeedsAllowsBinary(opt, id, candidate); err != nil { |
| + candidate = j.current |
| + if _, err := rootFloorBootCandidate(ctx, j, false, nil, nil); err != nil { |
| + return err |
| + } |
| + if err := rootSeedsAllowsBinary(opt, id, candidate); err != nil { |
| + return err |
| + } |
| + } |
| return effect(candidate) |
| }) |
| }) |
diff --git a/internal/host/rootmovesfloor_boot_root_linux_test.go b/internal/host/rootmovesfloor_boot_root_linux_test.go| index 528352643..a47102c3c 100644 |
| --- a/internal/host/rootmovesfloor_boot_root_linux_test.go |
| +++ b/internal/host/rootmovesfloor_boot_root_linux_test.go |
| @@ -20,6 +20,7 @@ import ( |
| "github.com/carlosframework/platform/internal/moves" |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/rootstate" |
| + "github.com/carlosframework/platform/internal/seeds" |
| ) |
| func TestPlan14RootFloorBootSelectors(t *testing.T) { |
| @@ -45,7 +46,7 @@ func TestPlan14RootFloorBootSelectors(t *testing.T) { |
| t.Fatal(err) |
| } |
| for _, unit := range []string{"carlos-system-update.service", "carlos-system-recover.service"} { |
| - for _, attack := range []string{"none", "below", "stale verdict", "missing floor", "corrupt floor", "old reader", "no guard", "missing accepted", "crash-1", "crash-2", "crash-3", "crash-4", "crash-5"} { |
| + for _, attack := range []string{"none", "seeds accepted", "seeds below", "seeds legacy", "below", "stale verdict", "missing floor", "corrupt floor", "old reader", "no guard", "missing accepted", "crash-1", "crash-2", "crash-3", "crash-4", "crash-5"} { |
| t.Run(unit+"/"+attack, func(t *testing.T) { |
| opt, err := os.MkdirTemp(dir, "box-") |
| if err != nil { |
| @@ -86,6 +87,35 @@ func TestPlan14RootFloorBootSelectors(t *testing.T) { |
| if err := j.judge(t.Context()); err != nil { |
| t.Fatal(err) |
| } |
| + if strings.HasPrefix(attack, "seeds ") { |
| + sf, err := openRootProtocolFloors(rootSeedsFloorDir(opt), id, true) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer sf.close() |
| + floor := moves.Floor{Min: 1, MinRelease: "v2.0.0"} |
| + if attack != "seeds accepted" { |
| + floor.MinRelease = "v3.0.0" |
| + } |
| + if attack == "seeds legacy" { |
| + dd := filepath.Join(opt, "data") |
| + if err := os.MkdirAll(dd, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + armLocalSeedsFloor(t, dd, seeds.Floor{Min: floor.Min, MinRelease: floor.MinRelease}) |
| + if err := refreshRootSeedsFloor(t.Context(), release.NewDirStore(t.TempDir()), opt, dd, id); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.RemoveAll(dd); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := sf.projection(); err == nil { |
| + t.Fatal("legacy seeds accepted") |
| + } |
| + } else if err := sf.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &floor, Source: "root history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, sf)}); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| switch attack { |
| case "below": |
| makeBinary(systemBinaryPath(opt), "v1.0.0", true) |
| @@ -171,7 +201,7 @@ func TestPlan14RootFloorBootSelectors(t *testing.T) { |
| } |
| out, runErr := exec.CommandContext(t.Context(), "systemctl", "start", name).CombinedOutput() |
| got, readErr := os.ReadFile(mark) |
| - if attack == "none" || attack == "no guard" || attack == "missing accepted" || attack == "missing floor" || attack == "corrupt floor" || attack == "crash-1" { |
| + if attack == "none" || attack == "seeds accepted" || attack == "no guard" || attack == "missing accepted" || attack == "missing floor" || attack == "corrupt floor" || attack == "crash-1" { |
| if runErr != nil || readErr != nil || string(got) != "v2.0.0" { |
| journal, _ := exec.Command("journalctl", "-u", name, "--no-pager").CombinedOutput() |
| t.Fatalf("positive: %v %s %v %s\n%s", runErr, out, readErr, got, journal) |
| @@ -216,6 +246,9 @@ func TestPlan14RootFloorBootPayload(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| + if err := rootSeedsAllowsBinary(opt, id, path); err != nil { |
| + t.Fatal(err) |
| + } |
| if out, err := exec.CommandContext(t.Context(), path, "system", "apply").CombinedOutput(); err != nil { |
| t.Fatal(err, string(out)) |
| } |
| @@ -233,7 +266,7 @@ func rootFloorSignRunnerBytes(t *testing.T, opt, path, version string, raw []byt |
| defer s.Close() |
| sum := sha256.Sum256(raw) |
| platform := runtime.GOOS + "-" + runtime.GOARCH |
| - p := release.SystemPointer{Schema: 1, Component: "router", Channel: "stable", Spread: "0s", PromotedAt: time.Now(), Current: release.SystemRelease{Version: version, ReleaseTag: version, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{platform: {Key: release.SystemArtifactKey(version, platform), SHA256: hex.EncodeToString(sum[:])}}}} |
| + p := release.SystemPointer{Schema: 1, Component: "router", Channel: "stable", Spread: "0s", PromotedAt: time.Now(), Current: release.SystemRelease{Version: version, ReleaseTag: version, MovesProtocol: 1, SeedsProtocol: 1, Artifacts: map[string]release.SystemArtifact{platform: {Key: release.SystemArtifactKey(version, platform), SHA256: hex.EncodeToString(sum[:])}}}} |
| wire, err := release.SignRootEnvelope(release.RootSigner(key), p) |
| if err != nil { |
| t.Fatal(err) |
diff --git a/internal/host/rootmovesfloor_crash_root_linux_test.go b/internal/host/rootmovesfloor_crash_root_linux_test.go| index 32bb39ecb..318cfe55d 100644 |
| --- a/internal/host/rootmovesfloor_crash_root_linux_test.go |
| +++ b/internal/host/rootmovesfloor_crash_root_linux_test.go |
| @@ -12,13 +12,19 @@ import ( |
| ) |
| func TestPlan14RootFloorSyscallCrashOrdering(t *testing.T) { |
| + testRootProtocolFloorCrashes(t, rootFloorDir) |
| +} |
| +func TestPlan14RootSeedsFloorSyscallCrashOrdering(t *testing.T) { |
| + testRootProtocolFloorCrashes(t, rootSeedsFloorDir) |
| +} |
| +func testRootProtocolFloorCrashes(t *testing.T, floorDir func(string) string) { |
| for _, stage := range []string{"write", "chmod", "file-sync", "close", "rename", "parent-sync"} { |
| for _, after := range []bool{false, true} { |
| for cut := 1; cut <= 5; cut++ { |
| t.Run(fmt.Sprintf("%s/after=%t/write=%d", stage, after, cut), func(t *testing.T) { |
| opt := generationRootRunnerDir(t) |
| id := rootFloorIdentity{Deployment: "deploy", Box: "box", Enrollment: "en"} |
| - f, err := openRootMovesFloors(opt, id, true) |
| + f, err := openRootProtocolFloors(floorDir(opt), id, true) |
| if err != nil { |
| t.Fatal(err) |
| } |
| @@ -51,7 +57,7 @@ func TestPlan14RootFloorSyscallCrashOrdering(t *testing.T) { |
| t.Fatalf("cut not reached: %v", err) |
| } |
| f.close() |
| - f, err = openRootMovesFloors(opt, id, false) |
| + f, err = openRootProtocolFloors(floorDir(opt), id, false) |
| if err != nil { |
| t.Fatal(err) |
| } |
| @@ -66,13 +72,13 @@ func TestPlan14RootFloorSyscallCrashOrdering(t *testing.T) { |
| if bVisible && (bound == nil || *bound != b) { |
| t.Fatalf("lost visible deny: %+v", bound) |
| } |
| - if p, e := readRootFloorProjection(opt); e == nil && p.Floor != nil && *p.Floor == b { |
| + if p, e := readRootProtocolProjection(floorDir(opt)); e == nil && p.Floor != nil && *p.Floor == b { |
| t.Fatal("failed syscall acknowledged B") |
| } |
| if err := f.refresh(t.Context(), &b, nil, nil); err != nil { |
| t.Fatal("retry", err) |
| } |
| - p, err := readRootFloorProjection(opt) |
| + p, err := readRootProtocolProjection(floorDir(opt)) |
| if err != nil || p.Floor == nil || *p.Floor != b { |
| t.Fatal("retry did not durably accept B", p, err) |
| } |
diff --git a/internal/host/rootmovesfloor_refresh.go b/internal/host/rootmovesfloor_refresh.go| index 2c9444d3a..bf2044a2d 100644 |
| --- a/internal/host/rootmovesfloor_refresh.go |
| +++ b/internal/host/rootmovesfloor_refresh.go |
| @@ -19,9 +19,9 @@ import ( |
| ) |
| type rootFloorBinary struct { |
| - Version, Digest string |
| - Protocol int |
| - File rootstate.FileIdentity |
| + Version, Digest string |
| + Protocol, SeedsProtocol int |
| + File rootstate.FileIdentity |
| } |
| type rootFloorVerdict struct { |
| Identity rootFloorIdentity |
| @@ -190,7 +190,7 @@ func readRootFloorBinary(opt, path string) (rootFloorBinary, error) { |
| return b, err |
| } |
| if p.Current.Artifacts[runtime.GOOS+"-"+runtime.GOARCH].SHA256 == b.Digest { |
| - b.Version, b.Protocol = p.Current.ReleaseTag, p.Current.MovesProtocol |
| + b.Version, b.Protocol, b.SeedsProtocol = p.Current.ReleaseTag, p.Current.MovesProtocol, p.Current.SeedsProtocol |
| return b, nil |
| } |
| } |
diff --git a/internal/host/rootmovesfloor_source.go b/internal/host/rootmovesfloor_source.go| index 35fa1c865..06386d6e3 100644 |
| --- a/internal/host/rootmovesfloor_source.go |
| +++ b/internal/host/rootmovesfloor_source.go |
| @@ -48,9 +48,13 @@ func readEffectiveMovesFloor(source rootFloorIdentitySource, opt, dd string) (*m |
| if !required { |
| return readLocalFloor(dd) |
| } |
| - f, err := openRootMovesFloors(opt, id, false) |
| + return readRootProtocolBound(rootFloorDir(opt), id) |
| +} |
| + |
| +func readRootProtocolBound(dir string, id rootFloorIdentity) (*moves.Floor, error) { |
| + f, err := openRootProtocolFloors(dir, id, false) |
| if errors.Is(err, os.ErrNotExist) { |
| - public, e := rootstate.Open(rootFloorDir(opt), false, true) |
| + public, e := rootstate.Open(dir, false, true) |
| if errors.Is(e, os.ErrNotExist) { |
| return nil, nil |
| } |
| @@ -85,14 +89,27 @@ func withEffectiveMovesFloorLock(source rootFloorIdentitySource, opt, dd string, |
| return err |
| } |
| defer unlock() |
| + seeds, err := openRootProtocolFloors(rootSeedsFloorDir(opt), id, true) |
| + if err != nil { |
| + return err |
| + } |
| + defer seeds.close() |
| + unlockSeeds, err := seeds.private.Lock(context.Background(), "floor.lock") |
| + if err != nil { |
| + return err |
| + } |
| + defer unlockSeeds() |
| return fn() |
| } |
| // Read both mirrors independently. Missing files add nothing; a malformed |
| // bounded file quarantines, while any usable sibling still contributes a deny. |
| func rootFloorLegacy(dd string) ([]*moves.Floor, error) { |
| + return rootProtocolLegacy(dd, moves.FloorFile, moves.FloorMinReleaseFile) |
| +} |
| +func rootProtocolLegacy(dd, jsonFile, lineFile string) ([]*moves.Floor, error) { |
| var legacy []*moves.Floor |
| - fl, jerr := readRootLegacyFloorJSON(dd) |
| + fl, jerr := readRootLegacyProtocolJSON(dd, jsonFile) |
| if fl != nil { |
| if validRootFloor(fl) { |
| legacy = append(legacy, fl) |
| @@ -100,7 +117,7 @@ func rootFloorLegacy(dd string) ([]*moves.Floor, error) { |
| jerr = errors.Join(jerr, errRootFloorHistory) |
| } |
| } |
| - file, err := openBeneath(dd, strings.Split(filepath.ToSlash(moves.FloorMinReleaseFile), "/")...) |
| + file, err := openBeneath(dd, strings.Split(filepath.ToSlash(lineFile), "/")...) |
| if errors.Is(err, os.ErrNotExist) { |
| return legacy, jerr |
| } |
| @@ -139,7 +156,10 @@ func refreshRootMovesFloor(ctx context.Context, store release.Store, opt, dd str |
| // Sync after reading makes those already-fsynced file bytes durable before the |
| // honest agent advertises them, even if root died just after the rename. |
| func readRootFloorProjection(opt string) (rootFloorProjection, error) { |
| - s, err := rootstate.Open(rootFloorDir(opt), false, true) |
| + return readRootProtocolProjection(rootFloorDir(opt)) |
| +} |
| +func readRootProtocolProjection(dir string) (rootFloorProjection, error) { |
| + s, err := rootstate.Open(dir, false, true) |
| if err != nil { |
| return rootFloorProjection{}, err |
| } |
| @@ -164,7 +184,10 @@ func readRootFloorProjection(opt string) (rootFloorProjection, error) { |
| } |
| func readRootLegacyFloorJSON(dd string) (*moves.Floor, error) { |
| - file, err := openBeneath(dd, strings.Split(filepath.ToSlash(moves.FloorFile), "/")...) |
| + return readRootLegacyProtocolJSON(dd, moves.FloorFile) |
| +} |
| +func readRootLegacyProtocolJSON(dd, name string) (*moves.Floor, error) { |
| + file, err := openBeneath(dd, strings.Split(filepath.ToSlash(name), "/")...) |
| if errors.Is(err, os.ErrNotExist) { |
| return nil, nil |
| } |
diff --git a/internal/host/rootseedsfloor.go b/internal/host/rootseedsfloor.go| new file mode 100644 |
| index 000000000..91693c51d |
| --- /dev/null |
| +++ b/internal/host/rootseedsfloor.go |
| @@ -0,0 +1,142 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/moves" |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "github.com/carlosframework/platform/internal/seeds" |
| +) |
| + |
| +func rootSeedsFloorDir(opt string) string { return filepath.Join(opt, "system", "guard", "seeds") } |
| +func (b rootFloorBinary) forSeeds() rootFloorBinary { b.Protocol = b.SeedsProtocol; return b } |
| +func readRootSeedsBinary(opt, path string) (rootFloorBinary, error) { |
| + b, err := readRootFloorBinary(opt, path) |
| + return b.forSeeds(), err |
| +} |
| +func readEffectiveSeedsFloor(source rootFloorIdentitySource, opt, dd string) (*moves.Floor, error) { |
| + id, required, err := source.read() |
| + if err != nil { |
| + return nil, err |
| + } |
| + if required { |
| + return readRootProtocolBound(rootSeedsFloorDir(opt), id) |
| + } |
| + f, err := readLocalSeedsFloor(dd) |
| + if f == nil { |
| + return nil, err |
| + } |
| + return &moves.Floor{Min: f.Min, MinRelease: f.MinRelease}, err |
| +} |
| +func refreshRootSeedsFloor(ctx context.Context, store release.Store, opt, dd string, id rootFloorIdentity) error { |
| + f, err := openRootProtocolFloors(rootSeedsFloorDir(opt), id, true) |
| + if err != nil { |
| + return err |
| + } |
| + defer f.close() |
| + remote, rerr := seeds.ReadFloor(ctx, store) |
| + var bound *moves.Floor |
| + if remote != nil { |
| + bound = &moves.Floor{Min: remote.Min, MinRelease: remote.MinRelease} |
| + } |
| + legacy, lerr := rootProtocolLegacy(dd, seeds.FloorFile, seeds.FloorMinReleaseFile) |
| + return f.refresh(ctx, bound, legacy, errors.Join(rerr, lerr)) |
| +} |
| +func judgeRootSeedsPrev(ctx context.Context, opt string, id rootFloorIdentity) error { |
| + f, err := openRootProtocolFloors(rootSeedsFloorDir(opt), id, true) |
| + if err != nil { |
| + return err |
| + } |
| + defer f.close() |
| + return (rootFloorJudge{floors: f, current: systemBinaryPath(opt), prev: systemPrevPath(opt), binary: func(path string) (rootFloorBinary, error) { return readRootSeedsBinary(opt, path) }, idle: rootFloorDowngradeIdle}).judge(ctx) |
| +} |
| +func rootRefreshAndJudgeSeedsPrev(id rootFloorIdentity) error { |
| + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) |
| + defer cancel() |
| + opt, dd := release.OptDir(), dataDir() |
| + return withPrevLock(opt, func() error { |
| + store, err := openDeployStore(ctx) |
| + if err != nil { |
| + return err |
| + } |
| + if err := refreshRootSeedsFloor(ctx, store, opt, dd, id); err != nil { |
| + return err |
| + } |
| + return judgeRootSeedsPrev(ctx, opt, id) |
| + }) |
| +} |
| +func (a *systemApplier) seedsActivationRefusal(version string) error { |
| + floor, err := readEffectiveSeedsFloor(a.rootMoves, a.optDir, a.dataDir) |
| + if err != nil { |
| + return refuse("seeds protocol floor: the floor on this box cannot be read: %v", err) |
| + } |
| + if floor == nil { |
| + return nil |
| + } |
| + if ok, _ := seeds.ReleaseAtOrAbove(version, floor.MinRelease); !ok { |
| + return refuse("seeds protocol floor: %s is below %s", version, floor.MinRelease) |
| + } |
| + return nil |
| +} |
| +func rootSeedsAllowsBinary(opt string, id rootFloorIdentity, path string) error { |
| + floor, err := readRootProtocolBound(rootSeedsFloorDir(opt), id) |
| + if err != nil { |
| + return err |
| + } |
| + b, err := readRootSeedsBinary(opt, path) |
| + if err != nil { |
| + return err |
| + } |
| + if !b.supports(floor) { |
| + return errRootFloorHistory |
| + } |
| + return nil |
| +} |
| +func withRootSeedsFields(f edgesFile, opt string) edgesFile { |
| + f.SeedsProtocol = seeds.ProtocolVersion |
| + f.SeedsFloorState = "quarantined" |
| + f.SeedsFloorMinRelease, f.PrevSeedsAllowedFor = "", "" |
| + p, err := readRootProtocolProjection(rootSeedsFloorDir(opt)) |
| + if err != nil { |
| + return f |
| + } |
| + if p.Identity.Deployment != os.Getenv("CARLOS_DEPLOYMENT_BUCKET") || p.Identity.Box != f.Box { |
| + return f |
| + } |
| + public, err := rootstate.Open(rootSeedsFloorDir(opt), false, true) |
| + if err != nil { |
| + return f |
| + } |
| + defer public.Close() |
| + ad, err := rootFloorAdvertisementFor(public, p) |
| + if err != nil || rootstate.Sync(public) != nil { |
| + return f |
| + } |
| + f.SeedsFloorState, f.SeedsFloorGeneration = ad.State, ad.Generation |
| + f.SeedsFloorMinRelease, f.PrevSeedsAllowedFor = ad.MinRelease, ad.PrevAllowedFor |
| + return f |
| +} |
| + |
| +func (a *systemApplier) rootProtocolAllowsBinary(path string) error { |
| + id, required, err := a.rootMoves.read() |
| + if err != nil || !required { |
| + return err |
| + } |
| + b, err := readRootFloorBinary(a.optDir, path) |
| + if err != nil { |
| + return err |
| + } |
| + floor, err := readRootProtocolBound(rootFloorDir(a.optDir), id) |
| + if err != nil { |
| + return err |
| + } |
| + if !b.supports(floor) { |
| + return errRootFloorHistory |
| + } |
| + return rootSeedsAllowsBinary(a.optDir, id, path) |
| +} |
diff --git a/internal/host/rootseedsfloor_root_linux_test.go b/internal/host/rootseedsfloor_root_linux_test.go| new file mode 100644 |
| index 000000000..bad92878b |
| --- /dev/null |
| +++ b/internal/host/rootseedsfloor_root_linux_test.go |
| @@ -0,0 +1,84 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "crypto/ed25519" |
| + "encoding/base64" |
| + "github.com/carlosframework/platform/internal/moves" |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/seeds" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| +) |
| + |
| +func TestPlan14RootSeedsFloorRootRunner(t *testing.T) { |
| + for _, prev := range []bool{false, true} { |
| + t.Run(map[bool]string{false: "absent", true: "present"}[prev], func(t *testing.T) { |
| + opt := generationRootRunnerDir(t) |
| + dd := filepath.Join(opt, "data") |
| + if err := os.Mkdir(dd, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + id := rootFloorIdentity{"deploy", "box", "en"} |
| + pub, key, _ := ed25519.GenerateKey(nil) |
| + t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(pub)) |
| + t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "deploy") |
| + rootFloorSignRunnerBytes(t, opt, systemBinaryPath(opt), "v2.0.0", []byte("#!/bin/sh\nexit 0\n"), key) |
| + if prev { |
| + rootFloorSignRunnerBytes(t, opt, systemPrevPath(opt), "v2.0.0", []byte("#!/bin/sh\nexit 0 #prev\n"), key) |
| + } |
| + f, err := openRootProtocolFloors(rootSeedsFloorDir(opt), id, true) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer f.close() |
| + a := moves.Floor{Min: 1, MinRelease: "v1.0.0"} |
| + b := moves.Floor{Min: 1, MinRelease: "v2.0.0"} |
| + if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: id, Floor: &a, Source: "root history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil { |
| + t.Fatal(err) |
| + } |
| + j := rootFloorJudge{floors: f, current: systemBinaryPath(opt), prev: systemPrevPath(opt), binary: func(path string) (rootFloorBinary, error) { return readRootSeedsBinary(opt, path) }, idle: func(context.Context) error { return nil }} |
| + if err := j.judge(t.Context()); err != nil { |
| + t.Fatal(err) |
| + } |
| + store := release.NewDirStore(t.TempDir()) |
| + putSeedsFloor(t, store, seeds.Floor{Min: b.Min, MinRelease: b.MinRelease}) |
| + if _, err := syncLocalSeedsFloor(t.Context(), store, dd); err != nil { |
| + t.Fatal(err) |
| + } |
| + edge := edgesFile{Box: id.Box, MovesFloorState: "accepted"} |
| + if got := withSeedsFields(edge, dd, opt); got.SeedsFloorMinRelease != a.MinRelease { |
| + t.Fatal("mirror advertised ahead", got) |
| + } |
| + if err := refreshRootSeedsFloor(t.Context(), store, opt, dd, id); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got := withSeedsFields(edge, dd, opt); got.SeedsFloorState == "accepted" { |
| + t.Fatal("stale verdict accepted", got) |
| + } |
| + if err := j.judge(t.Context()); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got := withSeedsFields(edge, dd, opt); got.SeedsFloorState != "accepted" || got.SeedsFloorMinRelease != b.MinRelease { |
| + t.Fatal("no root acceptance", got) |
| + } |
| + if err := os.RemoveAll(dd); err != nil { |
| + t.Fatal(err) |
| + } |
| + aplier := systemApplier{optDir: opt, dataDir: dd, rootMoves: func() (rootFloorIdentity, bool, error) { return id, true, nil }} |
| + if aplier.seedsActivationRefusal("v1.0.0") == nil { |
| + t.Fatal("erased mirror permitted downgrade") |
| + } |
| + if err := aplier.seedsActivationRefusal("v2.0.0"); err != nil { |
| + t.Fatal(err) |
| + } |
| + // The distinct moves namespace cannot inherit a seeds acceptance. |
| + if _, err := readRootFloorProjection(opt); !os.IsNotExist(err) { |
| + t.Fatal("moves namespace created", err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/rootseedsfloor_test.go b/internal/host/rootseedsfloor_test.go| new file mode 100644 |
| index 000000000..8652c9b73 |
| --- /dev/null |
| +++ b/internal/host/rootseedsfloor_test.go |
| @@ -0,0 +1,44 @@ |
| +package host |
| + |
| +import ( |
| + "github.com/carlosframework/platform/internal/moves" |
| + "testing" |
| +) |
| + |
| +func TestPlan14RootSeedsFloorIndependent(t *testing.T) { |
| + for _, family := range []string{"moves", "seeds"} { |
| + t.Run(family, func(t *testing.T) { |
| + f := rootMovesFloors{private: &rollbackMemory{files: map[string][]byte{}}, public: &rollbackMemory{files: map[string][]byte{}}, identity: rootFloorIdentity{"deploy", "box", "en"}} |
| + a, b := moves.Floor{Min: 1, MinRelease: "v1.0.0"}, moves.Floor{Min: 2, MinRelease: "v2.0.0"} |
| + if err := f.reconstruct(t.Context(), rootFloorReconstruction{Identity: f.identity, Floor: &a, Source: "root history", Digest: floorTestDigest, Authorization: floorTestAuthorization(t, f)}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := f.refresh(t.Context(), &a, []*moves.Floor{&b}, nil); err == nil { |
| + t.Fatal("legacy ahead accepted") |
| + } |
| + if _, err := f.projection(); err == nil { |
| + t.Fatal("deny-only floor advertised") |
| + } |
| + bound, err := f.localBound() |
| + if err != nil || bound == nil || *bound != b { |
| + t.Fatal("lost legacy deny", bound, err) |
| + } |
| + if err := f.refresh(t.Context(), nil, nil, nil); err == nil { |
| + t.Fatal("remote deletion repaired refusal") |
| + } |
| + if err := f.refresh(t.Context(), &b, nil, nil); err != nil { |
| + t.Fatal(err) |
| + } |
| + if p, err := f.projection(); err != nil || *p.Floor != b { |
| + t.Fatal(p, err) |
| + } |
| + }) |
| + } |
| + if rootSeedsFloorDir("/opt/carlos") == rootFloorDir("/opt/carlos") { |
| + t.Fatal("floors share authority") |
| + } |
| + b := rootFloorBinary{Version: "v2.0.0", Digest: "signed", Protocol: 5, SeedsProtocol: 1} |
| + if b.forSeeds().supports(&moves.Floor{Min: 2, MinRelease: "v1.0.0"}) { |
| + t.Fatal("moves protocol authorized seeds") |
| + } |
| +} |
diff --git a/internal/host/seedsfloor.go b/internal/host/seedsfloor.go| index 4ef17e39a..97b5aa96a 100644 |
| --- a/internal/host/seedsfloor.go |
| +++ b/internal/host/seedsfloor.go |
| @@ -55,6 +55,9 @@ func readLocalSeedsFloor(dataDir string) (*seeds.Floor, error) { |
| // withSeedsFields sets the edges file's seeds fields from this box's own |
| // files, read at each write, as withMovesFields does for the moves floor. |
| func withSeedsFields(f edgesFile, dataDir, optDir string) edgesFile { |
| + if _, err := os.Lstat(rootSeedsFloorDir(optDir)); f.MovesFloorState != "" || !errors.Is(err, os.ErrNotExist) { |
| + return withRootSeedsFields(f, optDir) |
| + } |
| f.SeedsProtocol = seeds.ProtocolVersion |
| f.SeedsFloorMinRelease = "" |
| if fl, err := readLocalSeedsFloor(dataDir); err == nil && fl != nil { |
| @@ -204,6 +207,11 @@ func seedsActivationRefusal(version, dataDir string) error { |
| // floor this box could not write down defers the apply; anything else is a |
| // log line. |
| func (a *systemApplier) armSeedsFloor(ctx context.Context) error { |
| + if id, required, err := a.rootMoves.read(); err != nil { |
| + return err |
| + } else if required { |
| + return refreshRootSeedsFloor(ctx, a.store, a.optDir, a.dataDir, id) |
| + } |
| f, err := syncLocalSeedsFloor(ctx, a.store, a.dataDir) |
| if err == nil { |
| return nil |
diff --git a/internal/host/systemapply.go b/internal/host/systemapply.go| index 174e588c0..1b63597ed 100644 |
| --- a/internal/host/systemapply.go |
| +++ b/internal/host/systemapply.go |
| @@ -522,6 +522,9 @@ func (a *systemApplier) install(ctx context.Context, req systemRequest, p *relea |
| // and the holds still across the rename. |
| renamed := false |
| if err := a.activationGuard(target, running, func() error { |
| + if err := a.rootProtocolAllowsBinary(staged); err != nil { |
| + return err |
| + } |
| renamed = true |
| return os.Rename(staged, systemBinaryPath(a.optDir)) |
| }); err != nil { |
| @@ -805,7 +808,7 @@ func (a *systemApplier) notePrev(ctx context.Context, replace bool, version stri |
| } |
| defer f.close() |
| j := rootFloorJudge{floors: f, current: systemBinaryPath(a.optDir), prev: systemPrevPath(a.optDir), idle: rootFloorDowngradeIdle, binary: func(path string) (rootFloorBinary, error) { return readRootFloorBinary(a.optDir, path) }} |
| - return errors.Join(j.judge(ctx), judgeSeedsPrev(a.optDir, a.dataDir)) |
| + return errors.Join(j.judge(ctx), judgeRootSeedsPrev(ctx, a.optDir, id)) |
| } |
| return a.floorStep(errors.Join(judgePrevWith(a.optDir, a.dataDir, a.freezeHolds, func() string { return a.prevBuild(ctx) }), judgeSeedsPrev(a.optDir, a.dataDir))) |
| } |
| @@ -830,7 +833,8 @@ func (a *systemApplier) notePrev(ctx context.Context, replace bool, version stri |
| // since the floor's steps write and judge both. |
| func (a *systemApplier) floorArmed() bool { |
| f, err := readEffectiveMovesFloor(a.rootMoves, a.optDir, a.dataDir) |
| - return err != nil || f != nil || seedsFloorArmed(a.dataDir) |
| + sf, se := readEffectiveSeedsFloor(a.rootMoves, a.optDir, a.dataDir) |
| + return err != nil || f != nil || se != nil || sf != nil |
| } |
| // floorStep is the dormancy rule for the floor's own steps (moves Task 13, |
| @@ -980,7 +984,7 @@ func (a *systemApplier) activationGuard(target, running string, fn func() error) |
| locked := false |
| err := withEffectiveMovesFloorLock(a.rootMoves, a.optDir, a.dataDir, true, func() error { |
| locked = true |
| - if err := seedsActivationRefusal(target, a.dataDir); err != nil { |
| + if err := a.seedsActivationRefusal(target); err != nil { |
| return err |
| } |
| floor, err := readEffectiveMovesFloor(a.rootMoves, a.optDir, a.dataDir) |
| @@ -1061,7 +1065,7 @@ func holdRefusal(version, running string, held []registry.Route) error { |
| // guards nothing by itself; the step it precedes runs under |
| // activationGuard. |
| func (a *systemApplier) activationRule(version string, running func() string) error { |
| - if err := seedsActivationRefusal(version, a.dataDir); err != nil { |
| + if err := a.seedsActivationRefusal(version); err != nil { |
| return err |
| } |
| floor, err := readEffectiveMovesFloor(a.rootMoves, a.optDir, a.dataDir) |
| @@ -1095,7 +1099,7 @@ func (a *systemApplier) activationRule(version string, running func() string) er |
| // the gated build is already the one on disk, so only the floor can refuse |
| // it. Unarmed: allowed. |
| func (a *systemApplier) floorAllowsForward(version string) error { |
| - if err := seedsActivationRefusal(version, a.dataDir); err != nil { |
| + if err := a.seedsActivationRefusal(version); err != nil { |
| return err |
| } |
| floor, err := readEffectiveMovesFloor(a.rootMoves, a.optDir, a.dataDir) |
| @@ -1693,6 +1697,9 @@ func (a *systemApplier) restoreFiles(ctx context.Context, j *systemJournal) (boo |
| } |
| renamed := false |
| if err := a.activationGuard(prevVersion, running, func() error { |
| + if err := a.rootProtocolAllowsBinary(restore); err != nil { |
| + return err |
| + } |
| renamed = true |
| if err := os.Rename(restore, systemBinaryPath(a.optDir)); err != nil { |
| return err |
| @@ -1845,6 +1852,9 @@ func (a *systemApplier) recoverIfNeeded(ctx context.Context) (bool, error) { |
| // floor can rise while the plan is staged (fix round 2). |
| finished := false |
| err = a.activationGuard(j.Target, j.Target, func() error { |
| + if err := a.rootProtocolAllowsBinary(systemBinaryPath(a.optDir)); err != nil { |
| + return err |
| + } |
| finished = true |
| return a.finish(req, j.Target, plan) |
| }) |
diff --git a/internal/hostguard/guard.go b/internal/hostguard/guard.go| index 1893386e0..6e82db90f 100644 |
| --- a/internal/hostguard/guard.go |
| +++ b/internal/hostguard/guard.go |
| @@ -219,7 +219,7 @@ func (r record) admits(c *hostclaim.Claim, captured Captured) error { |
| if captured.Stamp != r.ActiveStamp { |
| return ErrStampMismatch |
| } |
| - if migrationDenied(r.migrationDenies, captured, c.Holder.Session) { |
| + if migrationAdmissionDenied(r.migrationDenies, r.FenceJournal, captured, c.Holder.Session) { |
| return ErrFenced |
| } |
| if r.ActiveFence != nil && r.ActiveFence.matchesSession(c, captured) { |
diff --git a/internal/hostguard/plan14_root_fences_test.go b/internal/hostguard/plan14_root_fences_test.go| index 3b4736de3..fc55fdf5f 100644 |
| --- a/internal/hostguard/plan14_root_fences_test.go |
| +++ b/internal/hostguard/plan14_root_fences_test.go |
| @@ -326,7 +326,9 @@ func TestPlan14RootInventoryCommitRetainsDenials(t *testing.T) { |
| } |
| } |
| -func TestPlan14ColdHandoverRefreshesRootInventory(t *testing.T) { |
| +func TestPlan14ColdHandoverRefreshesRootInventory(t *testing.T) { testRootColdHandover(t, false) } |
| +func TestPlan14MigrationPredecessorReadmission(t *testing.T) { testRootColdHandover(t, true) } |
| +func testRootColdHandover(t *testing.T, migration bool) { |
| f, prior, captured := fenceFixture(t) |
| f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "app" |
| store := &rootTestStore{dir: t.TempDir()} |
| @@ -340,6 +342,11 @@ func TestPlan14ColdHandoverRefreshesRootInventory(t *testing.T) { |
| if err := f.g.EstablishRoot(t.Context(), in, allow); err != nil { |
| t.Fatal(err) |
| } |
| + if migration { |
| + if err := f.g.Root.RetainMigrationDeny(t.Context(), in, prior.Session); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| if _, err := f.g.BeginFence(t.Context(), prior); err != nil { |
| t.Fatal(err) |
| } |
| @@ -358,9 +365,22 @@ func TestPlan14ColdHandoverRefreshesRootInventory(t *testing.T) { |
| if err := f.g.Activate(t.Context(), prior.Host, prior.Box, nil); err == nil { |
| t.Fatal("activation cleared cold barrier") |
| } |
| - if err := f.g.CompleteColdHandover(t.Context(), next, false, allow, func() error { return nil }); err != nil { |
| + if migration { |
| + fail := errors.New("interrupted handover") |
| + if err := f.g.CompleteColdHandover(t.Context(), next, false, allow, func() error { return fail }); !errors.Is(err, fail) { |
| + t.Fatal(err) |
| + } |
| + if err := f.g.With(t.Context(), prior.Host, Captured{Incarnation: prior.Incarnation, Stamp: 17}, allow, func() error { t.Fatal("pending successor ran"); return nil }); err == nil { |
| + t.Fatal("pending handover allowed") |
| + } |
| + } |
| + completed := 0 |
| + if err := f.g.CompleteColdHandover(t.Context(), next, false, allow, func() error { completed++; return nil }); err != nil { |
| t.Fatal(err) |
| } |
| + if err := f.g.CompleteColdHandover(t.Context(), next, true, allow, func() error { completed++; return nil }); err != nil || completed != 1 { |
| + t.Fatal("handover replay", err, completed) |
| + } |
| view, err := f.g.Root.View(t.Context(), prior.Host, Captured{Incarnation: prior.Incarnation, Stamp: 17}) |
| if err != nil || view.Installation.InventoryRevision != 2 || view.Barrier != nil { |
| t.Fatalf("successor snapshot: %+v %v", view, err) |
diff --git a/internal/hostguard/root.go b/internal/hostguard/root.go| index b6f2f5a8b..57d634dfd 100644 |
| --- a/internal/hostguard/root.go |
| +++ b/internal/hostguard/root.go |
| @@ -165,7 +165,7 @@ func (g Guard) CommitRootInventoryArtifacts(ctx context.Context, in RootInstalla |
| return ErrRecordIncarnation |
| } |
| } |
| - if migrationDenied(r.migrationDenies, want, c.Holder.Session) { |
| + if migrationAdmissionDenied(r.migrationDenies, r.FenceJournal, want, c.Holder.Session) { |
| return ErrFenced |
| } |
| if r.ActiveFence != nil && r.ActiveFence.matchesSession(c, want) { |
diff --git a/internal/hostguard/rootmigration.go b/internal/hostguard/rootmigration.go| index df74858db..8ffdaa692 100644 |
| --- a/internal/hostguard/rootmigration.go |
| +++ b/internal/hostguard/rootmigration.go |
| @@ -83,3 +83,24 @@ func (a *RootAuthority) CompleteMigrationInventory(ctx context.Context, target F |
| r.installation.Artifacts = artifacts |
| return a.write(r) |
| } |
| + |
| +func migrationAdmissionDenied(denies []MigrationDeny, receipts []FenceReceipt, c Captured, session uint64) bool { |
| + if migrationDenied(denies, c, session) { |
| + return true |
| + } |
| + for _, d := range denies { |
| + if d.Incarnation != c.Incarnation || d.Stamp != c.Stamp { |
| + continue |
| + } |
| + stopped := false |
| + for _, r := range receipts { |
| + if r.Target.Incarnation == d.Incarnation && r.Target.Stamp == d.Stamp && r.Target.Session == d.Session { |
| + stopped = true |
| + } |
| + } |
| + if !stopped { |
| + return true |
| + } |
| + } |
| + return false |
| +} |
diff --git a/internal/hostguard/rootmigration_test.go b/internal/hostguard/rootmigration_test.go| index 5c7ae54af..c63b13353 100644 |
| --- a/internal/hostguard/rootmigration_test.go |
| +++ b/internal/hostguard/rootmigration_test.go |
| @@ -3,8 +3,10 @@ |
| package hostguard |
| import ( |
| + "context" |
| "errors" |
| "testing" |
| + "time" |
| ) |
| func TestPlan14MigrationDenyCoversRootAndAgent(t *testing.T) { |
| @@ -35,6 +37,17 @@ func TestPlan14MigrationDenyCoversRootAndAgent(t *testing.T) { |
| if err := view.admit(target.Host, captured, target.Session); !errors.Is(err, ErrFenced) { |
| t.Fatal("agent view bypass", err) |
| } |
| + target, err = f.g.BeginFence(t.Context(), target) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = f.g.CompleteFence(t.Context(), target, []byte("verified predecessor"), time.Now(), func(context.Context, FenceTarget, []byte, time.Time) error { return nil }); err != nil { |
| + t.Fatal(err) |
| + } |
| + view, err = root.View(t.Context(), target.Host, captured) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| f.obs.Claim.Holder.Session++ |
| if err := f.g.With(t.Context(), target.Host, captured, allow, func() error { return nil }); err != nil { |
| t.Fatal("fresh authoritative session denied", err) |
| @@ -50,3 +63,35 @@ func TestPlan14MigrationDenyCoversRootAndAgent(t *testing.T) { |
| t.Fatal("deny regressed", again, err) |
| } |
| } |
| + |
| +func TestPlan14MigrationReadmissionRequiresCompletedPredecessor(t *testing.T) { |
| + f, target, captured := fenceFixture(t) |
| + root := NewRootAuthority(&rootTestStore{dir: t.TempDir()}) |
| + f.g.Root = root |
| + f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "app" |
| + in := RootInstallation{Host: target.Host, Incarnation: captured.Incarnation, Stamp: captured.Stamp, Account: "bab", App: "app", Record: "record", Box: target.Box, Enrollment: target.Enrollment, Migration: 1} |
| + if err := root.RetainMigrationDeny(t.Context(), in, target.Session); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.obs.Claim.Holder.Session++ |
| + if f.g.With(t.Context(), target.Host, captured, allow, func() error { return nil }) == nil { |
| + t.Fatal("session advance bypassed predecessor stop") |
| + } |
| + f.obs.Claim.Holder.Session-- |
| + target, err := f.g.BeginFence(t.Context(), target) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := f.g.CompleteFence(t.Context(), target, []byte("verified exact root stop"), time.Now(), func(context.Context, FenceTarget, []byte, time.Time) error { return nil }); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.obs.Claim.Holder.Session++ |
| + effects := 0 |
| + if err := f.g.With(t.Context(), target.Host, captured, allow, func() error { effects++; return nil }); err != nil || effects != 1 { |
| + t.Fatal("completed predecessor", err, effects) |
| + } |
| + f.obs.Claim.Holder.Session-- |
| + if f.g.With(t.Context(), target.Host, captured, allow, func() error { t.Fatal("old session ran"); return nil }) == nil { |
| + t.Fatal("deny lost") |
| + } |
| +} |
diff --git a/internal/hostguard/rootview.go b/internal/hostguard/rootview.go| index a96ac3e59..862daed6b 100644 |
| --- a/internal/hostguard/rootview.go |
| +++ b/internal/hostguard/rootview.go |
| @@ -89,7 +89,7 @@ func (v FenceView) admit(host string, c Captured, claimSession uint64) error { |
| return ErrRetiredStamp |
| } |
| } |
| - if migrationDenied(v.MigrationDenies, c, claimSession) { |
| + if migrationAdmissionDenied(v.MigrationDenies, v.Receipts, c, claimSession) { |
| return ErrFenced |
| } |
| if v.Barrier != nil { |
diff --git a/internal/provision/testdata/plan14-root-reads-p3.tsv b/internal/provision/testdata/plan14-root-reads-p3.tsv| index c9817e5b9..c6b5f86ff 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-p3.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-p3.tsv |
| @@ -1,6 +1,8 @@ |
| -# Plan 14 Part 3 run 2; all rows remain proposed for controller review. |
| +# Plan 14 Part 3 run 3; all rows remain proposed for controller review. |
| # Earlier overlays and frozen legacy fingerprints are unchanged. |
| internal/console/boxselect.go Server.selectMoveBoxes authority F,J4 rev3=PROPOSED-p3;identity=carlos console serve, console.service uid carlos-console;basis=applies accepted/generation refusal only to advertised root-floor participants; legacy selection retains main exactly d51a6a29a190dd36e896567adf4e0e0e1c8c9a08ff909cfbedd7732c2321ae78 |
| +internal/console/seedfloor.go Server.seedRegionReady authority F,J4 rev3=PROPOSED-p3;identity=carlos console serve uid carlos-console;basis=requires accepted seeds generation only from root-floor participants; mixed fleet nonparticipants retain exact legacy readiness d116ff2f72a1386a3a4120c69fea671cd3d43af264a20a1b8e98b5c61f11efb2 |
| +internal/host/claimconstruction.go OpenProductionClaimAssembly authority G,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=verifies complete retained migration and typed inventories before constructing claim authority on a census box; no census preserves prior path file:Read@4278,file:Read@9540,file:ReadFile@3822,registry:Host@9567,whole:Route@9655 9c00a440613c5fdc402ef9fd038d6e193ac88dace1a0430d3fe59557efbce90c |
| internal/host/claimjournal_migration.go RunSystemJournalMigrate authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=explicit offline root operator entry requests full G2 bootstrap after verified claim owner setup 4f204d8a97620bfb22430a94e236d6c5b14a5265b35f68e21fb2ad34903ae0f3 |
| internal/host/claimjournal_migration.go migrateClaimJournals authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=retains legacy migration helper for existing import/mask behavior; explicit operator entry requests G2 separately e21da6b220796cc799dcd6dcbfe5b566b7bb6753394c786f44ac66cfcc07c6e3 |
| internal/host/claimjournal_migration.go migrateClaimJournalsWith authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=opens verified actor and root journal then selects full offline census or unchanged legacy journal import e9b719120521b324163865a620812115a126cafc21b62ffa41282cf4e8093c34 |
| @@ -10,6 +12,7 @@ internal/host/guardtransaction.go systemApplier.guardRun authority F,J4 rev3=PRO |
| internal/host/movesfloor.go rootJudgeMovesPrev authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt root pass, root unit uid 0;basis=adopt-only wrapper delegates to root-aware moves judge; no direct input reads 0c9aa7c7df1ddd5b21798bc0158c5496dad16d400a69e8ace2dd61e8e16f0d58 |
| internal/host/movesfloor.go rootJudgeMovesPrevWithSource authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt root pass, root unit uid 0;basis=adopt refreshes root floor only for census participants; legacy registry judgement otherwise unchanged file:Stat@289,file:Stat@490 fa942c1c4b8af2d2930be427e303f3b9838c0fce33f1b3f8e63e17093b638269 |
| internal/host/movesfloor.go rootJudgePrev authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt root pass, root unit uid 0;basis=adopt combines moves and seeds verdict errors; seeds root ratchet still pending P3-R4 8b5ecf5a0f33ee58d603f308302bcc8e68678da6b1e6ee7ed03845c35747f8c6 |
| +internal/host/movesfloor.go rootJudgePrevWithSource authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=normal adopt path refreshes and judges both independent protocol floors only on census boxes 8d25e75b26a43b93f4c635b5d3a9d25bf41f60a02face307dc11034c706dd916 |
| internal/host/movesfloor.go withMovesFields authority F,J4 rev3=PROPOSED-p3;identity=carlos agent edgesFileLoop, carlos-edge.service uid carlos;basis=agent uses root-owned public projection when participating and its original writable mirror otherwise file:Lstat@238,file:Stat@697 3e615afa769bb64bcb0ffdc1ad7b85b67883a34cd6afd83ce7da74a1f4218983 |
| internal/host/movesfloorgate.go floorGateMain authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=dispatches explicit root previous gate; legacy argument shape preserved 6123e52c08365414c1135aef0fbcdbadcd68b38596cc81d8d9ace482eb555949 |
| internal/host/rootmigration.go rootMigration.census authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=writes pending or complete root participation identity and revokes failed completion writes 7d9e3a6c0ed825e22e0a9c2d1910656f612acb37a1b217c2170c1f682f309d37 |
| @@ -18,13 +21,21 @@ internal/host/rootmigration.go rootMigration.write authority G,J4 rev3=PROPOSED- |
| internal/host/rootmigration.go rootMigrationSnapshot.fingerprint authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=binds exclusion and membership across observations/restart while allowing boot metadata and independently recovered payload revision 115b07f84d5aa210cd35d70e2ba4b53ff4deb0e4636a737a5dc144bf34a9473c |
| internal/host/rootmigration.go rootMigrationSnapshot.preflight authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=refuses active moves, unfinished seeds, incomplete inventories or mismatched migration identity before effects 60675bcf7e54360a9daaddffb9458b5eab2636c3db8b9a2c87541c6e4dbbce97 |
| internal/host/rootmigration_census.go rootMigrationCollector.collect authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=signed collecting manifest and actor directory bound complete records/claims census; registry Seeds are refusal-only file:Exec@4684,file:Read@2868,file:Read@689 6cfe43917f24a6dd6c7e3eee503f84897c944ec0c251ddfc64c16884f5090438 |
| -internal/host/rootmigration_production_linux.go migrateRootClaimAuthority authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=offline root composes claim denies, authenticated legacy proofs, root inventory and kernel scan under release/mask exclusion file:Open@1538,file:Open@302,file:Open@384,file:Open@508,file:Read@2593,file:ReadFile@1340,whole:Route@2927 ee738c2f20fdc767f96fe1a90e0e28ec67ec52f93b4512f97f86ac7b56875ecb |
| +internal/host/rootmigration_evidence.go readMigrationStop authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate offline operator uid 0;basis=revalidates enrolled immutable stop signature and exact launch inventory journal tuple; preserves original bytes and evidence time file:Read@592 ee9a331a4c022d045acff9eca5003f4df79f9f872ca132b1fa37b150d16338d9 |
| +internal/host/rootmigration_production_linux.go migrateRootClaimAuthority authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate offline operator uid 0;basis=signed collecting manifest excludes admission; enrolled immutable stop plus root journal and kernel absence establish inventory; registry timers can only refuse unexplained artifacts; publishes complete Seeds census after retained evidence file:Open@1538,file:Open@302,file:Open@384,file:Open@508,file:Read@2593,file:ReadFile@1340,whole:Route@3070 9807e5916f0eac64511c2eda0202615b14bb0584f663ccec4ed5b69e19349970 |
| internal/host/rootmigration_production_linux.go migrationExecutionEmpty authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=any retained execution entry other than census/locks refuses migration; conservative settled-record limitation remains c17efaf8e7191cf0cd36d0ee462793c78094e40aef46567a446e503aee494641 |
| internal/host/rootmigration_production_linux.go publishMigrationReceipt authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=reconstructs immutable key/digest from original signed receipt bytes without changing evidence time db91dbe64b57fe10aa1635fb46938e2ccb585afb6aa138800507acac6a83c377 |
| internal/host/rootmigration_production_linux.go randomMigrationID authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=cryptographic nonce identifies newly collected stop evidence, never authorizes a stop file:Read@64 c2779c80b5628eca7d484ffba0b289c1068317b1f474c06e1ee5b39c3f769196 |
| internal/host/rootmigration_production_linux.go rootMigrationLegacySuspects authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=agent-writable hostguard filenames only identify unexplained hosts to refuse, never authorize inventory file:Readdirnames@277 6bfa73d903baa8a028abb007a7ec1da0066dbcda966e92e29b50a2b56fda10ab |
| internal/host/rootmigration_production_other.go migrateRootClaimAuthority authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=offline root composes claim denies, authenticated legacy proofs, root inventory and kernel scan under release/mask exclusion 327e0adfe387eac7bc74b87258efc9831f5a35ee9c9c9e71640d4e6b54853aae |
| -internal/host/rootmovesfloor.go openRootMovesFloors authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=opens checked public and private root-owned floor descriptors; private parent is mode 0700 file:Open@167,file:Open@269 4ceed7e158d3cfc0b9b77df47c7656aea044f95e0706ed3cb9e1df698995f42a |
| +internal/host/rootmigration_startup.go var openRootMigrationParticipation authority G,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=fixed production participation opener remains rootparticipation.OpenForUpdate; package tests substitute explicit no-census fixtures without accessing the machine store 53a17d908b433d98cdb817ab72543bd53788962625ccc9cfa0e48f829bbc1aa3 |
| +internal/host/rootmigration_startup.go verifyRootMigrationFloors authority G,F,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=checks installed and guard signed binaries against both locally retained protocol bounds without remote access 257eac6eb0de322ca5ad251f98579f81c4fcad58aa51f586edae11b9e14004c9 |
| +internal/host/rootmigration_startup.go verifyRootMigrationRetained authority G,F,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=requires each migrated installation to retain its old-session deny, exact stopped predecessor receipt and complete root inventory f2dc230494f4659e6d4bf71961d617854454bd8f713585a1cd4489414c288cb1 |
| +internal/host/rootmigration_startup.go verifyRootMigrationStartup authority G,F,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=checks root-owned migration records and typed journal revisions before issuing startup grants; absent census leaves main path intact file:Open@337 b5149f8db369dbf474612ec6f6afe55c518f337f619479bdfeb648f518d4c5af |
| +internal/host/rootmigration_startup.go verifyRootMigrationState authority G,F,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=binds complete root census and durable complete migration record to deployment box enrollment and migration generation file:Read@415 7d09ad99c73d534730d092d873ba558eb2a0d352b5f8f9a0d50b9530eba1dff2 |
| +internal/host/rootmigration_stopped_linux.go verifyMigrationStopped authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate offline operator uid 0;basis=root checks real proc/cgroup mounts, closed writer journal and absence of cgroups and held inventory descriptors; all observations can only refuse recovery file:ReadFile@588 06b82196857608ace8bfaa8ce7108caad961868b530a0d9cb152326a1aaf6819 |
| +internal/host/rootmovesfloor.go openRootMovesFloors authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=selects fixed moves namespace then delegates checked root store opening 4af0bda66779af3474e64fda54015763761d058feead1b8a2ebf0c2478a05a3d |
| +internal/host/rootmovesfloor.go openRootProtocolFloors authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=checked root ancestry and leaf ownership for separate moves or seeds public/private stores file:Open@170,file:Open@258 a3b589e05e8cdc1062162562006ef7f93cc3159f2f24a10a155c038ce7e3ca28 |
| internal/host/rootmovesfloor.go rootMovesFloors.localBound authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=retains every readable independent deny; missing or corrupt history adds no invented refusal or acceptance file:Read@583 cc70feddffa2f611e9de3250d42b84a506f3d5ee688921c551125c4df60ac43a |
| internal/host/rootmovesfloor.go rootMovesFloors.persist authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=writes quarantine before selector and deny ratchet, then accepted state, then synced public acknowledgement 2c1e0843c5865a31c0fab011d7d4825963bbb5a301727f37f3b1b6c3519eba5e |
| internal/host/rootmovesfloor.go rootMovesFloors.projection authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=matches public acceptance to private generation, identity and retained componentwise maximum file:Read@109 6b0ed0053db247385ccbabfd95a738fd48a8d27b14882b5e73b4e5a82eda26b6 |
| @@ -34,13 +45,13 @@ internal/host/rootmovesfloor.go rootMovesFloors.refresh authority F,J4 rev3=PROP |
| internal/host/rootmovesfloor.go rootMovesFloors.selector authority F,J4 rev3=PROPOSED-p3;identity=root apply/recover/adopt units uid 0 and agent edges publisher carlos-edge.service uid carlos;basis=strict bounded canonical protocol/release line supplies independent crash-surviving deny file:Read@73 5c46438590323610c67dc5bbfaa191fc5ba23e3b280b6063dbd48e48e7ce3927 |
| internal/host/rootmovesfloor_authorization.go rootMovesFloors.authorizeReconstruction authority F,J4 rev3=PROPOSED-p3;identity=offline root reconstruction capability, uid 0; no production caller installed;basis=issues private nonce capability for this root identity and next generation under stable lock; no production caller file:Read@426 a5235f6aab5714cee3e3e41604eed5b1eff7aa4355f9444f44c4811f7dfbbc08 |
| internal/host/rootmovesfloor_authorization.go rootMovesFloors.authorizedReconstruction authority F,J4 rev3=PROPOSED-p3;identity=offline root reconstruction capability, uid 0; no production caller installed;basis=requires exact private persisted nonce, box identity and next generation; caller strings cannot authorize file:Read@249 c449d7087a6cbcdf4388b7945e84045a0b895349b1bc14347aca7040c822391c |
| -internal/host/rootmovesfloor_boot.go rootFloorAllowsPrev authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=old-reader-resistant root gate requires current generation verdict and signed previous executable 8fb85b6abbc251b4456d6ad6cce02331048804870e6b64a56e12f8fda0107d0d |
| +internal/host/rootmovesfloor_boot.go rootFloorAllowsPrev authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=signed prev identity must satisfy both local protocol floors and root downgrade completeness; sidecars never grant ffe7eca89c202165e6ed990667016aec67ced60519655dfd0db1921b61db8411 |
| internal/host/rootmovesfloor_boot.go rootFloorBootCandidate authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=local-only selection requires previous verdict plus idle proof; fallback current must be signed and support all readable denies 386a486b3235ad5152e009281500dc0f54e00283d96ea44f9d5a28eacd3f6758 |
| internal/host/rootmovesfloor_boot.go rootFloorJournalPresent authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=checked authority-file presence selects recovery without opening a deployment store b189d04dc579f4825f5668efd1f93f178acca738bb2ed886a14f3b5321d7755b |
| internal/host/rootmovesfloor_boot.go rootFloorPrevVerdict authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=matches actual signed previous binary to identity and accepted generation before allowing its gate file:Read@131 04a9909b6ff4c81380c1d974ce028ae525ff53513fb71f78b0e1076d35391967 |
| -internal/host/rootmovesfloor_boot.go withRootFloorGuardCandidate authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=holds release, previous-binary and private-floor exclusion around local signed selection and exec file:Open@218 2426c860c29e05c74bfb9d1beaab396688bad4fab4f1f1d278026099bea44337 |
| +internal/host/rootmovesfloor_boot.go withRootFloorGuardCandidate authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=holds moves then seeds locks across selector callback; checks signed candidate against both bounds and allows independently checked current fallback file:Open@218 3d268112ee87024d73e1930a3483d92e12016b69c66fc36b04ed06adfee87000 |
| internal/host/rootmovesfloor_identity.go rememberRootFloorSystemIdentity authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=re-fetches and verifies signed pointer equality before retaining its executable identity file:Open@543 b4e56a0637c28e5fb65d3a33b7dfd1fea8bed55acbf5231bf26e0011168141fe |
| -internal/host/rootmovesfloor_refresh.go readRootFloorBinary authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=checked root executable inode/digest must match independently verified signed identity envelope file:Open@684,file:Read@941,file:Stat@276 47062453004252de69e807a67ac94e40243d69b3a55033dacf2d8354a5d635a6 |
| +internal/host/rootmovesfloor_refresh.go readRootFloorBinary authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=verifies checked executable and signed system manifest; reads signed moves and seeds protocol numbers rather than version sidecar file:Open@684,file:Read@941,file:Stat@276 2a23dd9c46c6fa56476ecfa4061712f05c6f1a1a098295e14f395def2d24324a |
| internal/host/rootmovesfloor_refresh.go rootFloorAdvertisementFor authority F,J4 rev3=PROPOSED-p3;identity=carlos agent edgesFileLoop, carlos-edge.service uid carlos;basis=agent reports accepted only for matching generation with signed compatible current and previous verdict file:Read@203 4e464b6add24a06b1c3d7a5859be7a1055750bc25f27c8ca641241be3baa2ef8 |
| internal/host/rootmovesfloor_refresh.go rootFloorBinary.supports authority F,J4 rev3=PROPOSED-p3;identity=root apply/recover/adopt units uid 0 and agent edges publisher carlos-edge.service uid carlos;basis=signed digest/release are required; protocol minimum applies only when a retained floor exists, preserving unknown-history permission 7a6eaf5432f2668aa47f24d9c667355a985f83eadb8c4619a704168288b16fbf |
| internal/host/rootmovesfloor_refresh.go rootFloorDowngradeIdle authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=root participation ledger and staged-generation pins may only refuse downgrade file:Open@68 8bb82e0b179e0dfadd6f375299d9f40ceea1c2b911b1d53f18fb4958957c74ac |
| @@ -48,38 +59,57 @@ internal/host/rootmovesfloor_refresh.go rootFloorJudge.judge authority F,J4 rev3 |
| internal/host/rootmovesfloor_refresh.go rootFloorJudge.refresh authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=refreshes retained floor before signed current/previous judgement, even when previous is absent 362798a60b4e3d6fc798db52bfc204979f724efe33a858f3d7ca2c1ca12f6eaa |
| internal/host/rootmovesfloor_refresh.go rootRefreshAndJudgeMovesPrev authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt root pass, root unit uid 0;basis=adopt opens deployment store then refreshes floor before any no-prev return 2d4d1ce1d6143555f3b94b19359c014ffb20272f41f756f7c02bbf6ff29a70a9 |
| internal/host/rootmovesfloor_refresh.go withRootMovesFields authority F,J4 rev3=PROPOSED-p3;identity=carlos agent edgesFileLoop, carlos-edge.service uid carlos;basis=agent publishes only checked root public verdict and explicitly quarantines unreadable participant state file:Open@405 29fb652e22df888f18c691b91db6b83b9781d89d005a2fae431d8102e77b2f88 |
| -internal/host/rootmovesfloor_source.go readEffectiveMovesFloor authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=root enforcement uses all readable retained denies; no census uses original local mirror file:Open@321 287a0c1120786ed693b2b138c40082fc9eb5f5104065ec317b32ac1e7473f7ae |
| -internal/host/rootmovesfloor_source.go readRootFloorProjection authority F,J4 rev3=PROPOSED-p3;identity=root apply/recover/adopt units uid 0 and agent edges publisher carlos-edge.service uid carlos;basis=checked root public read plus directory fsync precedes honest acknowledgement file:Open@83,file:Read@243 170e020e2f05437de39145566fdd4055cc11bd668fe24a1ca2458ece49195aeb |
| -internal/host/rootmovesfloor_source.go readRootLegacyFloorJSON authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=agent-writable legacy JSON is bounded strict deny-only evidence; never permission file:ReadAll@443,file:Stat@280 1af52c869107011648314ac12a9687ce641329b5a92fa7d487c278fa082bf4f2 |
| +internal/host/rootmovesfloor_source.go readEffectiveMovesFloor authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=census chooses local root protocol bound; no census chooses existing legacy reader a6420f9074a459fa5107cbd60ca18828f0dec132eb53add849152db2a9b0f62a |
| +internal/host/rootmovesfloor_source.go readRootFloorProjection authority F,J4 rev3=PROPOSED-p3;identity=carlos agent in carlos-edge.service uid carlos;basis=selects moves public root projection for checked advertisement reader 572e2d0d6a783e823c3c76ca08c38fd86abc5265486c246bb1b123d834fea485 |
| +internal/host/rootmovesfloor_source.go readRootLegacyFloorJSON authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=selects existing moves JSON for bounded denial-only protocol reader 41475b13c77395562264e21faad32b142b053fc199919c39e1eeba28f0f17155 |
| +internal/host/rootmovesfloor_source.go readRootLegacyProtocolJSON authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=bounded descriptor read of optional legacy moves or seeds mirror; shape validation and caller denial-only merge prevent acceptance file:ReadAll@441,file:Stat@278 835f913509edbe65d72986bb23e7af580aaf2d4cc222e87064e29d0d16e61103 |
| +internal/host/rootmovesfloor_source.go readRootProtocolBound authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=reads local root accepted and retained deny state; public-only deny survives missing private history; missing unknown history is not acceptance file:Open@187 2e35ae8bdee1433877d928f22b4a3315dfd2027378a71124ca361284323c22b2 |
| +internal/host/rootmovesfloor_source.go readRootProtocolProjection authority F,J4 rev3=PROPOSED-p3;identity=carlos agent in carlos-edge.service uid carlos;basis=checked public root projection must agree with selector and directory sync before advertising acceptance file:Open@86,file:Read@232 36f45f87d3cabebaaf759e2cd2b3c4397a5247247e329a0fd71de291ad1a2bf0 |
| internal/host/rootmovesfloor_source.go refreshRootMovesFloor authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=normal adopt/update combines remote object with independent deny-only legacy evidence under root floor lock a41df2c69e7d869b5232d545440b8135e8c7124d490965e641b8d1fba83cb490 |
| -internal/host/rootmovesfloor_source.go rootFloorLegacy authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=agent-writable mirrors contribute only componentwise denies; malformed sibling cannot erase usable evidence file:ReadAll@710,file:Stat@518 37e37f7b57fb768eea5cf750fb1a88a1a43b4239180878f5479a77f276dce424 |
| +internal/host/rootmovesfloor_source.go rootFloorLegacy authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=selects existing moves mirror paths for denial-only generic legacy reader e5dacb9fa26be5f5f961e99909624f38643f93c43432ff3dce1cff552f7e486a |
| internal/host/rootmovesfloor_source.go rootMovesFloorIdentity authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=root-only census reader supplies deployment/box/enrollment; edge mode cannot call it 457cdc76e2cff8fdfe4d146c7baf086dd58d54f151ce9416c32efd0a03778d98 |
| -internal/host/rootmovesfloor_source.go withEffectiveMovesFloorLock authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=root census path holds private stable floor lock; no-census path uses unchanged legacy exclusion 9c40f6835a408e2c3e7ad9251a424ae1a03013fe630296cf1a751271da39283b |
| +internal/host/rootmovesfloor_source.go rootProtocolLegacy authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=bounded agent-writable JSON and line mirrors contribute only increasing denies; neither can establish accepted state file:ReadAll@729,file:Stat@537 0b62a121d683312c34a5c7af486d60d496482ef25502e8e2ef0ef18b5e7c6fd6 |
| +internal/host/rootmovesfloor_source.go withEffectiveMovesFloorLock authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=locks moves then seeds private root stores across final activation; no census uses unchanged legacy lock 752817cc4fc1110aa055324bedb2b9e2b7da7f87445b48c8290ebb53ea3d85d7 |
| +internal/host/rootseedsfloor.go judgeRootSeedsPrev authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=evaluates signed current and prev seeds support under shared durable floor judge and independent downgrade completeness 81b14db048a628e945ba2917e2974a0fcf6d7d81ffc536b5eed4dd0bd046302d |
| +internal/host/rootseedsfloor.go readEffectiveSeedsFloor authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=uses local root seeds bound only for census participants; no census retains existing mirror read 40bcb02d390a203e39e8565439f6e583151053b9a50fd0aa6ed313aa0bbfe71c |
| +internal/host/rootseedsfloor.go readRootSeedsBinary authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=delegates checked signed executable read then selects seeds protocol 8b34c01e9a76ddd185202ea9c8542ed0a1c1aca196e26e129507fa03e9610a89 |
| +internal/host/rootseedsfloor.go refreshRootSeedsFloor authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=merges authoritative store floor with denial-only legacy seeds maxima into durable independent root state 8886e3c2017edb222014f10db5e68d9fe14146004cdd193eabaf1600f1f0edec |
| +internal/host/rootseedsfloor.go rootFloorBinary.forSeeds authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=uses independently signed SeedsProtocol as the protocol compared against seeds bounds b84949e21b9955da9d37594e604f5a4340b5f2bb073866d77feb5b901d3a7de8 |
| +internal/host/rootseedsfloor.go rootRefreshAndJudgeSeedsPrev authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=normal adopt/update refreshes root seeds state under prev exclusion; recovery never calls this remote path d65b77431479fa0989ed6f7a486406f8ba176b6035868e4738b1e14e29ba9782 |
| +internal/host/rootseedsfloor.go rootSeedsAllowsBinary authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=checks signed executable seeds protocol and release against locally retained root bound without network 2a8c7e3c837bbda6ce8073477a7c3c5a217c33a56abb51370cb160ae22ac9658 |
| +internal/host/rootseedsfloor.go rootSeedsFloorDir authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=selects fixed seeds namespace separate from moves retained floor a4971110cfd11c46b8fec4f21e36101bce537014b52a5cd60acce87204fc8f26 |
| +internal/host/rootseedsfloor.go systemApplier.rootProtocolAllowsBinary authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=final installation restore or forward-completion checks signed image against both local root bounds while activation locks are held dac3212012c9ad50e8f2cf4165657c151c561e965c72a3aa0e2f98d9794af353 |
| +internal/host/rootseedsfloor.go systemApplier.seedsActivationRefusal authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=refuses activation below selected local seeds release bound; missing unknown history grants no acceptance projection b45b7ebcbc1f00fb8bb2bdb78b9ddf94af7452426dd794ad42c988f4c921b5a1 |
| +internal/host/rootseedsfloor.go withRootSeedsFields authority F,J4 rev3=PROPOSED-p3;identity=carlos agent in carlos-edge.service uid carlos;basis=advertises only checked synced root seeds projection matching deployment and box; no valid projection stays quarantined file:Open@413 f658ea5051cc904de7154205732a3ec764d24bd230298b8c9bcae0ab4b5b7e47 |
| +internal/host/seedsfloor.go systemApplier.armSeedsFloor authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=census participants refresh root seeds history; nonparticipants retain existing legacy sync ace92bbe72e79e998daeebfe5fc2380ce6174cc0241b60a26e0093bea614bce1 |
| +internal/host/seedsfloor.go withSeedsFields authority F,J4 rev3=PROPOSED-p3;identity=carlos agent in carlos-edge.service uid carlos;basis=root participation selects checked root projection; legacy agent mirror remains unchanged for nonparticipants file:Lstat@84 57febc09559f80119ba36ca13188febf0ceb27bcdc29d829403798aba1cb6f1d |
| internal/host/systemapply.go RunSystemApply authority D,F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=root system apply takes D3 release exclusion only for census participation before normal apply file:Open@1265 22440d8ad127c66958b9c4d06244db4ca1aec1b9dfd60fad8f5bceb24f789b25 |
| -internal/host/systemapply.go systemApplier.activationGuard authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=rechecks local root floor and root downgrade-idle proof before effects; registry holds add refusal only whole:Route@1083 49988cef27c704d1c937176f1bcad1d22a229beb18a8ef2a2ef949cd39fae9e6 |
| -internal/host/systemapply.go systemApplier.activationRule authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=provisional version/registry refusal check; every effect caller must subsequently pass activationGuard whole:Route@1019,whole:Route@810,whole:Route@817 bbe33714cf851396d775273545cece1aeed399f275bdaf303e126dc724ee0961 |
| +internal/host/systemapply.go systemApplier.activationGuard authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=final activation holds root moves and seeds locks and checks each local bound before effect; main route holds retained on legacy path whole:Route@1074 7a9f97134fcacad2d3aae258dbc40d82c5e19bc36ff2ac75dab4fd55f44e4463 |
| +internal/host/systemapply.go systemApplier.activationRule authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=preliminary activation refuses against both local protocol bounds before existing hold checks whole:Route@1010,whole:Route@801,whole:Route@808 42bfa3076ade44a0b3cf72287eb7fb866bcd51e55e79cd8d933ef17fc24c3ff3 |
| internal/host/systemapply.go systemApplier.armFloor authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=normal update refreshes root floor in census mode; unknown history remains unacknowledged; seeds path pending root conversion d7e16eaaa3994896bf9acd763777cc4d8998056dfa2d492c5320395102009a16 |
| -internal/host/systemapply.go systemApplier.floorAllowsForward authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=forward candidate checked against census local root floor or main legacy floor fc96e9d74e0b9b32778ac4a86363e4095a08d0b11c45e6c1321b6da9082d0f66 |
| -internal/host/systemapply.go systemApplier.floorArmed authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=root retained moves denies determine arming; legacy seeds arming still pending P3-R4 conversion 561ff8cfef8a66cc72ed1baef8729f2b1f513d4fae44acd7cac9b23648b6485c |
| +internal/host/systemapply.go systemApplier.floorAllowsForward authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=network-free forward recovery checks both locally retained protocol release minima ce69fced288baccce9482897f14b1f221da75f8184ed1008f1ad629dbda64bb1 |
| +internal/host/systemapply.go systemApplier.floorArmed authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=dormancy decision includes unreadable or present moves and seeds floors from selected authority 13a51640e1a6855a4c5b96bee413c73a68f362591e889ed9d3730ac41151456b |
| +internal/host/systemapply.go systemApplier.install authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=checks signed staged image against both protocol minima inside final locked activation before binary rename ba2335ffabe52f7c89fdf7100c606b5c8d30ff7c6b60fac61b455f24d084865b |
| internal/host/systemapply.go systemApplier.layoutReversalReasons authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=root participation and retained floor contribute refusal reasons before layout reversal ee4e5ee2961e86b6d2e700601d57bb605c3c945515ffc678d35919d70dc2b0af |
| -internal/host/systemapply.go systemApplier.notePrev authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=root participants judge signed previous binary against retained state; legacy mode unchanged d55d08f057fb6ea467e293b4e7f0ca3e2e639d26147cab2d1e823530404ff2db |
| -internal/host/systemapply.go systemApplier.recoverIfNeeded authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=recovery keeps journal until local signed restore/forward checks finish; no remote floor refresh ea7db9e194e0beed0bba492f08a96ab6a46f2f9dc1e35d8e30abe9d0733d664c |
| -internal/host/systemapply.go systemApplier.restoreFiles authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=local signed previous identity supplies rollback version before copy/rename; never reads remote store file:Stat@226 85c25d0ff4f3e83f967f55afc39ed1f58e050285fc03eb56d70fc8fab0f9caff |
| +internal/host/systemapply.go systemApplier.notePrev authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=writes prev then judges both signed protocol identities under root floor authority on census boxes 2642cc413f5eb6a3f5661eff6452798c296c1199037f4699d5fff0fa00ca6335 |
| +internal/host/systemapply.go systemApplier.recoverIfNeeded authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=network-free recovery uses local root floors and rechecks signed image at final forward-completion boundary c15e47720b641beee468a0d2a52d2a8e2b7546f47b3ee720d1bc315067854c41 |
| +internal/host/systemapply.go systemApplier.restoreFiles authority F,J4 rev3=PROPOSED-p3;identity=carlos adopt/system apply uid 0; system recovery and guard selectors uid 0;basis=selects signed prev identity and checks staged restore against both root bounds before rename; version sidecar cannot authorize file:Stat@226 024ce4b2a5e65b76f2a6506d8b0022f99bca73959d942300ed21d89bf517fd3f |
| internal/host/systemapply.go systemApplier.restoreVersion authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=census rollback and recovery completion use signed previous executable identity; legacy sidecar remains only without census 57cf91fc81568e5b1ce05428ed928242c825c6847bb193333137845c7190d6c4 |
| internal/host/systemapply.go systemApplier.verifyPointer authority F,J4 rev3=PROPOSED-p3;identity=carlos system apply/recover and root adopt, systemd root units or offline CLI, uid 0;basis=signed pointer verified and independently cached for census target execution before earlier history exists 50961a77b1c9b4e915302c6b95cd57d9f0bbc238e7bd00b94f06a5960ab541c1 |
| internal/host/units/carlos-system-recover.service external authority F,J4 rev3=PROPOSED-p3;identity=systemd update/recover oneshot shell uid 0;basis=guard branch is conditional on executable presence; unarmed units retain main selector; installed guard selects from root local floor file:external@0 f16b260868c4b258c804b3a4720e17f02f6c020eda9dcc2a67d2ca032a077b45 |
| internal/host/units/carlos-system-update.service external authority F,J4 rev3=PROPOSED-p3;identity=systemd update/recover oneshot shell uid 0;basis=guard branch is conditional on executable presence; unarmed units retain main selector; installed guard selects from root local floor file:external@0 cfcaeea6308021ce5a7cb088464e8df3abc6b03ffaa0c7a0153c6d91bf1e3fd6 |
| internal/hostbox/migration_census_linux.go CheckMigrationRoots authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=offline scan requires every managed payload to match independent retained inode/type inventory or explicit shared path rules file:Lstat@1678,file:Lstat@932,file:WalkDir@1202 3eb5e56f13b0932364b68c7d80ad18135b262614b105c41bfcc3477d6f558e79 |
| internal/hostbox/migration_census_other.go CheckMigrationRoots authority G,J4 rev3=PROPOSED-p3;identity=carlos system journal-migrate, offline operator CLI, uid 0;basis=offline scan requires every managed payload to match independent retained inode/type inventory or explicit shared path rules 594ecda260fcf80baf5a2709c9cc5a04e91c655c8bd2f7ac307eef44216eb9c8 |
| -internal/hostguard/guard.go record.admits authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=existing lease checks additionally refuse migration-retired sessions 3e0a40e659744b48ed8aa3f3f6cd367d45098bbcbcffba4c6876d62e34856f3f |
| -internal/hostguard/root.go Guard.CommitRootInventoryArtifacts authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=retained migration deny refuses inventory commit for an old session before authority transfer edd70bbf6c152e17d73a39fb7cd0d6ebf7382ae2bf1f80160f3ddc2c4c827937 |
| +internal/hostguard/guard.go record.admits authority G,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=root admission retains migration denial until the exact predecessor session has a completed retained receipt 5864d1fa79a66ca6f69057416079b46524ed4bfdf1ce1de88124b5a24a428418 |
| +internal/hostguard/root.go Guard.CommitRootInventoryArtifacts authority G,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=root inventory commit admits a newer migration writer only after its exact predecessor completed; old sessions stay denied 08e42f8810e2645a875b5177fc9e104ed4b307c7d80dc77d204e2516b1bd66de |
| internal/hostguard/root.go RootAuthority.read authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=strict checked root record decoding includes retained migration session denials file:Read@175 7ff862f573a05a17951546a56ff791d42962d693331a69e8338bd8eb4f283d6b |
| internal/hostguard/root.go RootAuthority.write authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=retains migration denials atomically with the existing root installation, fence and retired-state record 678156528bb5d3e92f8c07835ff7a7d05afb5d498ff604966d2b6d9293867c0c |
| internal/hostguard/rootmigration.go RootAuthority.CompleteMigrationInventory authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=root completed fence receipt and retained session deny required before installing recovered inventory revision a548470f72eb9f5f9a457c292e06a303f4a7dca2068f10eda5fcbb4e118ab783 |
| internal/hostguard/rootmigration.go RootAuthority.RetainMigrationDeny authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=root retains old incarnation/stamp/session denial under stable per-host lock; grants no launch authority fb821d19b6ca6e1454d025013d27ef8a856a684262421d3ee76804723e875c37 |
| +internal/hostguard/rootmigration.go migrationAdmissionDenied authority G,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=old migrated sessions remain denied; a newer same-installation session requires completed receipt for every retained migration predecessor c7312f5e0e67b2b69bbea1b8f432dd48ba06f0b4a26ebf349b6724769651631e |
| internal/hostguard/rootmigration.go migrationDenied authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=old sessions at/below retained captured stamp refuse root and agent admission 6ccf5c00e33d880aa50ea15d2bce22a3cce14455d5cc7f05197e42f789da8805 |
| internal/hostguard/rootmigration_inventory.go RootAuthority.MigrationInstallations authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=read-only root installation enumeration includes incomplete records and never repairs before preflight ad855b461cb12e5499ecec74b210f6f41a758a3ed36c0eac0ef5931bc7fad6c2 |
| -internal/hostguard/rootview.go FenceView.admit authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=agent view applies the same retained root migration session denial f512598847c55a6c585206dc8e1daf7f6dd8625c4d408795ffe72ef3a442982a |
| +internal/hostguard/rootview.go FenceView.admit authority G,J4 rev3=PROPOSED-p3;identity=root claim controller construction uid 0;basis=read-only root view applies same predecessor-completion requirement as locked admission 5bcc6b69bb3e98a1bda6ace2b0b2a147f8c39b61601c609c30e3fb806231efae |
| internal/hostguard/rootview.go Guard.fenceLocked authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=projects root-only migration denials into agent admission snapshot without trusting local hostguard records 290295ed69e06548a41e101317098ae2d6aac10c41b260e120af55860686a96d |
| internal/hostguard/rootview.go RootAuthority.viewLocked authority C,G,J4 rev3=PROPOSED-p3;identity=carlos instance-control, root control service uid 0; denial views also consumed by carlos agent uid carlos;basis=projects root migration denials together with canonical installation and existing fences 5db6824b38f6ad3e4072008ffa3ff9fab98db6a5d40ecba59ca30900e331a46f |
| internal/rootstate/fault_rootrunner_linux.go WithWriteFault diagnostic A,F rev3=PROPOSED-p3;identity=rootrunner test binary uid 0 only;basis=rootrunner-only wrapper injects failures into the same validated production write steps; absent from shipped binary 31c00dbe4dd1441867d2a1184598be8a8fc5aa743e6f4227968b3b7f50d9c11d |
| @@ -91,11 +121,12 @@ internal/rootstate/state_linux.go directory.write authority A,F,G rev3=PROPOSED- |
| internal/safefile/fault_rootrunner_linux.go WriteAtFault diagnostic A,F rev3=PROPOSED-p3;identity=rootrunner test binary uid 0 only;basis=rootrunner-only seam runs actual safefile operations before/after chosen interruption dff2fd53a3561df3aa04b6f7462ab62c4cc32c826d152464989b6a399d83056c |
| internal/safefile/file_linux.go writeAt authority A,F,G rev3=PROPOSED-p3;identity=checked filesystem callers including root control/apply/migration uid 0;basis=production wrapper always invokes each real syscall operation eadb1d4dd961ebc7dc7ba8a469669003695d618d6c0dc13794763c7fa881d22c |
| internal/safefile/file_linux.go writeAtSteps authority A,F,G rev3=PROPOSED-p3;identity=checked filesystem callers including root control/apply/migration uid 0;basis=checked staged file write, chmod, sync, close, rename and parent sync propagate interruptions before acknowledgement file:Fstat@1116,file:Fstat@700,file:Openat@522,file:Openat@875,file:Read@277 865b847b97df6753e1abd16a00d018d27cdabdd23c96d42a97b381103d088d21 |
| -@coverage internal/console 5ce330f276a80d2ae901698ac81c7e2861f6363ba70aa1e5f9164b76c0c03bd8 |
| -@coverage internal/edgesfile 46558f38ee9cef8def5a1915a2545e91795ede008b4b296f102a7ffd45dd1b82 |
| -@coverage internal/host 92c9b29e256e6afa8fe90457752c2dc86f3cecb2340af0f655d097bef164a885 |
| +@coverage internal/console 6f4998c39e6d66cd73ea98500000cbecac251dd21dafd58f51a85c2291e0c533 |
| +@coverage internal/edgesfile 7b48368150a427e4620f1c9ded49942b688c9e6c9cd983b7da43d3f2c0810b72 |
| +@coverage internal/host 1f1b2f8b947763fe600c432a8100f4b999fafb3b4da1913af8555d55cc090219 |
| @coverage internal/host/units bf6ae516012701e0ca41908167e4efd60dd3c2c24e9af1555dec077e8be44461 |
| @coverage internal/hostbox 85594e031100a619d3bae5e555dcda2e496bdc4d91d0a53b0f4da0dbe6bbd261 |
| -@coverage internal/hostguard f97ec930b80a6b09fec120e4b25f17a01012400dcf433e96586a4140b6db2c63 |
| +@coverage internal/hostguard 98971d1a09a9c32d644752d441ae9e7072ac17352754e733cb6fa30eeeea218c |
| +@coverage internal/release df0c381af44576fc3d30648e6dbfdad63fb2f56d6f9add4be7ead8e34b66b451 |
| @coverage internal/rootstate 8415892a08d6c9c1b2d71aa25b0952cbcaa53e533213b7fac56dd0666ee787cf |
| @coverage internal/safefile 7030f312f7a60487381f0dbae2cdecaaa83625b639b6af15104f912592550816 |
diff --git a/internal/release/systempointer.go b/internal/release/systempointer.go| index 569794229..117f5af25 100644 |
| --- a/internal/release/systempointer.go |
| +++ b/internal/release/systempointer.go |
| @@ -37,6 +37,7 @@ type SystemPointer struct { |
| type SystemRelease struct { |
| ConsoleArtifacts map[string]SystemArtifact `json:"console_artifacts,omitempty"` |
| ReleaseTag string `json:"release_tag,omitempty"` |
| + SeedsProtocol int `json:"seeds_protocol,omitempty"` |
| MovesProtocol int `json:"moves_protocol,omitempty"` |
| Version string `json:"version"` |
| Artifacts map[string]SystemArtifact `json:"artifacts"` // keyed by "<goos>-<goarch>" |