carlos / platform Public

WIP claim Plan 14 track E checkpoint 38 (not for merge)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 5b4b7beccb427b601eb8415e904c355e207fe240 parent 62a99c1
10 files changed, +1069 −13
  • internal/host/plan14_generation_parked_linux_test.go +457 −0
  • internal/host/plan14_generation_parked_root_linux_test.go +224 −0
  • internal/host/plan14_generation_timer_retirement_test.go +121 −0
  • internal/host/rootgeneration_inventory.go +16 −3
  • internal/host/rootgeneration_parked.go +193 −0
  • internal/host/rootgeneration_rows.go +8 −1
  • internal/hostbox/reconcile_adapter.go +9 −2
  • internal/hostbox/retirement_timers.go +27 −0
  • internal/provision/plan14_e_write_census_test.go +1 −1
  • internal/provision/testdata/plan14-root-reads-e.tsv +13 −6

CI failed — 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	350.922s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.137s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.168s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.704s
ok  	github.com/carlosframework/platform/internal/activator	233.786s
ok  	github.com/carlosframework/platform/internal/agentsock	0.015s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.344s
ok  	github.com/carlosframework/platform/internal/apperr	0.071s
ok  	github.com/carlosframework/platform/internal/applog	0.125s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.896s
?   	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.008s
ok  	github.com/carlosframework/platform/internal/caps	0.012s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.702s
ok  	github.com/carlosframework/platform/internal/changelog	0.302s
ok  	github.com/carlosframework/platform/internal/clamd	0.055s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	365.913s
?   	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	6.459s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
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.004s
ok  	github.com/carlosframework/platform/internal/domains	2.168s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	26.120s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.007s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.127s
ok  	github.com/carlosframework/platform/internal/gate	1.656s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
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.155s
ok  	github.com/carlosframework/platform/internal/host	135.347s
ok  	github.com/carlosframework/platform/internal/hostactors	0.497s
ok  	github.com/carlosframework/platform/internal/hostbox	2.277s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.497s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.015s
ok  	github.com/carlosframework/platform/internal/hostexec	4.917s
ok  	github.com/carlosframework/platform/internal/hostfence	0.030s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.527s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.014s
ok  	github.com/carlosframework/platform/internal/hostguard	1.019s
ok  	github.com/carlosframework/platform/internal/hostops	1.448s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.014s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.014s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.441s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.136s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.014s
ok  	github.com/carlosframework/platform/internal/infraview	0.014s
ok  	github.com/carlosframework/platform/internal/instancectl	1.815s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.724s
ok  	github.com/carlosframework/platform/internal/instancefs	0.019s
ok  	github.com/carlosframework/platform/internal/instancekey	0.016s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.022s
ok  	github.com/carlosframework/platform/internal/layout	1.439s
ok  	github.com/carlosframework/platform/internal/lease	9.446s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.005s
ok  	github.com/carlosframework/platform/internal/ledger	0.012s
ok  	github.com/carlosframework/platform/internal/mail	0.171s
ok  	github.com/carlosframework/platform/internal/maintenance	0.081s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.523s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.079s
ok  	github.com/carlosframework/platform/internal/metrics	0.004s
ok  	github.com/carlosframework/platform/internal/moves	21.061s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.766s
ok  	github.com/carlosframework/platform/internal/objstore	0.175s
ok  	github.com/carlosframework/platform/internal/ops	0.172s
ok  	github.com/carlosframework/platform/internal/park	0.214s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	12.461s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.010s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.448s
ok  	github.com/carlosframework/platform/internal/relay	7.234s
ok  	github.com/carlosframework/platform/internal/release	22.041s
--- FAIL: TestAMoveOntoTheInstalledGoodBuildWaitsForASignedRollback (5.03s)
    --- FAIL: TestAMoveOntoTheInstalledGoodBuildWaitsForASignedRollback/worker (5.01s)
        holdran_test.go:796: timed out waiting for the move onto v1
FAIL
FAIL	github.com/carlosframework/platform/internal/rollout	8.619s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.007s
ok  	github.com/carlosframework/platform/internal/rootstate	0.027s
ok  	github.com/carlosframework/platform/internal/s3compat	0.007s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.319s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.600s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.837s
ok  	github.com/carlosframework/platform/internal/status	0.045s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.216s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.546s
FAIL
make: *** [Makefile:18: test] Error 1

amadan: step gate failed: exit status 2

amadan: job failed at step gate — its output is above, not at the tail of this log
diff --git a/internal/host/plan14_generation_parked_linux_test.go b/internal/host/plan14_generation_parked_linux_test.go
new file mode 100644
index 000000000..eec9a6032
--- /dev/null
+++ b/internal/host/plan14_generation_parked_linux_test.go
@@ -0,0 +1,457 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+ "github.com/carlosframework/platform/internal/rootstate"
+ "golang.org/x/sys/unix"
+)
+
+func generationParkedFixture(t *testing.T) (generationInventory, hostbox.RetirementJournal, string) {
+ t.Helper()
+ root := t.TempDir()
+ t.Setenv("CARLOS_DATA_DIR", root)
+ host := "parked.example"
+ in := hostbox.StopInventory{Complete: true, Host: host, Incarnation: "inc", InstallationStamp: 1, Box: "box", Enrollment: "en", DirectoryRevision: "1"}
+ in.Entries = []hostbox.StopInventoryEntry{
+ {Kind: "route", ArtifactID: "route", ManagedRoot: "registry", RelativePath: host, Ownership: "owner", RowIdentity: "row", Materialization: "materialized"},
+ {Kind: "timer", ArtifactID: "timer", ManagedRoot: "registry", RelativePath: host + "/backup", Ownership: "owner", RowIdentity: "timer", Materialization: "materialized"},
+ }
+ path := filepath.Join(root, "instances", host)
+ if err := os.MkdirAll(path, 0700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(path, "payload"), []byte("retained"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ var st unix.Stat_t
+ if err := unix.Stat(path, &st); err != nil {
+ t.Fatal(err)
+ }
+ in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Kind: "path", ArtifactID: "state", ManagedRoot: "data", RelativePath: "instances/" + host, Ownership: "owner", Materialization: "materialized", Device: uint64(st.Dev), Inode: st.Ino, Type: unix.S_IFDIR})
+ target := hostguard.FenceTarget{Host: host, Incarnation: in.Incarnation, Stamp: 1, Session: 1, Box: in.Box, Enrollment: in.Enrollment, DirectoryRevision: in.DirectoryRevision, FenceID: "retire", BootID: "boot", ControllerID: "controller", Op: "op", Phase: "retiring", ObligationID: "park"}
+ receipt := StopReceipt{V: 1, ID: "receipt", Kind: "stopped", Host: host, Incarnation: target.Incarnation, InstallationStamp: target.Stamp, WriterSession: target.Session, Box: target.Box, Enrollment: target.Enrollment, DirectoryRevision: target.DirectoryRevision, FenceID: target.FenceID, BootID: target.BootID, ControllerID: target.ControllerID, OperationID: target.Op, Phase: target.Phase, ObligationID: target.ObligationID, EvidenceAt: time.Unix(100, 0).UTC()}
+ raw, err := json.Marshal(receipt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ inventory := hostbox.RetirementInventory{Host: host, Captured: hostguard.Captured{Incarnation: in.Incarnation, Stamp: 1}, Ownership: "owner"}
+ for _, e := range in.Entries {
+ inventory.Artifacts = append(inventory.Artifacts, hostbox.Artifact{Kind: hostbox.ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Dev: e.Device, Ino: e.Inode, Type: e.Type})
+ }
+ j, err := hostbox.NewRetirementJournal(target.FenceID, inventory)
+ if err != nil {
+ t.Fatal(err)
+ }
+ j.Target = &target
+ j.StopReceipt = raw
+ j.Complete = true
+ for i := range j.Steps {
+ j.Steps[i].Done = true
+ }
+ view := hostguard.FenceView{Schema: 1, Host: host, Installation: hostguard.RootInstallation{Host: host, Incarnation: in.Incarnation, Stamp: 1, Box: in.Box, Enrollment: in.Enrollment, Migration: 1, InventoryRevision: 1, Account: "bab", App: "shop", Record: "apps/bab/shop/instances/" + host + ".json", Artifacts: hostguard.InstallationArtifacts{Schema: 1, StateDirectory: true, Timers: "backup"}}, Receipts: []hostguard.FenceReceipt{{Target: target, Bytes: raw, EvidenceAt: receipt.EvidenceAt}}}
+ snapshot := generationInventory{Census: rootparticipation.Census{Schema: 1, Box: in.Box, Enrollment: in.Enrollment, Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}, Installations: []hostguard.FenceView{view}, Payloads: []hostbox.GenerationPayload{{Inventory: in, Revision: 1}}, Launches: []hostbox.LaunchInventory{{Target: target, WritersClosed: true}}}
+ return snapshot, j, root
+}
+
+// Decode the durable snapshot shape, including the newly retained journal list.
+func generationParkedSnapshot(t *testing.T, snapshot generationInventory, journals []hostbox.RetirementJournal) generationInventory {
+ t.Helper()
+ raw, err := json.Marshal(snapshot)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var fields map[string]json.RawMessage
+ if err = json.Unmarshal(raw, &fields); err != nil {
+ t.Fatal(err)
+ }
+ fields["Retirements"], err = json.Marshal(journals)
+ if err != nil {
+ t.Fatal(err)
+ }
+ raw, err = json.Marshal(fields)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = json.Unmarshal(raw, &snapshot); err != nil {
+ t.Fatal(err)
+ }
+ return snapshot
+}
+
+func TestPlan14GenerationParkedPayload(t *testing.T) {
+ for _, attack := range []string{"complete", "predecessor", "pending", "unfinished-step", "wrong-inode", "wrong-owner", "wrong-target", "missing-receipt", "wrong-receipt", "missing-artifact", "extra-artifact", "duplicate-journal", "missing-parked", "replaced-parked", "symlink-parked", "live-row", "live-timer", "successor", "changed-during-kernel"} {
+ t.Run(attack, func(t *testing.T) {
+ snapshot, j, root := generationParkedFixture(t)
+ original := filepath.Join(root, "instances", j.Inventory.Host)
+ parked := filepath.Join(root, ".parked", j.Inventory.Captured.Incarnation, j.Inventory.Host, j.ID, "data", "instances", j.Inventory.Host)
+ if err := os.MkdirAll(filepath.Dir(parked), 0700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Rename(original, parked); err != nil {
+ t.Fatal(err)
+ }
+ reg, err := registry.Open(filepath.Join(root, "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer reg.Close()
+ t.Setenv("CARLOS_HOST_CLAIM", "true")
+ switch attack {
+ case "predecessor", "successor":
+ snapshot.Installations[0].Retired = []hostguard.Captured{j.Inventory.Captured}
+ snapshot.Installations[0].Installation.Stamp = 2
+ snapshot.Installations[0].Installation.Empty = true
+ current := snapshot.Payloads[0]
+ current.Inventory.InstallationStamp = 2
+ current.Inventory.Entries = nil
+ if attack == "successor" {
+ snapshot.Installations[0].Installation.Empty = false
+ current.Inventory.Entries = []hostbox.StopInventoryEntry{{Kind: "route", ArtifactID: "route", ManagedRoot: "registry", RelativePath: j.Inventory.Host, Ownership: "successor", RowIdentity: "next", Materialization: "materialized"}}
+ snapshot.Launches = append(snapshot.Launches, hostbox.LaunchInventory{Target: hostguard.FenceTarget{Host: j.Inventory.Host, Incarnation: "inc", Stamp: 2, Box: "box", Enrollment: "en"}, WritersClosed: true})
+ if err := reg.Upsert(registry.Route{Host: j.Inventory.Host, ClaimOwnership: registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 2, ClaimOwner: "successor", ClaimRowID: "next"}}); err != nil {
+ t.Fatal(err)
+ }
+ }
+ snapshot.Payloads = append(snapshot.Payloads, current)
+ case "pending":
+ j.Complete = false
+ case "unfinished-step":
+ j.Steps[0].Done = false
+ case "wrong-inode":
+ j.Inventory.Artifacts[0].Ino++
+ case "wrong-owner":
+ j.Inventory.Ownership = "other"
+ case "wrong-target":
+ j.Target.Session++
+ case "missing-receipt":
+ snapshot.Installations[0].Receipts = nil
+ case "wrong-receipt":
+ j.StopReceipt = []byte("forged")
+ case "missing-artifact":
+ j.Inventory.Artifacts = j.Inventory.Artifacts[:len(j.Inventory.Artifacts)-1]
+ case "extra-artifact":
+ j.Inventory.Artifacts = append(j.Inventory.Artifacts, hostbox.Artifact{Kind: hostbox.ArtifactTimer, Root: "registry", Name: j.Inventory.Host + "/unknown", Owner: "owner", RowID: "extra"})
+ case "missing-parked":
+ if err := os.Rename(parked, parked+".old"); err != nil {
+ t.Fatal(err)
+ }
+ case "replaced-parked", "symlink-parked":
+ if err := os.Rename(parked, parked+".old"); err != nil {
+ t.Fatal(err)
+ }
+ if attack == "replaced-parked" {
+ err = os.Mkdir(parked, 0700)
+ } else {
+ err = os.Symlink(parked+".old", parked)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ case "live-row":
+ if err := reg.Upsert(registry.Route{Host: j.Inventory.Host, ClaimOwnership: registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"}}); err != nil {
+ t.Fatal(err)
+ }
+ case "live-timer":
+ if err := reg.ReplaceTimers(j.Inventory.Host, []registry.Timer{{Host: j.Inventory.Host, Name: "backup", Kind: "cron", Registration: "timer", ClaimOwnership: registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "timer"}}}); err != nil {
+ t.Fatal(err)
+ }
+ }
+ journals := []hostbox.RetirementJournal{j}
+ if attack == "duplicate-journal" {
+ journals = append(journals, j)
+ }
+ snapshot = generationParkedSnapshot(t, snapshot, journals)
+ v := generationReconciler{read: func(ctx context.Context) (generationInventory, error) {
+ return generationRegistryInventory(ctx, reg, snapshot)
+ }, kernel: func(context.Context) (string, string, error) {
+ if attack == "changed-during-kernel" {
+ j.Complete = false
+ snapshot = generationParkedSnapshot(t, snapshot, []hostbox.RetirementJournal{j})
+ }
+ return "boot", generationInventoryDigest("kernel"), nil
+ }}
+ got, err := v.reconcile(t.Context(), nil)
+ good := attack == "complete" || attack == "predecessor" || attack == "successor"
+ if good && (err != nil || got.Installations == "") {
+ t.Fatalf("completed root retirement did not reconcile: %+v %v", got, err)
+ }
+ if !good && err == nil {
+ t.Fatal("invalid retirement certified a snapshot")
+ }
+ })
+ }
+}
+
+type generationParkAuthority struct{}
+
+func (generationParkAuthority) WithCleanup(_ context.Context, _ hostguard.FenceTarget, effect func() error) error {
+ return effect()
+}
+
+type generationCrashParker struct {
+ hostbox.TypedParker
+ crash bool
+}
+
+func (p generationCrashParker) Park(ctx context.Context, id string, a hostbox.Artifact) error {
+ if err := p.TypedParker.Park(ctx, id, a); err != nil {
+ return err
+ }
+ if p.crash {
+ os.Exit(38)
+ }
+ return nil
+}
+
+type generationCrashRows struct {
+ hostbox.RegistryRows
+ crash bool
+}
+
+func (r generationCrashRows) RetireRowsJournal(ctx context.Context, id, owner string, rows []hostbox.Artifact, archive func(json.RawMessage) error) error {
+ if err := r.RegistryRows.RetireRowsJournal(ctx, id, owner, rows, archive); err != nil {
+ return err
+ }
+ if r.crash {
+ os.Exit(38)
+ }
+ return nil
+}
+
+func generationRunPark(t *testing.T, root string, j hostbox.RetirementJournal, reg *registry.Registry, crash string) {
+ t.Helper()
+ j.Complete = false
+ for i := range j.Steps {
+ j.Steps[i].Done = false
+ }
+ p := hostbox.TypedParker{Roots: map[string]string{"data": root}, ParkedRoots: map[string]string{"data": root}, Incarnation: j.Inventory.Captured.Incarnation, Host: j.Inventory.Host, Retirement: j.ID}
+ r := hostbox.TypedRetirement{JournalDir: filepath.Join(root, "retire"), Authority: generationParkAuthority{}, Filesystem: generationCrashParker{p, crash == "park"}, Rows: generationCrashRows{hostbox.RegistryRows{Registry: reg, Inventory: j.Inventory}, crash == "rows"}, Ack: func(context.Context, hostbox.RetirementJournal) error {
+ if crash == "complete" {
+ os.Exit(38)
+ }
+ return nil
+ }}
+ if err := r.Run(t.Context(), *j.Target, j); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPlan14GenerationParkedCrash(t *testing.T) {
+ if root := os.Getenv("PLAN14_PARK_CRASH_ROOT"); root != "" {
+ var j hostbox.RetirementJournal
+ raw, err := os.ReadFile(filepath.Join(root, "intent.json"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = json.Unmarshal(raw, &j); err != nil {
+ t.Fatal(err)
+ }
+ reg, err := registry.Open(filepath.Join(root, "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer reg.Close()
+ generationRunPark(t, root, j, reg, os.Getenv("PLAN14_PARK_CRASH_POINT"))
+ t.Fatal("crash boundary did not exit")
+ }
+ for _, point := range []string{"park", "rows", "complete"} {
+ t.Run(point, func(t *testing.T) {
+ snapshot, j, root := generationParkedFixture(t)
+ t.Setenv("CARLOS_HOST_CLAIM", "true")
+ reg, err := registry.Open(filepath.Join(root, "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ own := registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"}
+ if err := reg.Upsert(registry.Route{Host: j.Inventory.Host, ClaimOwnership: own}); err != nil {
+ t.Fatal(err)
+ }
+ own.ClaimRowID = "timer"
+ if err := reg.ReplaceTimers(j.Inventory.Host, []registry.Timer{{Host: j.Inventory.Host, Name: "backup", Kind: "cron", Registration: "timer", ClaimOwnership: own}}); err != nil {
+ t.Fatal(err)
+ }
+ reg.Close()
+ raw, err := json.Marshal(j)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(root, "intent.json"), raw, 0600); err != nil {
+ t.Fatal(err)
+ }
+ cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestPlan14GenerationParkedCrash$", "-test.timeout=20s")
+ cmd.Dir = root
+ cmd.Env = append(os.Environ(), "PLAN14_PARK_CRASH_ROOT="+root, "PLAN14_PARK_CRASH_POINT="+point)
+ out, err := cmd.CombinedOutput()
+ var exited *exec.ExitError
+ if !errors.As(err, &exited) || exited.ExitCode() != 38 {
+ t.Fatalf("child did not crash at %s: %v %s", point, err, out)
+ }
+ reg, err = registry.Open(filepath.Join(root, "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer reg.Close()
+ read := func(ctx context.Context) (generationInventory, error) {
+ records, err := generationRetirementRecords(ctx, &rollbackDisk{dir: filepath.Join(root, "retire")})
+ if err != nil {
+ return generationInventory{}, err
+ }
+ return generationRegistryInventory(ctx, reg, generationParkedSnapshot(t, snapshot, records))
+ }
+ v := generationReconciler{read: read, kernel: func(context.Context) (string, string, error) { return "boot", generationInventoryDigest("kernel"), nil }}
+ _, err = v.reconcile(t.Context(), nil)
+ if (err == nil) != (point == "complete") {
+ t.Fatalf("interrupted retirement at %s: %v", point, err)
+ }
+ generationRunPark(t, root, j, reg, "")
+ if _, err := v.reconcile(t.Context(), nil); err != nil {
+ t.Fatal("resumed retirement", err)
+ }
+ generationRunPark(t, root, j, reg, "")
+ if _, err := v.reconcile(t.Context(), nil); err != nil {
+ t.Fatal("replayed retirement", err)
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationParkedReadback(t *testing.T) {
+ _, j, _ := generationParkedFixture(t)
+ for _, attack := range []string{"complete", "sync-failure", "unknown-field", "trailing", "wrong-name", "malformed", "unknown-file", "reader-failure"} {
+ t.Run(attack, func(t *testing.T) {
+ dir := t.TempDir()
+ raw, err := json.Marshal(j)
+ if err != nil {
+ t.Fatal(err)
+ }
+ name := j.ID + ".json"
+ switch attack {
+ case "unknown-field":
+ raw = append([]byte(`{"unexpected":true,`), raw[1:]...)
+ case "trailing":
+ raw = append(raw, []byte(` {}`)...)
+ case "wrong-name":
+ name = "other.json"
+ case "unknown-file":
+ name = "other"
+ case "malformed":
+ raw = []byte(`{"complete":`)
+ }
+ if err := os.WriteFile(filepath.Join(dir, name), raw, 0600); err != nil {
+ t.Fatal(err)
+ }
+ open := func(path string) (rootstate.Store, error) {
+ if attack == "reader-failure" {
+ return nil, os.ErrPermission
+ }
+ return &rollbackDisk{dir: path}, nil
+ }
+ calls := 0
+ sync := func(string) error {
+ calls++
+ if attack == "sync-failure" {
+ return os.ErrPermission
+ }
+ return nil
+ }
+ got, err := generationReadRetirements(t.Context(), dir, open, sync)
+ if attack == "complete" {
+ if err != nil || len(got) != 1 || calls != 1 {
+ t.Fatalf("complete read: %v %v %d", got, err, calls)
+ }
+ return
+ }
+ if err == nil || got != nil {
+ t.Fatal("invalid retirement read granted evidence", got, err)
+ }
+ if attack == "sync-failure" {
+ got, err = generationReadRetirements(t.Context(), dir, open, sync)
+ if err == nil || got != nil || calls != 2 {
+ t.Fatal("failed sync was not retried", got, err, calls)
+ }
+ got, err = generationReadRetirements(t.Context(), dir, open, func(string) error { return nil })
+ if err != nil || len(got) != 1 {
+ t.Fatal("durable reread failed", got, err)
+ }
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationParkedKeepsReservation(t *testing.T) {
+ snapshot, j, root := generationParkedFixture(t)
+ reg, err := registry.Open(filepath.Join(root, "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer reg.Close()
+ t.Setenv("CARLOS_HOST_CLAIM", "true")
+ own := registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"}
+ if err := reg.Upsert(registry.Route{Host: j.Inventory.Host, ClaimOwnership: own}); err != nil {
+ t.Fatal(err)
+ }
+ own.ClaimRowID = "timer"
+ if err := reg.ReplaceTimers(j.Inventory.Host, []registry.Timer{{Host: j.Inventory.Host, Name: "backup", Kind: "cron", Registration: "timer", ClaimOwnership: own}}); err != nil {
+ t.Fatal(err)
+ }
+ generationRunPark(t, root, j, reg, "")
+ opt := t.TempDir()
+ path := release.VersionPath(opt, "bab", "v1", "shop")
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte("reserved build"), 0755); err != nil {
+ t.Fatal(err)
+ }
+ identity := func(path string) rootstate.FileIdentity {
+ t.Helper()
+ var s unix.Stat_t
+ if err := unix.Stat(path, &s); err != nil {
+ t.Fatal(err)
+ }
+ return rootstate.FileIdentity{Device: uint64(s.Dev), Inode: s.Ino}
+ }
+ in := snapshot.Installations[0].Installation
+ binding := release.GenerationBinding{Binding: release.HistoryBinding{Host: in.Host, Account: in.Account, App: in.App}, Target: generationOwner(*j.Target), Installation: rootHistoryInstallation(in), Record: in.Record, RecordETag: "record", Attempt: "pending", Inputs: "inputs", Invocation: "invocation", ControllerPID: 42, ControllerStart: "start", Version: "v1", Digest: generationInventoryDigest("build"), Path: path, Tree: filepath.Dir(path), Executable: identity(path), TreeIdentity: identity(filepath.Dir(path))}
+ snapshot.Launches[0].ControllerPID = 42
+ snapshot.Launches[0].ProcessStart = "start"
+ disk := &rollbackDisk{dir: t.TempDir()}
+ ledger := &release.RootGenerations{Store: disk}
+ reserved, err := ledger.Reserve(binding)
+ if err != nil {
+ t.Fatal(err)
+ }
+ read := func(ctx context.Context) (generationInventory, error) {
+ records, err := generationRetirementRecords(ctx, &rollbackDisk{dir: filepath.Join(root, "retire")})
+ if err != nil {
+ return generationInventory{}, err
+ }
+ return generationRegistryInventory(ctx, reg, generationParkedSnapshot(t, snapshot, records))
+ }
+ reconciler := generationReconciler{read: read, kernel: func(context.Context) (string, string, error) { return "boot", generationInventoryDigest("kernel"), nil }}
+ pins, err := (release.RootGenerationPins{Ledger: ledger, Reconcile: reconciler.reconcile}).StagedPins(t.Context(), opt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if pinned, err := pins.Refers(path); err != nil || !pinned {
+ t.Fatal("completed retirement dropped pending reservation", pinned, err)
+ }
+ if got, err := ledger.Read(binding); err != nil || got != reserved {
+ t.Fatal("payload retirement changed generation authority", got, err)
+ }
+}
diff --git a/internal/host/plan14_generation_parked_root_linux_test.go b/internal/host/plan14_generation_parked_root_linux_test.go
new file mode 100644
index 000000000..7020c8228
--- /dev/null
+++ b/internal/host/plan14_generation_parked_root_linux_test.go
@@ -0,0 +1,224 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/rootstate"
+ "golang.org/x/sys/unix"
+)
+
+func TestPlan14GenerationParkedRootRunner(t *testing.T) {
+ const child = "PLAN14_E38_PARKED_ROOT"
+ dir := os.Getenv(child)
+ if dir == "" {
+ generationRunnerRegistryUser(t)
+ var err error
+ dir, err = os.MkdirTemp("/run", "plan14-parked-")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if err := os.RemoveAll(dir); err != nil {
+ t.Error(err)
+ }
+ })
+ if err := os.Chmod(dir, 0755); err != nil {
+ t.Fatal(err)
+ }
+ binary, err := os.Executable()
+ if err != nil {
+ t.Fatal(err)
+ }
+ cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", binary, "-test.run=^TestPlan14GenerationParkedRootRunner$", "-test.v", "-test.timeout=2m")
+ cmd.Dir = dir
+ cmd.Env = append(os.Environ(), child+"="+dir, "TMPDIR="+dir)
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("isolated parked inventory: %v\n%s", err, out)
+ }
+ t.Logf("%s", out)
+ return
+ }
+ if os.Geteuid() != 0 {
+ t.Fatal("designated runner requires root")
+ }
+ if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, ""); err != nil {
+ t.Fatal(err)
+ }
+ private := filepath.Join(dir, "varlib")
+ if err := os.Mkdir(private, 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := unix.Mount(private, "/var/lib", "", unix.MS_BIND, ""); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if err := unix.Unmount("/var/lib", 0); err != nil {
+ t.Error(err)
+ }
+ })
+ snapshot, j, root := generationParkedFixture(t)
+ t.Setenv("CARLOS_HOST_CLAIM", "true")
+ t.Setenv("CARLOS_OPT_DIR", dir)
+ if err := os.Mkdir(filepath.Join(dir, "versions"), 0755); err != nil {
+ t.Fatal(err)
+ }
+ for _, path := range []string{filepath.Dir(root), root} {
+ if err := os.Chmod(path, 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ reg, err := registry.Open(filepath.Join(root, "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ owner := registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"}
+ if err := reg.Upsert(registry.Route{Host: j.Inventory.Host, ClaimOwnership: owner}); err != nil {
+ t.Fatal(err)
+ }
+ owner.ClaimRowID = "timer"
+ if err := reg.ReplaceTimers(j.Inventory.Host, []registry.Timer{{Host: j.Inventory.Host, Name: "backup", Kind: "cron", Registration: "timer", ClaimOwnership: owner}}); err != nil {
+ t.Fatal(err)
+ }
+ // The installation, closed launch and stop receipt are fixtures. Parking,
+ // conditional row retirement, checked readback and kernel scanning are real.
+ generationRunPark(t, root, j, reg, "")
+ if err := reg.Close(); err != nil {
+ t.Fatal(err)
+ }
+ uid, gid := generationRunnerRegistryUser(t)
+ if err := os.Chown(filepath.Join(root, "registry.db"), uid, gid); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Chown(root, uid, gid); err != nil {
+ t.Fatal(err)
+ }
+ path := claimRetireJournalDir("", "")
+ state, err := rootstate.Open(path, true, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer state.Close()
+ raw, err := os.ReadFile(filepath.Join(root, "retire", j.ID+".json"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := state.Write(j.ID+".json", raw); err != nil {
+ t.Fatal(err)
+ }
+ read := func(ctx context.Context) (generationInventory, error) {
+ journals, err := productionGenerationRetirements(ctx)
+ if err != nil {
+ return generationInventory{}, err
+ }
+ return productionGenerationRegistryInventory(ctx, generationParkedSnapshot(t, snapshot, journals))
+ }
+ v := generationReconciler{read: read, kernel: productionGenerationKernelCensus}
+ accept := func() {
+ t.Helper()
+ if _, err := v.reconcile(t.Context(), nil); err != nil {
+ t.Fatal("checked parked reconciliation", err)
+ }
+ }
+ accept()
+ for _, attack := range []string{"uid", "writable-file", "symlink", "writable-parent", "missing-journal", "pending", "parked-replacement"} {
+ t.Run(attack, func(t *testing.T) {
+ file := filepath.Join(path, j.ID+".json")
+ switch attack {
+ case "uid":
+ if err := os.Chown(file, uid, gid); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if err := os.Chown(file, 0, 0); err != nil {
+ t.Error(err)
+ }
+ })
+ case "writable-file":
+ if err := os.Chmod(file, 0666); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if err := os.Chmod(file, 0600); err != nil {
+ t.Error(err)
+ }
+ })
+ case "writable-parent":
+ if err := os.Chmod(filepath.Dir(path), 0777); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if err := os.Chmod(filepath.Dir(path), 0700); err != nil {
+ t.Error(err)
+ }
+ })
+ case "symlink", "missing-journal":
+ if err := os.Rename(file, file+".saved"); err != nil {
+ t.Fatal(err)
+ }
+ if attack == "symlink" {
+ if err := os.Symlink(file+".saved", file); err != nil {
+ t.Fatal(err)
+ }
+ }
+ t.Cleanup(func() {
+ if attack == "symlink" {
+ if err := os.Remove(file); err != nil {
+ t.Error(err)
+ }
+ }
+ if err := os.Rename(file+".saved", file); err != nil {
+ t.Error(err)
+ }
+ })
+ case "pending":
+ var pending hostbox.RetirementJournal
+ if err := json.Unmarshal(raw, &pending); err != nil {
+ t.Fatal(err)
+ }
+ pending.Complete = false
+ body, err := json.Marshal(pending)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := state.Write(j.ID+".json", body); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if err := state.Write(j.ID+".json", raw); err != nil {
+ t.Error(err)
+ }
+ })
+ case "parked-replacement":
+ parked := filepath.Join(root, ".parked", "inc", j.Inventory.Host, j.ID, "data", "instances", j.Inventory.Host)
+ if err := os.Rename(parked, parked+".saved"); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir(parked, 0700); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if err := os.Remove(parked); err != nil {
+ t.Error(err)
+ }
+ if err := os.Rename(parked+".saved", parked); err != nil {
+ t.Error(err)
+ }
+ })
+ }
+ if _, err := v.reconcile(t.Context(), nil); err == nil {
+ t.Fatal("hostile parked state certified")
+ }
+ })
+ accept()
+ }
+}
diff --git a/internal/host/plan14_generation_timer_retirement_test.go b/internal/host/plan14_generation_timer_retirement_test.go
new file mode 100644
index 000000000..c41e4da8c
--- /dev/null
+++ b/internal/host/plan14_generation_timer_retirement_test.go
@@ -0,0 +1,121 @@
+package host
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+func TestPlan14GenerationTimerRetirement(t *testing.T) {
+ t.Setenv("CARLOS_HOST_CLAIM", "true")
+ for _, mismatch := range []bool{false, true} {
+ name := "exact"
+ if mismatch {
+ name = "replacement"
+ }
+ t.Run(name, func(t *testing.T) {
+ coordinator, req, _ := claimStopFixture(t)
+ result, err := coordinator.FenceAndStop(t.Context(), req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ base := t.TempDir()
+ inventory, err := hostbox.OpenStopInventoryDB(filepath.Join(base, "inventory.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer inventory.Close()
+ reg, err := registry.Open(filepath.Join(base, "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer reg.Close()
+ owner := registry.ClaimOwnership{ClaimIncarnation: req.Incarnation, ClaimStamp: req.InstallationStamp, ClaimOwner: "installation", ClaimRowID: "route-1"}
+ row := registry.Route{Host: req.Host, ClaimOwnership: owner}
+ if err := reg.Upsert(row); err != nil {
+ t.Fatal(err)
+ }
+ timer := registry.Timer{Host: req.Host, Name: "backup", Kind: "cron", Registration: "timer-1", ClaimOwnership: owner}
+ timer.ClaimRowID = "timer-1"
+ if mismatch {
+ timer.Registration = "replacement"
+ timer.ClaimRowID = "replacement"
+ }
+ if err := reg.ReplaceTimers(req.Host, []registry.Timer{timer}); err != nil {
+ t.Fatal(err)
+ }
+ in := hostbox.StopInventory{Complete: true, Host: req.Host, Incarnation: req.Incarnation, InstallationStamp: req.InstallationStamp, Box: req.Box, Enrollment: req.Enrollment, DirectoryRevision: req.ExpectedDirectoryRevision, Entries: []hostbox.StopInventoryEntry{
+ {Kind: "route", ArtifactID: "route", ManagedRoot: "registry", RelativePath: req.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"},
+ {Kind: "timer", ArtifactID: "timer", ManagedRoot: "registry", RelativePath: req.Host + "/backup", RowIdentity: "timer-1", Ownership: owner.ClaimOwner, Materialization: "materialized"},
+ }}
+ if err := inventory.WriteStopInventory(t.Context(), in); err != nil {
+ t.Fatal(err)
+ }
+ journal := filepath.Join(base, "retire")
+ root := ClaimRootHTTP{Stop: ClaimStopHTTP{Coordinator: coordinator}, Roots: map[string]string{"data": base}, Reconcile: hostbox.ReconcileAdapter{Inventory: inventory, Registry: reg, Retirement: hostbox.TypedRetirement{JournalDir: journal}}}
+ // Remote acknowledgement is deliberately unavailable. Local completion
+ // must already be durable, and cannot be confused with the remote result.
+ if err := root.retire(t.Context(), result.Target); err == nil {
+ t.Fatal("missing claim accepted")
+ }
+ got, err := reg.Get(req.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ timers, err := reg.TimersFor(req.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if mismatch {
+ if got == nil || len(timers) != 1 || timers[0].Registration != "replacement" {
+ t.Fatal("replacement changed")
+ }
+ return
+ }
+ if got != nil || len(timers) != 0 {
+ t.Fatalf("retained timer prevented exact retirement: route=%v timers=%v", got, timers)
+ }
+ records, err := generationRetirementRecords(t.Context(), &rollbackDisk{dir: journal})
+ if err != nil || len(records) != 1 || !records[0].Complete {
+ t.Fatal("missing local completion", records, err)
+ }
+ if len(records[0].Inventory.Artifacts) != 2 {
+ t.Fatal("duplicated timer", records[0])
+ }
+ before, err := os.ReadFile(filepath.Join(journal, result.Target.FenceID+".json"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ row.ClaimStamp++
+ row.ClaimOwner = "successor"
+ row.ClaimRowID = "route-2"
+ if err := reg.Upsert(row); err != nil {
+ t.Fatal(err)
+ }
+ timer.ClaimOwnership = row.ClaimOwnership
+ timer.ClaimRowID = "timer-2"
+ timer.Registration = "timer-2"
+ if err := reg.ReplaceTimers(req.Host, []registry.Timer{timer}); err != nil {
+ t.Fatal(err)
+ }
+ if err := root.retire(t.Context(), result.Target); err == nil {
+ t.Fatal("missing claim accepted on replay")
+ }
+ got, err = reg.Get(req.Host)
+ if err != nil || got == nil || got.ClaimRowID != "route-2" {
+ t.Fatal("successor route changed", got, err)
+ }
+ timers, err = reg.TimersFor(req.Host)
+ if err != nil || len(timers) != 1 || timers[0].Registration != "timer-2" {
+ t.Fatal("successor timer changed", timers, err)
+ }
+ after, err := os.ReadFile(filepath.Join(journal, result.Target.FenceID+".json"))
+ if err != nil || string(before) != string(after) {
+ t.Fatal("replay changed completed journal", err)
+ }
+ })
+ }
+}
diff --git a/internal/host/rootgeneration_inventory.go b/internal/host/rootgeneration_inventory.go
index 1a58bf67c..191abe5e9 100644
--- a/internal/host/rootgeneration_inventory.go
+++ b/internal/host/rootgeneration_inventory.go
@@ -18,6 +18,7 @@ import (
)
type generationInventory struct {
+ Retirements []hostbox.RetirementJournal
Census rootparticipation.Census
Installations []hostguard.FenceView
Launches []hostbox.LaunchInventory
@@ -78,7 +79,11 @@ func (v generationReconciler) reconcile(ctx context.Context, records []release.G
if err := generationPayloadBindings(before.Payloads, installs); err != nil {
return zero, err
}
- if err := generationPayloadFiles(ctx, dataDir(), before.Payloads); err != nil {
+ physical, err := generationPhysicalPayloads(ctx, before)
+ if err != nil {
+ return zero, err
+ }
+ if err := generationPayloadFiles(ctx, dataDir(), physical); err != nil {
return zero, err
}
matched := make([]bool, len(records))
@@ -178,10 +183,14 @@ func (v generationReconciler) reconcile(ctx context.Context, records []release.G
if snapshotDigest != generationInventoryDigest(after) {
return zero, os.ErrPermission
}
- if err := generationPayloadFiles(ctx, dataDir(), after.Payloads); err != nil {
+ physical, err = generationPhysicalPayloads(ctx, after)
+ if err != nil {
return zero, err
}
- return release.GenerationEvidence{Box: c.Box, Enrollment: c.Enrollment, Migration: c.Migration, Boot: boot, Installations: generationInventoryDigest([]any{before.Installations, before.Payloads, before.Records}), Launches: generationInventoryDigest([]any{c.LaunchRevision, before.Launches}), Kernel: kernel}, nil
+ if err := generationPayloadFiles(ctx, dataDir(), physical); err != nil {
+ return zero, err
+ }
+ return release.GenerationEvidence{Box: c.Box, Enrollment: c.Enrollment, Migration: c.Migration, Boot: boot, Installations: generationInventoryDigest([]any{before.Installations, before.Payloads, before.Records, before.Retirements}), Launches: generationInventoryDigest([]any{c.LaunchRevision, before.Launches}), Kernel: kernel}, nil
}
func generationPayloadBindings(payloads []hostbox.GenerationPayload, installs map[string]hostguard.FenceView) error {
@@ -234,6 +243,10 @@ func productionGenerationInventory(ctx context.Context) (generationInventory, er
if err != nil {
return generationInventory{}, err
}
+ snapshot.Retirements, err = productionGenerationRetirements(ctx)
+ if err != nil {
+ return generationInventory{}, err
+ }
if len(snapshot.Installations) == 0 {
return productionGenerationRegistryInventory(ctx, snapshot)
}
diff --git a/internal/host/rootgeneration_parked.go b/internal/host/rootgeneration_parked.go
new file mode 100644
index 000000000..b4eaba56f
--- /dev/null
+++ b/internal/host/rootgeneration_parked.go
@@ -0,0 +1,193 @@
+package host
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "os"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+// A completed root retirement changes where an obligation is checked. It never
+// deletes the payload, its requirements, or a generation reservation.
+func generationRetirements(ctx context.Context, snapshot generationInventory) (map[int]string, error) {
+ retired := map[int]string{}
+ for _, j := range snapshot.Retirements {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ canonical, err := hostbox.NewRetirementJournal(j.ID, j.Inventory)
+ if err != nil || j.V != 1 || !j.Complete || j.Target == nil || j.ID != j.Target.FenceID || !reflect.DeepEqual(j.Inventory, canonical.Inventory) || len(j.Steps) != len(canonical.Steps) {
+ return nil, os.ErrPermission
+ }
+ for i, step := range j.Steps {
+ if !step.Done || step.Artifact != canonical.Steps[i].Artifact {
+ return nil, os.ErrPermission
+ }
+ }
+ t := *j.Target
+ if t.Incarnation == "." || t.Incarnation == ".." || strings.ContainsAny(t.Incarnation, "/\\\x00") {
+ return nil, os.ErrPermission
+ }
+ index := -1
+ for i, p := range snapshot.Payloads {
+ in := p.Inventory
+ if in.Host == t.Host && in.Incarnation == t.Incarnation && in.InstallationStamp == t.Stamp {
+ if index != -1 || in.Box != t.Box || in.Enrollment != t.Enrollment || in.DirectoryRevision != t.DirectoryRevision {
+ return nil, os.ErrPermission
+ }
+ index = i
+ }
+ }
+ if index < 0 || retired[index] != "" || j.Inventory.Host != t.Host || j.Inventory.Captured.Incarnation != t.Incarnation || j.Inventory.Captured.Stamp != t.Stamp {
+ return nil, os.ErrPermission
+ }
+ receipt := false
+ for _, view := range snapshot.Installations {
+ if view.Host != t.Host {
+ continue
+ }
+ for _, r := range view.Receipts {
+ if r.Target == t && bytes.Equal(r.Bytes, j.StopReceipt) && verifyRetirementReceipt(ctx, r) == nil {
+ receipt = true
+ }
+ }
+ }
+ if !receipt {
+ return nil, os.ErrPermission
+ }
+ expected := map[hostbox.Artifact]bool{}
+ for _, e := range snapshot.Payloads[index].Inventory.Entries {
+ if e.Ownership != j.Inventory.Ownership || e.Materialization != "materialized" {
+ return nil, os.ErrPermission
+ }
+ a := hostbox.Artifact{Kind: hostbox.ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Dev: e.Device, Ino: e.Inode, Type: e.Type}
+ if expected[a] {
+ return nil, os.ErrPermission
+ }
+ expected[a] = true
+ }
+ matched := map[hostbox.Artifact]bool{}
+ for _, a := range j.Inventory.Artifacts {
+ // The retirement producer also captures the timer registration. Root's
+ // inventory establishes that registration as the timer's exact row ID.
+ if a.Kind == hostbox.ArtifactTimer {
+ row, registration, ok := strings.Cut(a.RowID, "~")
+ if ok {
+ if row != registration {
+ return nil, os.ErrPermission
+ }
+ a.RowID = row
+ }
+ }
+ if !expected[a] {
+ return nil, os.ErrPermission
+ }
+ matched[a] = true
+ }
+ if len(matched) != len(expected) {
+ return nil, os.ErrPermission
+ }
+ retired[index] = j.ID
+ }
+ return retired, nil
+}
+
+func generationPhysicalPayloads(ctx context.Context, snapshot generationInventory) ([]hostbox.GenerationPayload, error) {
+ retired, err := generationRetirements(ctx, snapshot)
+ if err != nil {
+ return nil, err
+ }
+ payloads := append([]hostbox.GenerationPayload(nil), snapshot.Payloads...)
+ for i, id := range retired {
+ in := &payloads[i].Inventory
+ in.Entries = append([]hostbox.StopInventoryEntry(nil), in.Entries...)
+ for n, e := range in.Entries {
+ if e.Kind == "path" {
+ if !filepath.IsLocal(e.RelativePath) || filepath.Clean(e.RelativePath) != e.RelativePath {
+ return nil, os.ErrPermission
+ }
+ in.Entries[n].RelativePath = filepath.Join(".parked", in.Incarnation, in.Host, id, e.ManagedRoot, e.RelativePath)
+ }
+ }
+ }
+ return payloads, nil
+}
+
+func generationRetirementRecords(ctx context.Context, store rootstate.Store) ([]hostbox.RetirementJournal, error) {
+ names, err := store.Names()
+ if err != nil {
+ return nil, err
+ }
+ if len(names) > 8192 {
+ return nil, os.ErrPermission
+ }
+ sort.Strings(names)
+ var out []hostbox.RetirementJournal
+ for _, name := range names {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ // Cold handover retains its own payload inventory. It is never parking
+ // authority here. Temporary writes cannot establish completed retirement.
+ if name == "cold" || strings.HasPrefix(name, ".retire-") {
+ continue
+ }
+ if !strings.HasSuffix(name, ".json") {
+ return nil, os.ErrPermission
+ }
+ raw, err := store.Read(name)
+ if err != nil {
+ return nil, err
+ }
+ var j hostbox.RetirementJournal
+ dec := json.NewDecoder(bytes.NewReader(raw))
+ dec.DisallowUnknownFields()
+ if dec.Decode(&j) != nil || dec.Decode(new(any)) != io.EOF || j.ID+".json" != name {
+ return nil, os.ErrPermission
+ }
+ out = append(out, j)
+ }
+ return out, nil
+}
+
+func productionGenerationRetirements(ctx context.Context) ([]hostbox.RetirementJournal, error) {
+ return generationReadRetirements(ctx, claimRetireJournalDir("", ""), func(path string) (rootstate.Store, error) {
+ return rootstate.Open(path, false, false)
+ }, func(path string) error {
+ directory, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer directory.Close()
+ return directory.Sync()
+ })
+}
+
+func generationReadRetirements(ctx context.Context, path string, open func(string) (rootstate.Store, error), sync func(string) error) ([]hostbox.RetirementJournal, error) {
+ store, err := open(path)
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ defer store.Close()
+ journals, err := generationRetirementRecords(ctx, store)
+ if err != nil {
+ return nil, err
+ }
+ // Sync the completion rename without rewriting concurrent progress.
+ if err := sync(path); err != nil {
+ return nil, err
+ }
+ return journals, nil
+}
diff --git a/internal/host/rootgeneration_rows.go b/internal/host/rootgeneration_rows.go
index 7fb2a1aa8..916208c29 100644
--- a/internal/host/rootgeneration_rows.go
+++ b/internal/host/rootgeneration_rows.go
@@ -28,12 +28,19 @@ func generationRegistryInventory(ctx context.Context, reg *registry.Registry, sn
if err := generationRetainedTimers(snapshot); err != nil {
return zero, err
}
+ retired, err := generationRetirements(ctx, snapshot)
+ if err != nil {
+ return zero, err
+ }
type key struct{ kind, host, name string }
expected := map[key]registry.ClaimOwnership{}
hosts := map[string]bool{}
- for _, p := range snapshot.Payloads {
+ for index, p := range snapshot.Payloads {
in := p.Inventory
hosts[in.Host] = true
+ if retired[index] != "" {
+ continue
+ }
for _, entry := range in.Entries {
if entry.Kind != "route" && entry.Kind != "timer" {
continue
diff --git a/internal/hostbox/reconcile_adapter.go b/internal/hostbox/reconcile_adapter.go
index 81cd4ab40..fd85509ee 100644
--- a/internal/hostbox/reconcile_adapter.go
+++ b/internal/hostbox/reconcile_adapter.go
@@ -335,7 +335,11 @@ func (a ReconcileAdapter) Retire(ctx context.Context, row registry.Route) error
timers = append(timers, x)
}
}
- want, err := NewRetirementJournal(target.FenceID, RetirementInventory{Host: inventory.Host, Captured: inventory.Captured, Ownership: inventory.Ownership, Artifacts: append(append([]Artifact(nil), inventory.Artifacts...), timers...)})
+ combined, err := retirementWithTimers(inventory, timers)
+ if err != nil {
+ return err
+ }
+ want, err := NewRetirementJournal(target.FenceID, combined)
if err != nil {
return ErrTypedJournal
}
@@ -355,7 +359,10 @@ func (a ReconcileAdapter) Retire(ctx context.Context, row registry.Route) error
if t.ClaimIncarnation != row.ClaimIncarnation || t.ClaimStamp != row.ClaimStamp || t.ClaimOwner != row.ClaimOwner || t.ClaimRowID == "" || t.Registration == "" {
return ErrTypedInventory
}
- inventory.Artifacts = append(inventory.Artifacts, Artifact{Kind: ArtifactTimer, Root: "registry", Name: row.Host + "/" + t.Name, Owner: row.ClaimOwner, RowID: t.ClaimRowID + "~" + t.Registration})
+ inventory, err = retirementWithTimers(inventory, []Artifact{{Kind: ArtifactTimer, Root: "registry", Name: row.Host + "/" + t.Name, Owner: row.ClaimOwner, RowID: t.ClaimRowID + "~" + t.Registration}})
+ if err != nil {
+ return err
+ }
}
}
diff --git a/internal/hostbox/retirement_timers.go b/internal/hostbox/retirement_timers.go
new file mode 100644
index 000000000..2e3019361
--- /dev/null
+++ b/internal/hostbox/retirement_timers.go
@@ -0,0 +1,27 @@
+package hostbox
+
+import "strings"
+
+func retirementWithTimers(in RetirementInventory, timers []Artifact) (RetirementInventory, error) {
+ in.Artifacts = append([]Artifact(nil), in.Artifacts...)
+ for _, timer := range timers {
+ found := false
+ for _, retained := range in.Artifacts {
+ if retained.Kind != ArtifactTimer || retained.Name != timer.Name {
+ continue
+ }
+ row, registration, combined := strings.Cut(timer.RowID, "~")
+ if !combined {
+ row, registration = timer.RowID, timer.RowID
+ }
+ if retained.Root != timer.Root || retained.Owner != timer.Owner || retained.RowID != row || row != registration || timer.Dev != 0 || timer.Ino != 0 || timer.Type != 0 {
+ return RetirementInventory{}, ErrTypedInventory
+ }
+ found = true
+ }
+ if !found {
+ in.Artifacts = append(in.Artifacts, timer)
+ }
+ }
+ return in, nil
+}
diff --git a/internal/provision/plan14_e_write_census_test.go b/internal/provision/plan14_e_write_census_test.go
index a81438fbd..6e4b2c8e3 100644
--- a/internal/provision/plan14_e_write_census_test.go
+++ b/internal/provision/plan14_e_write_census_test.go
@@ -3,7 +3,7 @@ package provision
// Track E overlays the reviewed Part 1 write census without changing its baseline.
var plan14EWriteCensusOverlay = []censusEntry{
{"internal/host/rootgeneration_requirements.go", "generationRequiredInventory", "key-site", "key:RecordKey", "0ceac8b867717d959ccc6a66a44505a177ad56d09bd79d826c89a506eb8d00a8"},
- {"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "d928288910227cc14f158595e44d052436a23c407e89682dd0930ee1011d7e57"},
+ {"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "790671d424afb68f27e6ac404f4709443ce7b9a5af0df030d5a4784a20e61441"},
{"internal/host/rootgenerations_launch.go", "rootGenerationSource.resolve", "key-site", "key:RecordKey", "d69a3c0c86b03d5469708ac8bf084f4f516c0b61a18519d46cc3c0a0616f5381"},
{"internal/historyreadback/workflow.go", "Operation.deliverGrant", "nonrecord", "sel:Put", "fb410ef6274ef7de0997a804c837ad1ecf29d8826ba1c4dcda00c2ae986fc434"},
{"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "12b08da25b91b0827412cfe0119bdcc9a98bda5a60c56a987e0d44f7016d3bab"},
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv
index 69330a916..e5aa8e9b0 100644
--- a/internal/provision/testdata/plan14-root-reads-e.tsv
+++ b/internal/provision/testdata/plan14-root-reads-e.tsv
@@ -54,7 +54,7 @@ internal/release/roothistory_replace.go replacementReconstruction authority E1 S
internal/release/signedhead.go StageSignedHead authority E1 Canonical route is supplied by the root unit verifier. Verifies signed channel, digest ratchet and manifest/artifact before staging; unavailable pin census never authorizes restaging differing bytes. Fixtures: TestPlan14Incident20261006WithLedger and TestPlan14SignedNamedRecoveryAuthority. registry:Account@202,registry:Account@302,registry:Account@403,registry:App@214,registry:App@415,registry:Channel@222,registry:Host@434 ca02f04e55556ba0233dfa0848b0c533a9d9aa06cad6abd33cfd2968b3bd838a
internal/rootstate/link_linux.go ReadTrustedLink authority A,E1 Checks the full root-owned parent chain before descriptor-based live-link reading. Fixture: TestPlan14TrustedLiveLink; privileged acceptance remains required. file:Open@156 90f8e8de4e2c71f07b5d749cbe4bb1c1dfa426576a82138823449f56fc9ec5ab
internal/rootstate/link_linux.go directory.readLink authority A,E1 Pins the symlink inode with O_PATH and checks owner/type/link count before readlinkat. Fixture: TestPlan14TrustedLiveLink. file:Fstat@356,file:Openat@183,file:Readlinkat@554 a8559fe63dce48830916004ecfe7fbbe5d419568c7daf7a3073cf1b5bb12c3d7
-@coverage internal/host 95dbbe0e1849ac78eb20c623ea6929a8433970bd238fe16953e52e803643fd5c
+@coverage internal/host 224a569675197b714ee2f78cdefd72a2d3efa3e74fd9d95f78a49bcdd2a3b8e8
@coverage internal/release 2bb546a86c1ab49b02e424eb0b366b46776857a1249baa2d6fb61ead4209d6bd
@coverage internal/rootstate a773d6dbc0e4150e0054f7a94d0e1493c88d0d57f4f4530138b3427e2c217bc5
internal/release/roothistory_status.go RootHistory.MigrationStatus authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. file:Read@1062,file:Read@3891 f0ab7ad2d2d70f8c22ebfd575f5a1c6931f7eba30ccdba80ad43d0efffc88154
@@ -295,16 +295,16 @@ internal/host/rootgeneration_census_linux.go generationKernel.census authority E
internal/host/rootgeneration_inventory.go generationInventoryAt authority E2 E26: checked read-only payload enumeration includes inventory and revision orphans; completeness binds current and retained predecessor inventories, exact current revision and both source reads. Fixtures: TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationPayloadInventory, TestPlan14GenerationPayloadReaderDoesNotInitialize. Checked root reader acceptance passed at E27; running-tenant containment remains unfinished. file:Open@173,file:Open@424 094f659671b83f647aa57b613b9b2d51a723cc6d00e49a6e19c71ff9f63c2087
internal/hostbox/generationinventory.go LaunchJournal.GenerationLaunches authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. file:QueryContext@270 6694bd3a35b8d91f53e67134145c44854086f08593110ed1b905bca9587be290
internal/hostbox/generationinventory.go OpenGenerationLaunchReader authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. file:Lstat@218,file:Open@283 552d8629a7932c4ae5dca9d2beef4559f7e8a2864259d11e2ff98c699fdcd999
-@coverage internal/hostbox 2350dd34cc3415255fb3a95a0ddf2e140970c2742136abcaf6bff2e7e5745f5c
+@coverage internal/hostbox 3ec834ccebd28c7624cd1737a0db267b736e93ba8943fb26e65e9a9e3b85eff6
internal/host/rootgeneration_census_linux.go productionGenerationKernelCensus authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. 85f085029e0cd557c8ff8eb2d522fdf131c388229c14515fda0f45453d40eebb
internal/host/rootgeneration_census_other.go productionGenerationKernelCensus authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. a1217cf881e9d05336fc16d6050f7f7a1a2184a0c20ff2988d0cf0f9012eaa45
internal/host/rootgeneration_inventory.go generationInstallation authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. 3e5e15d50752510acb44b10eb29621757a87bff54ceae015f704dd7f3a848ecb
internal/host/rootgeneration_inventory.go generationInventoryDigest authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. 36ff5a31c6a013ac907d85e59b754cf4088df4600646ba7df177cf14e990177a
internal/host/rootgeneration_inventory.go generationOwner authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. 69f89153837761f13116937b330ffbf406891299d5580296ea4aade905aa02ce
-internal/host/rootgeneration_inventory.go generationReconciler.reconcile authority E2 E30: derive route and ordinary-instance state-container requirements from schema- and shape-validated canonical deployment records bound to retained installation incarnation; include record contents and ETag in both source reads and the certificate digest. Missing, mismatched, omitted or changed evidence refuses without retiring pins. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationProductionModeGate. Production fixed-path witness TestPlan14GenerationRequiredArtifactsRootRunner is runner-pending. Timer classes, parked artifacts, positive empty-installation evidence and running-tenant containment remain incomplete. d928288910227cc14f158595e44d052436a23c407e89682dd0930ee1011d7e57
+internal/host/rootgeneration_inventory.go generationReconciler.reconcile authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. 790671d424afb68f27e6ac404f4709443ce7b9a5af0df030d5a4784a20e61441
internal/host/rootgeneration_inventory.go openGenerationLaunchInventory authority E2 E33: census-only generation journal reads accept private root-owned ancestry without chmod or repair. Every ancestor remains descriptor-checked, root-owned and non-writable by group/other; leaves remain single-link regular root-owned files, checked for DB/WAL/SHM. Public-file callers remain unchanged. Fixtures: TestPlan14AuthorityFilePrivateAncestors, TestPlan14AuthorityFileAbsoluteBoundary; TestPlan14GenerationPrivateJournalRootRunner and TestPlan14GenerationRequiredArtifactsRootRunner are runner-pending. Pre-flight 76, 78, 82 and 89. 2b54531e16282caeb0ddbc1dc92b18d6cb2db652589eb3757daefe10daa63a53
-internal/host/rootgeneration_inventory.go productionGenerationInventory authority E2 E32: census-only E2 completeness discovers the bounded union of route and timer hosts, including inactive orphan timers, through the service-user read-only parser. Unknown hosts refuse even with an empty root installation list; discovery supplies no authority and never removes a retained obligation. Pre-flight 33, 80, 82, 83, 89 and 90. Fixtures: TestArtifactHosts, TestRemoteReadOnlyWALRegistry, TestPlan14GenerationRegistryRows, TestPlan14GenerationRegistryProductionReader; extended TestPlan14GenerationRequiredArtifactsRootRunner remains runner-pending. E34: census-only generation completeness requires E1 root creation evidence for an empty installation without retained predecessors. The exact host/account/app/installation must remain fresh on both sides of kernel inspection. Existing predecessor obligations remain mandatory. Production root history uses checked private ancestry and recommits before granting freshness, so read or sync failure refuses. Pre-flight 20, 33, 79 and 80. Fixtures: TestPlan14GenerationEmptyInstallationRequiresCreation, TestPlan14GenerationCreationFromRegistration, TestPlan14GenerationRequiredArtifacts; production checked-path TestPlan14GenerationRequiredArtifactsRootRunner remains runner-pending. No new history initialization or claims-off call. 43f12726911af472eeeec8253fac21ef4bfac2c19c66f9bfe6fc28036c557842
-internal/host/rootgeneration_inventory.go type generationInventory authority E2 E30: derive route and ordinary-instance state-container requirements from schema- and shape-validated canonical deployment records bound to retained installation incarnation; include record contents and ETag in both source reads and the certificate digest. Missing, mismatched, omitted or changed evidence refuses without retiring pins. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationProductionModeGate. Production fixed-path witness TestPlan14GenerationRequiredArtifactsRootRunner is runner-pending. Timer classes, parked artifacts, positive empty-installation evidence and running-tenant containment remain incomplete. 4de1dc2a2c9261b1e7bbd811d4d78a271d2fc5a37aeca196177f3e9527d0408e
+internal/host/rootgeneration_inventory.go productionGenerationInventory authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. 32fe8188ec5a22297f4e0aa68f2741839461637a2ef2e7a282f9c5115141db6d
+internal/host/rootgeneration_inventory.go type generationInventory authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. 7a1e16370c99823e83e11e7824162082f85abf82e5dd5e18f029c82ab9e26f08
internal/host/rootgeneration_inventory.go type generationReconciler authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. a979cdff1cd6311cf57d4a93e4501fb8dc2f65772d0013e622175b2a1f610d91
internal/hostguard/generationinventory.go RootAuthority.GenerationInstallations authority E2 E37: retain independently initialized state-directory and timer requirements in the root installation transaction; preserve them through crash/reopen and rebind. Unknown history cannot certify completeness. Recommit under root-host exclusion before generation use. Canonical record and root retained obligations constrain agent candidate lists; timer enumeration is deny-only. Pre-flight 33, 80, 83. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationRegistryRows, TestPlan14RootTimerInventory, TestPlan14InventoryRequirementsRegistration, TestPlan14InventoryRequirementsUnknownAndRatchet, TestPlan14InventoryArtifactsCrash, TestPlan14InventoryRequirementsHistoryIdentity. Changed RequiredArtifacts and ExactRetirement root-runner witnesses await acceptance; remaining E2 launch/parked/containment integration is incomplete. 4219e7952cc7c83513f810bae72dbed60384f8bddfd3d121de92eb8b1f82e866
internal/host/rootgeneration_retirement.go generationRetirement.closure authority E2 E25: validate installation, controller, exact unit and process before retaining an active or prelaunch pin. Only closed writers plus a settled unit reach durable retirement and independent kernel absence. Use the checked read-only journal and continue past active pins. Fixtures: TestPlan14GenerationRetirementKeepsActivePin, TestPlan14GenerationRetirementKeepsPrelaunchPin, TestPlan14GenerationRetirementContinuesPastActivePin, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Running-tenant completeness remains unfinished. f698ed949535287a55d5a8fc103f7dc5bf0f2c5020e27ff96552f42852024d03
@@ -317,7 +317,7 @@ internal/host/rootgeneration_payload_linux.go generationPayloadFiles authority E
internal/host/rootgeneration_payload_other.go generationPayloadFiles authority E2 E28: retained data payloads require exact device/inode/type through no-symlink descriptor opens before and after kernel inspection. Missing, replaced and intended payloads remain unresolved. Tests: TestPlan14GenerationPayloadPhysicalIdentity; privileged TestPlan14GenerationPayloadPhysicalRootRunner remains runner-pending. Route/timer and running-tenant containment are not accepted by this check. candidate:Kind@183,candidate:Kind@204 182189d8409113d671cdc76fc5d2d73e7056057bca230d546e65300c428b2c18
internal/host/rootgeneration_requirements.go generationRequiredInventory authority E2 E37: retain independently initialized state-directory and timer requirements in the root installation transaction; preserve them through crash/reopen and rebind. Unknown history cannot certify completeness. Recommit under root-host exclusion before generation use. Canonical record and root retained obligations constrain agent candidate lists; timer enumeration is deny-only. Pre-flight 33, 80, 83. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationRegistryRows, TestPlan14RootTimerInventory, TestPlan14InventoryRequirementsRegistration, TestPlan14InventoryRequirementsUnknownAndRatchet, TestPlan14InventoryArtifactsCrash, TestPlan14InventoryRequirementsHistoryIdentity. Changed RequiredArtifacts and ExactRetirement root-runner witnesses await acceptance; remaining E2 launch/parked/containment integration is incomplete. 0ceac8b867717d959ccc6a66a44505a177ad56d09bd79d826c89a506eb8d00a8
internal/host/rootgeneration_requirements.go type generationRecord authority E2 E30: derive route and ordinary-instance state-container requirements from schema- and shape-validated canonical deployment records bound to retained installation incarnation; include record contents and ETag in both source reads and the certificate digest. Missing, mismatched, omitted or changed evidence refuses without retiring pins. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationProductionModeGate. Production fixed-path witness TestPlan14GenerationRequiredArtifactsRootRunner is runner-pending. Timer classes, parked artifacts, positive empty-installation evidence and running-tenant containment remain incomplete. ff50bc959af62afbd73b01e9640346f9c79d6b6578e03cb3a7d6bdff4d90fc28
-internal/host/rootgeneration_rows.go generationRegistryInventory authority E2 E37: retain independently initialized state-directory and timer requirements in the root installation transaction; preserve them through crash/reopen and rebind. Unknown history cannot certify completeness. Recommit under root-host exclusion before generation use. Canonical record and root retained obligations constrain agent candidate lists; timer enumeration is deny-only. Pre-flight 33, 80, 83. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationRegistryRows, TestPlan14RootTimerInventory, TestPlan14InventoryRequirementsRegistration, TestPlan14InventoryRequirementsUnknownAndRatchet, TestPlan14InventoryArtifactsCrash, TestPlan14InventoryRequirementsHistoryIdentity. Changed RequiredArtifacts and ExactRetirement root-runner witnesses await acceptance; remaining E2 launch/parked/containment integration is incomplete. registry:ClaimOwnership@2058,registry:ClaimOwnership@2437,registry:ClaimRowID@2490,registry:Host@2036,registry:Host@2415,registry:Name@2356,registry:Registration@2468,whole:ClaimOwnership@1334,whole:ClaimOwnership@1398,whole:ClaimOwnership@1412,whole:ClaimOwnership@1426,whole:ClaimOwnership@1935,whole:ClaimOwnership@2064,whole:ClaimOwnership@2082,whole:ClaimOwnership@2137,whole:ClaimOwnership@2386,whole:ClaimOwnership@2443,whole:ClaimOwnership@2461,whole:ClaimOwnership@2556,whole:ClaimOwnership@2584,whole:Route@1953,whole:Timer@2242,whole:Timer@2320 2878cd80e2828dcbfa77db4d9ad589d698cdbf61a3506ac2ecc13dae30970c5f
+internal/host/rootgeneration_rows.go generationRegistryInventory authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. registry:ClaimOwnership@2199,registry:ClaimOwnership@2578,registry:ClaimRowID@2631,registry:Host@2177,registry:Host@2556,registry:Name@2497,registry:Registration@2609,whole:ClaimOwnership@1475,whole:ClaimOwnership@1539,whole:ClaimOwnership@1553,whole:ClaimOwnership@1567,whole:ClaimOwnership@2076,whole:ClaimOwnership@2205,whole:ClaimOwnership@2223,whole:ClaimOwnership@2278,whole:ClaimOwnership@2527,whole:ClaimOwnership@2584,whole:ClaimOwnership@2602,whole:ClaimOwnership@2697,whole:ClaimOwnership@2725,whole:Route@2094,whole:Timer@2383,whole:Timer@2461 b4105502dc32425e8566a988cb45f11289b32c4ca4d5d78ab249c31f8d164ec7
@coverage internal/registry b8bc7442621fce6ae121cf8557f44289803ec85971ee30581bd4c85ffad480bd
internal/registry/remote.go serveRemote authority E2 Mixed declaration: E2 adds only the ArtifactHosts read operation; all existing dispatch and write behavior stays legacy. Original immutable legacy fingerprints retained. E32: census-only E2 completeness discovers the bounded union of route and timer hosts, including inactive orphan timers, through the service-user read-only parser. Unknown hosts refuse even with an empty root installation list; discovery supplies no authority and never removes a retained obligation. Pre-flight 33, 80, 82, 83, 89 and 90. Fixtures: TestArtifactHosts, TestRemoteReadOnlyWALRegistry, TestPlan14GenerationRegistryRows, TestPlan14GenerationRegistryProductionReader; extended TestPlan14GenerationRequiredArtifactsRootRunner remains runner-pending. file:Open@332,registry:CallbackError@1712,registry:CallbackFailed@1669,registry:Deadline@1295,registry:Deadline@1369,registry:Method@1022,registry:Method@1043,registry:Method@1070,registry:Method@1604,whole:remoteRequest@1426,whole:remoteRequest@835,whole:remoteRequest@922,whole:remoteResponse@1148,whole:remoteResponse@1194,whole:remoteResponse@1488,whole:remoteResponse@1786,whole:remoteResponse@1822,whole:remoteResponse@392,whole:remoteResponse@431 40254c291f7b1a89b28e39eb1e4ffb3aa5852fd00de076ced1d3f7c48885e6dd
internal/registry/artifacthosts.go Registry.ArtifactHosts authority E2 E32: census-only E2 completeness discovers the bounded union of route and timer hosts, including inactive orphan timers, through the service-user read-only parser. Unknown hosts refuse even with an empty root installation list; discovery supplies no authority and never removes a retained obligation. Pre-flight 33, 80, 82, 83, 89 and 90. Fixtures: TestArtifactHosts, TestRemoteReadOnlyWALRegistry, TestPlan14GenerationRegistryRows, TestPlan14GenerationRegistryProductionReader; extended TestPlan14GenerationRequiredArtifactsRootRunner remains runner-pending. file:Query@196,registry:Hosts@164,registry:remote@59,registry:remote@91,registry:sql@196,whole:remoteClient@61,whole:remoteClient@93 7310bda43df947e7b2d237dce83291dfe15596802d9db404f6ec0bb457cd6540
@@ -336,3 +336,10 @@ internal/hostguard/root.go Guard.CommitRootInventoryArtifacts authority E2 E37:
internal/hostguard/root.go type InstallationArtifacts authority E2 E37: retain independently initialized state-directory and timer requirements in the root installation transaction; preserve them through crash/reopen and rebind. Unknown history cannot certify completeness. Recommit under root-host exclusion before generation use. Canonical record and root retained obligations constrain agent candidate lists; timer enumeration is deny-only. Pre-flight 33, 80, 83. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationRegistryRows, TestPlan14RootTimerInventory, TestPlan14InventoryRequirementsRegistration, TestPlan14InventoryRequirementsUnknownAndRatchet, TestPlan14InventoryArtifactsCrash, TestPlan14InventoryRequirementsHistoryIdentity. Changed RequiredArtifacts and ExactRetirement root-runner witnesses await acceptance; remaining E2 launch/parked/containment integration is incomplete. fab0473af135dbb33e0f4cf9709a07b5026feedd43709c1a681c650c3ae82dd5
internal/hostguard/root.go type RootInstallation authority E2 E37: retain independently initialized state-directory and timer requirements in the root installation transaction; preserve them through crash/reopen and rebind. Unknown history cannot certify completeness. Recommit under root-host exclusion before generation use. Canonical record and root retained obligations constrain agent candidate lists; timer enumeration is deny-only. Pre-flight 33, 80, 83. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationRegistryRows, TestPlan14RootTimerInventory, TestPlan14InventoryRequirementsRegistration, TestPlan14InventoryRequirementsUnknownAndRatchet, TestPlan14InventoryArtifactsCrash, TestPlan14InventoryRequirementsHistoryIdentity. Changed RequiredArtifacts and ExactRetirement root-runner witnesses await acceptance; remaining E2 launch/parked/containment integration is incomplete. 33adc00085827994991a0ba71e06cee27157eb04a870506b72c1f16ec3962606
internal/hostguard/rootrebind.go Guard.RebindRootInventory authority E2 E37: retain independently initialized state-directory and timer requirements in the root installation transaction; preserve them through crash/reopen and rebind. Unknown history cannot certify completeness. Recommit under root-host exclusion before generation use. Canonical record and root retained obligations constrain agent candidate lists; timer enumeration is deny-only. Pre-flight 33, 80, 83. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationRegistryRows, TestPlan14RootTimerInventory, TestPlan14InventoryRequirementsRegistration, TestPlan14InventoryRequirementsUnknownAndRatchet, TestPlan14InventoryArtifactsCrash, TestPlan14InventoryRequirementsHistoryIdentity. Changed RequiredArtifacts and ExactRetirement root-runner witnesses await acceptance; remaining E2 launch/parked/containment integration is incomplete. 15f4c0b9159f193f9bad5215fe0224ba02276a9b58d036c98b4c4fe190856216
+internal/host/rootgeneration_parked.go generationPhysicalPayloads authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. 40f718df6477870f1c6e57b779a39bf7a27ae98b3e3afbcdaca32c16b76dc109
+internal/host/rootgeneration_parked.go generationReadRetirements authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. ba341f9665adbb204cf8ecf87a417b65022ce947291a4b99b1e8fa1bd16f3dec
+internal/host/rootgeneration_parked.go generationRetirementRecords authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. file:Read@707 02eca3d34294974c09e93199efb37a223e7615eaa32faffdafb982b5d4ba53e7
+internal/host/rootgeneration_parked.go generationRetirements authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. be9fd54e443994c423e0e315637b8cf05a361cbb32913b9bc9f891fbb65ad59f
+internal/host/rootgeneration_parked.go productionGenerationRetirements authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. file:Open@221,file:Open@306 fe83ff81bb9966f50de7fb3a049b8acd8e87f5d86346809090fc46b5c1e65189
+internal/hostbox/reconcile_adapter.go ReconcileAdapter.Retire authority C,E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. registry:ClaimIncarnation@1309,registry:ClaimIncarnation@322,registry:ClaimIncarnation@3392,registry:ClaimIncarnation@3414,registry:ClaimOwner@1366,registry:ClaimOwner@1434,registry:ClaimOwner@2190,registry:ClaimOwner@2596,registry:ClaimOwner@3472,registry:ClaimOwner@3488,registry:ClaimOwner@3726,registry:ClaimOwnership@147,registry:ClaimRowID@3506,registry:ClaimRowID@3749,registry:ClaimStamp@1338,registry:ClaimStamp@3438,registry:ClaimStamp@3454,registry:ClaimStamp@362,registry:Host@1257,registry:Host@2095,registry:Host@2641,registry:Host@2698,registry:Host@288,registry:Host@3311,registry:Host@3694,registry:Name@3711,registry:Registration@3528,registry:Registration@3770,whole:ClaimOwnership@151,whole:Route@1069,whole:Route@1152,whole:Route@231,whole:Timer@3377 bbee00fbe7b9ae791cc78fab1a06d79945b475b3a5e31d082ae37edd9c69f611
+internal/hostbox/retirement_timers.go retirementWithTimers authority C,E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. b4c4d4bcf2b3de23af676655cc4aa2901c7c0fd9a189d906fbffc81cd7d1bbf7