carlos / platform Public

WIP claim Plan 14 track E checkpoint 23 (not for merge): E2 versioned generation-completeness record and pin reader, retained-predecessor retirement after rebind, held-stage relay pinned

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev ebff275626a5d67bd1e15a7662148f11dbcc7a29 parent c6ba25b
12 files changed, +918 −14
  • internal/host/heldstage.go +1 −1
  • internal/host/plan14_generation_rebind_retirement_test.go +82 −0
  • internal/host/rootgeneration_retirement.go +7 −0
  • internal/provision/testdata/plan14-root-reads-e.tsv +24 −7
  • internal/release/materializeheld.go +14 −4
  • internal/release/plan14_generation_completeness_process_test.go +94 −0
  • internal/release/plan14_generation_completeness_root_test.go +122 −0
  • internal/release/plan14_generation_completeness_test.go +317 −0
  • internal/release/plan14_generation_legacy_relay_test.go +97 −0
  • internal/release/rootgenerationcompleteness.go +142 −0
  • internal/release/rootgenerationpins.go +15 −2
  • internal/release/rootgenerations.go +3 −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	346.952s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.164s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.259s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.727s
ok  	github.com/carlosframework/platform/internal/activator	233.511s
ok  	github.com/carlosframework/platform/internal/agentsock	0.016s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.020s
ok  	github.com/carlosframework/platform/internal/appconfig	2.353s
ok  	github.com/carlosframework/platform/internal/apperr	0.075s
ok  	github.com/carlosframework/platform/internal/applog	0.170s
ok  	github.com/carlosframework/platform/internal/awsinv	0.003s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	1.050s
?   	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.011s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.718s
ok  	github.com/carlosframework/platform/internal/changelog	0.309s
ok  	github.com/carlosframework/platform/internal/clamd	0.054s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	363.627s
?   	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.564s
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.004s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.130s
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.069s
?   	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.124s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.016s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.152s
ok  	github.com/carlosframework/platform/internal/host	132.499s
ok  	github.com/carlosframework/platform/internal/hostactors	0.424s
ok  	github.com/carlosframework/platform/internal/hostbox	2.151s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.491s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.006s
ok  	github.com/carlosframework/platform/internal/hostexec	4.906s
ok  	github.com/carlosframework/platform/internal/hostfence	0.036s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.510s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.012s
ok  	github.com/carlosframework/platform/internal/hostguard	1.005s
ok  	github.com/carlosframework/platform/internal/hostops	1.411s
?   	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.446s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.133s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.004s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	1.823s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.728s
ok  	github.com/carlosframework/platform/internal/instancefs	0.004s
ok  	github.com/carlosframework/platform/internal/instancekey	0.014s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.019s
ok  	github.com/carlosframework/platform/internal/layout	1.436s
ok  	github.com/carlosframework/platform/internal/lease	9.465s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.018s
ok  	github.com/carlosframework/platform/internal/ledger	0.011s
ok  	github.com/carlosframework/platform/internal/mail	0.162s
ok  	github.com/carlosframework/platform/internal/maintenance	0.076s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.507s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.081s
ok  	github.com/carlosframework/platform/internal/metrics	0.004s
ok  	github.com/carlosframework/platform/internal/moves	20.988s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.781s
ok  	github.com/carlosframework/platform/internal/objstore	0.177s
ok  	github.com/carlosframework/platform/internal/ops	0.157s
ok  	github.com/carlosframework/platform/internal/park	0.200s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	13.700s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	3.992s
ok  	github.com/carlosframework/platform/internal/relay	7.272s
ok  	github.com/carlosframework/platform/internal/release	21.605s
ok  	github.com/carlosframework/platform/internal/rollout	3.747s
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.025s
ok  	github.com/carlosframework/platform/internal/s3compat	0.006s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.283s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.639s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.176s
ok  	github.com/carlosframework/platform/internal/status	0.040s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.228s
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.004s
ok  	github.com/carlosframework/platform/internal/version	0.001s
ok  	github.com/carlosframework/platform/internal/vet	1.530s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.022s
=== RUN   TestT4StorageProbeMinio
2026/10/14 08:34:28 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE585F59B6209C, 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/host/heldstage.go b/internal/host/heldstage.go
index df347fb34..8adc33b80 100644
--- a/internal/host/heldstage.go
+++ b/internal/host/heldstage.go
@@ -202,7 +202,7 @@ func heldStageClaim(obs hostclaim.Observed, req release.HeldStageRequest) error
// adoption uses). Anything else is dropped before a byte is downloaded.
func heldStagePass(ctx context.Context, dataDir, optDir string, pub ed25519.PublicKey, r *release.DigestRatchet, reg *registry.Registry, store release.Store, box string) {
proof := release.NewRootProofGate(release.ProofPolicyPath, dataDir)
- if err := release.ServeHeldStageRequests(ctx, store, pub, r, optDir, dataDir, proof, heldStageAuthority(reg, store, box, dataDir)); err != nil {
+ if err := release.ServeHeldStageRequestsWithPins(ctx, store, pub, r, optDir, dataDir, proof, heldStageAuthority(reg, store, box, dataDir), productionRootGenerationPins()); err != nil {
log.Printf("adopt: held stage requests: %v", err)
}
retireEndedHeldRefusals(ctx, store, dataDir, box)
diff --git a/internal/host/plan14_generation_rebind_retirement_test.go b/internal/host/plan14_generation_rebind_retirement_test.go
new file mode 100644
index 000000000..f7a2169cd
--- /dev/null
+++ b/internal/host/plan14_generation_rebind_retirement_test.go
@@ -0,0 +1,82 @@
+package host
+
+import (
+ "context"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func TestPlan14GenerationRetirementRetainedRebind(t *testing.T) {
+ for _, change := range []string{"none", "open-writers", "unsettled", "wrong-installation", "wrong-incarnation", "wrong-stamp", "wrong-account", "wrong-enrollment"} {
+ t.Run(change, func(t *testing.T) {
+ f, disk, _ := generationLaunchFixture(t)
+ if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil {
+ t.Fatal(err)
+ }
+ r := readGenerationLaunch(t, disk)
+ if change != "unsettled" {
+ if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if change != "open-writers" {
+ if err := f.journal.CloseWriters(t.Context(), f.target); err != nil {
+ t.Fatal(err)
+ }
+ }
+ prior := hostguard.Captured{Incarnation: f.target.Incarnation, Stamp: f.target.Stamp}
+ in, err := f.root.Guard.Root.Installation(t.Context(), f.target.Host, prior)
+ if err != nil {
+ t.Fatal(err)
+ }
+ next := in
+ next.Stamp += 3
+ next.InventoryRevision++
+ f.claims.set(func(c *hostclaim.Claim) { c.Holder.Stamp = next.Stamp })
+ if err := f.root.Guard.RebindRootInventory(t.Context(), prior, in.InventoryRevision, next, []byte("root-verified-rebind"), func(*hostclaim.Claim) error { return nil }, func() error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ switch change {
+ case "wrong-installation":
+ r.Binding.Installation = "other"
+ case "wrong-incarnation":
+ r.Binding.Target.Incarnation = "other"
+ case "wrong-stamp":
+ r.Binding.Target.Stamp++
+ case "wrong-account":
+ r.Binding.Binding.Account = "other"
+ case "wrong-enrollment":
+ r.Binding.Target.Enrollment = "other"
+ }
+ calls := 0
+ verifier := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) {
+ calls++
+ return r.Binding.Digest, nil
+ }}
+ _, err = verifier.verify(t.Context(), r)
+ if change != "none" {
+ if err == nil || calls != 0 {
+ t.Fatalf("rebind erased old obligation: %v kernel=%d", err, calls)
+ }
+ return
+ }
+ if err != nil || calls != 1 {
+ t.Fatalf("retained predecessor refused: %v kernel=%d", err, calls)
+ }
+ if err := verifier.retire(t.Context(), &release.RootGenerations{Store: disk}, r); err != nil {
+ t.Fatal(err)
+ }
+ got := readGenerationLaunch(t, disk)
+ if got.Retirement == "" {
+ t.Fatal("exact old reservation retained after verified retirement")
+ }
+ current, err := f.root.Guard.Root.Installation(t.Context(), next.Host, hostguard.Captured{Incarnation: next.Incarnation, Stamp: next.Stamp})
+ if err != nil || current != next {
+ t.Fatal("retirement changed successor installation", err)
+ }
+ })
+ }
+}
diff --git a/internal/host/rootgeneration_retirement.go b/internal/host/rootgeneration_retirement.go
index 591dfc3fd..fe09845f9 100644
--- a/internal/host/rootgeneration_retirement.go
+++ b/internal/host/rootgeneration_retirement.go
@@ -29,6 +29,13 @@ func (v generationRetirement) closed(ctx context.Context, r release.GenerationRe
if err != nil {
return err
}
+ // Installation admits an older capture only when root retained it as
+ // retired. Rebind preserves this identity's account, enrollment and migration.
+ // Its old launch journal and kernel obligations still have to settle below.
+ if in.Incarnation != t.Incarnation || in.Stamp < t.Stamp {
+ return os.ErrPermission
+ }
+ in.Stamp = t.Stamp
if in.Host != b.Binding.Host || in.Account != b.Binding.Account || in.App != b.Binding.App || in.Box != t.Box || in.Enrollment != t.Enrollment || in.InventoryRevision == 0 || in.Record != b.Record || rootHistoryInstallation(in) != b.Installation {
return os.ErrPermission
}
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv
index 4ee88057d..59ad98868 100644
--- a/internal/provision/testdata/plan14-root-reads-e.tsv
+++ b/internal/provision/testdata/plan14-root-reads-e.tsv
@@ -54,8 +54,8 @@ 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 fc312f5ee0e7725d5c4949b40a79a97d185cd52ba90232c79eb2f4ae6ba7c301
-@coverage internal/release d29fc60f16dd714f5b0521654666f33bf04165181d813583dc010bde6d863dd2
+@coverage internal/host 8e78fc36197a3e57ec73d46b824b93a87c4a5031a6f0fd11d6e86f86d188260d
+@coverage internal/release 2bb546a86c1ab49b02e424eb0b366b46776857a1249baa2d6fb61ead4209d6bd
@coverage internal/rootstate 6338f74a6b2059cf77cf478bd6111b37b441eb9d65a309464c378359d4bdac05
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
internal/host/legacyrestart.go restartEffectFailed authority B3 E8 merge ruling 5: optional identity failures preserve the same restart outcome; no second restart; real not-up takes main rollback. Fixtures: TestPlan14ERecoveryRestartOutcome, TestPlan14ERecoveryBindingRefusalKeepsPromotedFix, TestPlan14ERecoveryNotUpPrecedesBinding. b68bb65884a14fe0cf78a65f56ded4ea41584bba99af9195827d1ea191b340a9
@@ -202,7 +202,7 @@ internal/release/rootgenerations.go RootGenerations.Check authority E2 E20: exac
internal/release/rootgenerations.go RootGenerations.Read authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. file:Read@206,file:Read@26 d77a3e6d7f8bdc2c16973910b3089aa7fb7c3c40b2d0b94f2ca7cdf32f9463b6
internal/release/rootgenerations.go RootGenerations.Reserve authority E2 E20: exact reservation closure precedes retirement verification; retained tombstones forbid replay and require sync recommit. Retired records release only their own pins; completeness remains a separate mandatory verifier, still unavailable in production. Fixtures: TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationRetirementRefusals, TestPlan14GenerationClosedReplayRefused, TestPlan14GenerationRetirementProcessCrash, TestPlan14GenerationRetirementKeepsIndependentReference, TestPlan14GenerationClaimsOffRestageMatchesMain. file:Read@409,file:Read@99 b6d4fe53dbd011641d69789f8f7926a6234fd819b9c455670b57e3385ffb0e58
internal/release/rootgenerations.go RootGenerations.Started authority E2 E20: exact reservation closure precedes retirement verification; retained tombstones forbid replay and require sync recommit. Retired records release only their own pins; completeness remains a separate mandatory verifier, still unavailable in production. Fixtures: TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationRetirementRefusals, TestPlan14GenerationClosedReplayRefused, TestPlan14GenerationRetirementProcessCrash, TestPlan14GenerationRetirementKeepsIndependentReference, TestPlan14GenerationClaimsOffRestageMatchesMain. file:Read@147 932a9a6497e8d64c7d9366c504acdb94f012f899a32b4ae4ce8080371627c2b7
-internal/release/rootgenerations.go RootGenerations.write authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. 72f8b4aa9f0487ee8b26843b5cbdc76aa5447d60535766cd4e4ce32f4baa5a2a
+internal/release/rootgenerations.go RootGenerations.write authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 3b7ee54d0f46efb1507f9a5f421b979047d8c0eb654ac5f31b1c3c42f67763c3
internal/release/rootgenerations.go const RootGenerationsDir authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. e235f24f29b1f8511349cae97fbbc3f3472b49fdc6781c781d5b6f93ff3d45ff
internal/release/rootgenerations.go generationKey authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. cdd7e0d8cdff508772ab54e421a4ab914dbb15ea5d97847f35a93544f0a9259a
internal/release/rootgenerations.go type GenerationBinding authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. 9bd76e1f6ef4d809df1a59719a1880215c910b0cab78449ad8cd79a678da401a
@@ -219,11 +219,11 @@ internal/host/versionsweep.go sweepStagedVersionsPass authority E2 E19: both des
internal/release/adopt.go stageFrom authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. 30efb69f9a7ee5aca5a1909d94d3c6669065c7880207430e7e0d2a23ae90a8b6
internal/release/adopt.go type passConfig authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. 1726cb5cee66d8aa0b04c44ad758b05a6aec239bb6a963ec2cfec13431ab765c
internal/release/adopt.go type stageCheck authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. b031290fdcac7eb495da71ffbe395b151504078d237ff279fe5fc2143e6b0f34
-internal/release/rootgenerationpins.go RootGenerationPins.StagedPins authority E2 E21: retirement and both destructive readers share strict root reservation enumeration and sync recommit. The independent completeness verifier remains mandatory and unset in production. Fixtures: TestPlan14GenerationRetirementCensusGate, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationClaimsOffRestageMatchesMain. 5405788a77184059126c7dfbd2f2558e633aac6c0fe9e30daf48a7ea82aeb9f4
+internal/release/rootgenerationpins.go RootGenerationPins.StagedPins authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. dff534cec0d4bea04750c4ea0774e71c0fbd3bf2d10dda7806df5bf797a68b36
internal/release/rootgenerationpins.go WithRootGenerationPins authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. befc123585ff625901fd8b0516b2fa73b51968db21a28fa0d64c417382d54f79
internal/release/rootgenerationpins.go generationFileIdentity authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. file:Lstat@96,file:Open@222 20a8f6c7c1c20fc2007dd89f2244b7797faede8d43f2206cd534a8033183b6ff
internal/release/rootgenerationpins.go passConfig.generationWriteCheck authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. file:Lstat@537 f32259e8d00c8a196c9326fdaec2de5727211bd61117a2e1620339f68791e6ee
-internal/release/rootgenerationpins.go type RootGenerationPins authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. c680202be840bd6e420cc0c9ae192f6f73d259498e564a1c4dbaf413e3e944a1
+internal/release/rootgenerationpins.go type RootGenerationPins authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. b10f7631296398380745d742ab966bbb1e39fe0768a054589fee7c1ac8c999e3
internal/host/rootinstallation.go rootInstallationHostApp authority E2,J1 E20: read the atomic root installation under the existing host exclusion without recursive lock acquisition; retain fresh claim, canonical record, signed artifact and final reservation checks. Fixtures: TestPlan14GenerationLaunchDoesNotReenterRootLock, TestPlan14GenerationLaunchRefusals, TestPlan14RootDispatchRejectsRegistryMutation. file:Read@488,registry:Account@1062,registry:App@1091 0bbe17aff8c82841ae47fb01e060e1e1a80865e635d4a5c95dc5a638d2754d1c
internal/hostguard/rootview.go RootAuthority.Installation authority E2,J1 E20: read the atomic root installation under the existing host exclusion without recursive lock acquisition; retain fresh claim, canonical record, signed artifact and final reservation checks. Fixtures: TestPlan14GenerationLaunchDoesNotReenterRootLock, TestPlan14GenerationLaunchRefusals, TestPlan14RootDispatchRejectsRegistryMutation. d819256332950ec26c1b541f8be21a6e8cea839708fcbd0f7b8367d118574f92
internal/release/rootgenerations.go RootGenerations.Retire authority E2 E20: exact reservation closure precedes retirement verification; retained tombstones forbid replay and require sync recommit. Retired records release only their own pins; completeness remains a separate mandatory verifier, still unavailable in production. Fixtures: TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationRetirementRefusals, TestPlan14GenerationClosedReplayRefused, TestPlan14GenerationRetirementProcessCrash, TestPlan14GenerationRetirementKeepsIndependentReference, TestPlan14GenerationClaimsOffRestageMatchesMain. file:Read@297 21bb00d11d6fd8474b4d0a556bd1af7e69dbf6bd75755daeb0f14481166f2a90
@@ -251,12 +251,12 @@ internal/host/rootgeneration_kernel_linux.go productionGenerationAbsence authori
internal/host/rootgeneration_kernel_linux.go type generationKernel authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. 5a8673c8cf4c7b2e660688c4cd0a1043271159cebb11835da2b00bdfef798f69
internal/host/rootgeneration_kernel_linux.go type generationMount authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. b4cd115a225b26a7906357404f8d04692a8d1b062662f80672280972049a8acb
internal/host/rootgeneration_kernel_other.go productionGenerationAbsence authority E2 E21: non-Linux production generation retirement refuses. Darwin, Windows and Linux ARM64 compile gates; no non-Linux retirement authority. de47d997019075326e1d5513d7576390d085566fd69edc35efd00f067fb1db24
-internal/host/rootgeneration_retirement.go generationRetirement.closed authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. e641da8055443fc03baae1a76548b76a863cd38f44c9c9b9f93e82f5b47749e0
+internal/host/rootgeneration_retirement.go generationRetirement.closed authority E2 E23: the root installation reader admits an old capture only from its retained retired set; restore that same-incarnation stamp solely to check the original installation identity. Require exact closed launch and independent kernel absence afterward. Fixtures: TestPlan14GenerationRetirementRetainedRebind, TestPlan14GenerationRetirementRootBinding. Running-tenant kernel acceptance remains pending. f871fd29171851a2300b68c947124d5d014ed97cdef45eef496645411df76107
internal/host/rootgeneration_retirement.go generationRetirement.retire authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. e4caafad8e42625aa21927f447f5f50a82f0890778635b74c3498c1d86efe1e2
internal/host/rootgeneration_retirement.go generationRetirement.verify authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. 7bf7540ad8bac41c5ea23d7bbf2f09c870b9bed78cf912fb9645c1a621289f70
internal/host/rootgeneration_retirement.go productionRetireGenerations authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. file:Open@292 fbe2431f88b9f2bd904f046855b63f619d20d5bdde6bcb0dc6d962388046a3c7
internal/host/rootgeneration_retirement.go type generationRetirement authority E2 E21: release then host exclusion; root installation and exact settled launch with irreversibly closed writers bind retirement before and after independent kernel verification. Production invokes it from the census-only pin source. Fixtures: TestPlan14GenerationRetirementRootBinding, TestPlan14GenerationRetirementAfterLostStarted, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Privileged production opener acceptance remains pending. 942978edbce75390dd3fcbfee5ec8b1d86102053f99da7915b7afdd89a503b8e
-internal/release/rootgenerationpins.go RootGenerations.Reservations authority E2 E21: retirement and both destructive readers share strict root reservation enumeration and sync recommit. The independent completeness verifier remains mandatory and unset in production. Fixtures: TestPlan14GenerationRetirementCensusGate, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationClaimsOffRestageMatchesMain. file:Read@412,file:Read@761 773119e7f716096834b4e384b02e0f7672a2a27703d6760e745088bf62dbf709
+internal/release/rootgenerationpins.go RootGenerations.Reservations authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. file:Read@541,file:Read@890 87f58596d5807a0e9eaed6dbd0cf5ba4e21a0906fba5959a2d974a68e9365d8c
internal/host/movesheldrelease_production.go WithProductionMovesHeldRelease authority B2,C,E1,E2 Root held-release constructor requires root guard and private release store, authenticates peer and reads root health history. E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. file:Open@1365,file:Read@1612 9981c473733dd5214ab5baf79fa4402e8112ec3ca9095d1ff742fcde10119d5f
internal/host/movesheldrelease_root.go type movesHeldReleaseRoot authority E2 E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. b42ce49488270ad07fd3f6b3607ed3f1d3d36ee23f60f55cc7c01c3fb8928233
internal/host/movesheldrelease_root.go movesHeldReleaseRoot.stageRelease authority E2 E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. whole:Route@1777 4ed5a64b1dd790e214c2e6b32b82804f40b40ceb1698325aec752d05eb9a6a19
@@ -273,3 +273,20 @@ internal/release/materializeheld.go StageHeldVerified authority C,E1,J1,E2 root-
internal/release/materializeheld.go StageHeldVerifiedWithPins authority E2 E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. registry:HoldOp@739,registry:Host@596,registry:Host@854,whole:Route@231,whole:Route@400,whole:Route@503,whole:Route@676 32ba175691ea06a141bdb4bb8bba8218354878927afd287704cc584cff6e0589
internal/release/materializeheld.go stageHeld authority C,E1,J1,E2 root-route;kept=C,E1;scope=protected-decision-only E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. registry:App@308,registry:App@467,registry:App@818,registry:Host@484,registry:Host@835,whole:Route@197,whole:Route@303,whole:Route@462,whole:Route@813 4a7f555f1f30e626d447cacf79541747e75d3ff90ea101c46c982f4e591f024b
internal/release/rootgenerationpins.go stageGenerationSibling authority E2 E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant. 572860919099c7d09c63350b88754950fa95dd67c21566fa9d84758928cf84b3
+internal/release/rootgenerationcompleteness.go GenerationEvidence.valid authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. c75845d27ace65dff1fee22b4e9d0174ae7e1769c1c967d9af08d39429169c24
+internal/release/rootgenerationcompleteness.go RootGenerations.Certify authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 0de395b25205d34db23511d4c2cbe6fabbbc1ac55efec15a548a65db57515a3e
+internal/release/rootgenerationcompleteness.go RootGenerations.Completeness authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. file:Read@183 01d502f20c896188722ead35de66dea745e34a4b37fa14736b4ae620c88621df
+internal/release/rootgenerationcompleteness.go RootGenerations.invalidateCompleteness authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 2ca32eef40bcb1ac6d44327615725b526b60aa45d88cfb90c0b55e18c3dcea55
+internal/release/rootgenerationcompleteness.go RootGenerations.writeCompleteness authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. d0a679b392995290ac8de99773aba981c27afef41d2ce4bf98fd0f8935a7aaa7
+internal/release/rootgenerationcompleteness.go const generationCompletenessKey authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 77774aa68284ad4f21124b957e1bf7562cb6e68b427bb8445a307f188a07dfc6
+internal/release/rootgenerationcompleteness.go const rootGenerationCompletenessLimit authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. e32dc27303a30317ac81a686cbcf258402163f33001dece4a1a5e09410c98766
+internal/release/rootgenerationcompleteness.go generationDigest authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 535b6a9639f825c40541a6b5002e8cf9452d5470fb09607f7473d115544dae61
+internal/release/rootgenerationcompleteness.go generationHash authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. bb7bd4705e38dbf222ad1791c7d833ddbf6a505dd023ac94cfbe58643a612def
+internal/release/rootgenerationcompleteness.go type GenerationCompleteness authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 7ef6167e481c979a5e6f5f14edf5428bfb5c7c96bded22e7487484ddc6c9167b
+internal/release/rootgenerationcompleteness.go type GenerationEvidence authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore is runner-pending. 7b56092e5735237424e046507ca24e3df8f4c3f32bab4822389c55703cff2366
+internal/release/materializeheld.go MaterializeHeld authority E2 E23: old held-stage relay forwards the root generation source before artifact writes and outside publication exclusion. Request fields retain their prior hint/authorization checks; only root pins authorize destructive staging on census boxes. No-census source yields empty pins. Fixtures: TestPlan14GenerationLegacyRelay, TestHeldStagePassIsANoOpWithoutRequests, TestHeldStagePassDropsAnUnauthorizedBuild, TestServeHeldStageRequestsRefusesStaleAndTraversal. Production source wiring is inspected; privileged completeness is not claimed. whole:Route@276 91f97547e14dccf71ee408979b47f641f8f3594775e7482f8437b7d1ed0509fc
+internal/release/materializeheld.go materializeHeldChecked authority E2 E23: old held-stage relay forwards the root generation source before artifact writes and outside publication exclusion. Request fields retain their prior hint/authorization checks; only root pins authorize destructive staging on census boxes. No-census source yields empty pins. Fixtures: TestPlan14GenerationLegacyRelay, TestHeldStagePassIsANoOpWithoutRequests, TestHeldStagePassDropsAnUnauthorizedBuild, TestServeHeldStageRequestsRefusesStaleAndTraversal. Production source wiring is inspected; privileged completeness is not claimed. whole:Route@272,whole:Route@368 8d751e8dc8e8d768a74b601f088dba9fe81bcfe32ec32025e30579f9f29a8f82
+internal/release/materializeheld.go ServeHeldStageRequests authority E2 E23: old held-stage relay forwards the root generation source before artifact writes and outside publication exclusion. Request fields retain their prior hint/authorization checks; only root pins authorize destructive staging on census boxes. No-census source yields empty pins. Fixtures: TestPlan14GenerationLegacyRelay, TestHeldStagePassIsANoOpWithoutRequests, TestHeldStagePassDropsAnUnauthorizedBuild, TestServeHeldStageRequestsRefusesStaleAndTraversal. Production source wiring is inspected; privileged completeness is not claimed. 3ada05f6519d2a3e9ba95bb8a1238098ee84994c1dc9f1e0f4b0dfed52c7c0aa
+internal/release/materializeheld.go ServeHeldStageRequestsWithPins authority E2 E23: old held-stage relay forwards the root generation source before artifact writes and outside publication exclusion. Request fields retain their prior hint/authorization checks; only root pins authorize destructive staging on census boxes. No-census source yields empty pins. Fixtures: TestPlan14GenerationLegacyRelay, TestHeldStagePassIsANoOpWithoutRequests, TestHeldStagePassDropsAnUnauthorizedBuild, TestServeHeldStageRequestsRefusesStaleAndTraversal. Production source wiring is inspected; privileged completeness is not claimed. file:ReadDir@429 edb9927c53af09311b4dd3e277a34d5eba7ab048d343b9b05e25f0b24a48e79d
+internal/release/materializeheld.go serveHeldStageRequest authority E2 E23: old held-stage relay forwards the root generation source before artifact writes and outside publication exclusion. Request fields retain their prior hint/authorization checks; only root pins authorize destructive staging on census boxes. No-census source yields empty pins. Fixtures: TestPlan14GenerationLegacyRelay, TestHeldStagePassIsANoOpWithoutRequests, TestHeldStagePassDropsAnUnauthorizedBuild, TestServeHeldStageRequestsRefusesStaleAndTraversal. Production source wiring is inspected; privileged completeness is not claimed. file:OpenFile@615,file:ReadAll@812,whole:Route@1676 bbda896335311f5e09a75542d82c57fb993da72cbc27875b74bda79bf0239280
+internal/host/heldstage.go heldStagePass authority E2 E23: old held-stage relay forwards the root generation source before artifact writes and outside publication exclusion. Request fields retain their prior hint/authorization checks; only root pins authorize destructive staging on census boxes. No-census source yields empty pins. Fixtures: TestPlan14GenerationLegacyRelay, TestHeldStagePassIsANoOpWithoutRequests, TestHeldStagePassDropsAnUnauthorizedBuild, TestServeHeldStageRequestsRefusesStaleAndTraversal. Production source wiring is inspected; privileged completeness is not claimed. 759f041ad504375f3000acccc10b5fba80aaa44ab574c3491d1df2cb49a35087
diff --git a/internal/release/materializeheld.go b/internal/release/materializeheld.go
index 9e98503d0..b389e3c97 100644
--- a/internal/release/materializeheld.go
+++ b/internal/release/materializeheld.go
@@ -258,10 +258,14 @@ func verifyHeldLive(ctx context.Context, s Store, optDir string, rt registry.Rou
// signed promotion record and no ratchet-admitted pointer of the app
// authorizes is ErrHeldNotAuthorized, before anything is downloaded.
func MaterializeHeld(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir string, rt registry.Route, version string, proof *ProofGate, guard func(publish func() error) error) (*Manifest, error) {
+ return materializeHeldChecked(ctx, s, pub, r, optDir, rt, version, proof, guard)
+}
+
+func materializeHeldChecked(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir string, rt registry.Route, version string, proof *ProofGate, guard func(publish func() error) error, pins ...PinSource) (*Manifest, error) {
if err := heldStageable(rt, version); err != nil {
return nil, err
}
- return materializeHeld(ctx, s, pub, r, optDir, rt, version, proof, guard)
+ return materializeHeld(ctx, s, pub, r, optDir, rt, version, proof, guard, pins...)
}
// MaterializeHeldUnder is MaterializeHeld for a held row under the host
@@ -717,6 +721,12 @@ const heldStageRequestMax = 4096
// and removal goes through safefile: no symlink is followed at any
// component, and only a regular file with one link is read.
func ServeHeldStageRequests(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir, dataDir string, proof *ProofGate, authorize func(req HeldStageRequest, then func() error) error) error {
+ return ServeHeldStageRequestsWithPins(ctx, s, pub, r, optDir, dataDir, proof, authorize)
+}
+
+// ServeHeldStageRequestsWithPins protects the old relay on census boxes, including
+// after claims are disabled. The caller holds release exclusion through the pass.
+func ServeHeldStageRequestsWithPins(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir, dataDir string, proof *ProofGate, authorize func(req HeldStageRequest, then func() error) error, pins ...PinSource) error {
reqDir := HeldStageDir(dataDir)
dir, err := safefile.OpenDir(reqDir)
if errors.Is(err, os.ErrNotExist) {
@@ -736,7 +746,7 @@ func ServeHeldStageRequests(ctx context.Context, s Store, pub ed25519.PublicKey,
if strings.HasPrefix(name, ".") || !strings.HasSuffix(name, ".json") {
continue // a writer's temp file, or not a request
}
- if err := serveHeldStageRequest(ctx, s, pub, r, optDir, dataDir, reqDir, name, proof, authorize); err != nil {
+ if err := serveHeldStageRequest(ctx, s, pub, r, optDir, dataDir, reqDir, name, proof, authorize, pins...); err != nil {
return err
}
}
@@ -745,7 +755,7 @@ func ServeHeldStageRequests(ctx context.Context, s Store, pub ed25519.PublicKey,
// serveHeldStageRequest serves one request file. It returns an error only
// when this build cannot do the work at all.
-func serveHeldStageRequest(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir, dataDir, reqDir, name string, proof *ProofGate, authorize func(HeldStageRequest, func() error) error) error {
+func serveHeldStageRequest(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir, dataDir, reqDir, name string, proof *ProofGate, authorize func(HeldStageRequest, func() error) error, pins ...PinSource) error {
drop := func(format string, args ...any) error {
fmt.Printf(" held stage request %q: "+format+"; removed\n", append([]any{name}, args...)...)
if err := safefile.Remove(filepath.Join(reqDir, name)); err != nil {
@@ -786,7 +796,7 @@ func serveHeldStageRequest(ctx context.Context, s Store, pub ed25519.PublicKey,
}
rt := registry.Route{Host: req.Host, Account: req.Account, App: req.App, HoldOp: req.Op}
guard := func(publish func() error) error { return authorize(req, publish) }
- if _, err := MaterializeHeld(ctx, s, pub, r, optDir, rt, req.Version, proof, guard); err != nil {
+ if _, err := materializeHeldChecked(ctx, s, pub, r, optDir, rt, req.Version, proof, guard, pins...); err != nil {
if errors.Is(err, ErrStaleHeldStage) {
return drop("refused at publication: %v", err)
}
diff --git a/internal/release/plan14_generation_completeness_process_test.go b/internal/release/plan14_generation_completeness_process_test.go
new file mode 100644
index 000000000..cc75f7270
--- /dev/null
+++ b/internal/release/plan14_generation_completeness_process_test.go
@@ -0,0 +1,94 @@
+package release
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "os/exec"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+type generationCertificateCrash struct {
+ rootstate.Store
+ phase string
+}
+
+func (s generationCertificateCrash) Write(k string, b []byte) error {
+ if err := s.Store.Write(k, b); err != nil {
+ return err
+ }
+ var c GenerationCompleteness
+ if k == generationCompletenessKey && json.Unmarshal(b, &c) == nil {
+ if s.phase == "invalidate" && !c.Complete {
+ os.Exit(74)
+ }
+ if s.phase == "certified" && c.Complete && c.Revision > 1 {
+ os.Exit(74)
+ }
+ }
+ return nil
+}
+
+func TestPlan14GenerationCompletenessProcessCrash(t *testing.T) {
+ if dir := os.Getenv("PLAN14_GENERATION_CERT_CHILD"); dir != "" {
+ phase := os.Getenv("PLAN14_GENERATION_CERT_PHASE")
+ disk := &healthyRetryStore{dir: dir}
+ g := &RootGenerations{Store: generationCertificateCrash{disk, phase}}
+ records, err := g.Reservations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, err = g.Certify(t.Context(), records, func(context.Context, []GenerationReservation) (GenerationEvidence, error) {
+ if phase == "verify" {
+ os.Exit(74)
+ }
+ return generationEvidence(), nil
+ })
+ t.Fatal("crash not reached", err)
+ }
+ for _, phase := range []string{"invalidate", "verify", "certified"} {
+ t.Run(phase, func(t *testing.T) {
+ g, s, b := generationFixture(t)
+ r, err := g.Reserve(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ records, err := g.Reservations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) {
+ return generationEvidence(), nil
+ }
+ if _, err = g.Certify(t.Context(), records, verify); err != nil {
+ t.Fatal(err)
+ }
+ cmd := exec.Command(os.Args[0], "-test.run=^TestPlan14GenerationCompletenessProcessCrash$", "-test.count=1")
+ cmd.Dir = t.TempDir()
+ cmd.Env = append(os.Environ(), "PLAN14_GENERATION_CERT_CHILD="+s.dir, "PLAN14_GENERATION_CERT_PHASE="+phase)
+ out, err := cmd.CombinedOutput()
+ if code, ok := err.(*exec.ExitError); !ok || code.ExitCode() != 74 {
+ t.Fatalf("wrong crash %v %s", err, out)
+ }
+ reopened := &RootGenerations{Store: s}
+ c, err := reopened.Completeness()
+ if err != nil || c.Complete != (phase == "certified") {
+ t.Fatalf("crash state %+v %v", c, err)
+ }
+ if got, err := reopened.Read(b); err != nil || got != r {
+ t.Fatal("crash lost pin", err)
+ }
+ // Even the positive record cannot replace fresh kernel reconciliation.
+ if _, err := reopened.Certify(t.Context(), records, func(context.Context, []GenerationReservation) (GenerationEvidence, error) {
+ return GenerationEvidence{}, os.ErrPermission
+ }); err == nil {
+ t.Fatal("crashed certificate supplied authority")
+ }
+ if _, err := reopened.Certify(t.Context(), records, verify); err != nil {
+ t.Fatal(err)
+ }
+ })
+ }
+}
diff --git a/internal/release/plan14_generation_completeness_root_test.go b/internal/release/plan14_generation_completeness_root_test.go
new file mode 100644
index 000000000..65b0e230c
--- /dev/null
+++ b/internal/release/plan14_generation_completeness_root_test.go
@@ -0,0 +1,122 @@
+package release
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func TestPlan14GenerationCompletenessCheckedRootStore(t *testing.T) {
+ parent := os.Getenv("PLAN14_GENERATION_ROOT_RUNNER")
+ if parent == "" {
+ t.Skip("requires a designated disposable root runner and its private scratch parent")
+ }
+ if os.Geteuid() != 0 {
+ t.Fatal("designated runner must execute as root")
+ }
+ // The runner supplies a root-owned private parent, outside production state.
+ check, err := rootstate.Open(parent, false, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ check.Close()
+ dir, err := os.MkdirTemp(parent, "generation-certificate-")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if err := os.RemoveAll(dir); err != nil {
+ t.Error(err)
+ }
+ })
+ state := filepath.Join(dir, "state")
+ disk, err := rootstate.Open(state, true, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer disk.Close()
+ g := &RootGenerations{Store: disk}
+ _, _, b := generationFixture(t)
+ if _, err := g.Reserve(b); err != nil {
+ t.Fatal(err)
+ }
+ records, err := g.Reservations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) {
+ return generationEvidence(), nil
+ }
+ if _, err := g.Certify(t.Context(), records, verify); err != nil {
+ t.Fatal(err)
+ }
+ file := filepath.Join(state, generationCompletenessKey)
+ for _, mode := range []os.FileMode{0666, 0620} {
+ if err := os.Chmod(file, mode); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := g.Completeness(); err == nil {
+ t.Fatal("accepted writable certificate")
+ }
+ if err := os.Chmod(file, 0600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := os.Chown(file, 65534, 65534); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := g.Completeness(); err == nil {
+ t.Fatal("accepted service-owned certificate")
+ }
+ if err := os.Chown(file, 0, 0); err != nil {
+ t.Fatal(err)
+ }
+ saved := filepath.Join(state, "saved")
+ if err := os.Rename(file, saved); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(saved, file); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := g.Completeness(); err == nil {
+ t.Fatal("accepted symlink certificate")
+ }
+ if err := os.Remove(file); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Rename(saved, file); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := g.Certify(t.Context(), records, verify); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Rename(state, state+".retained"); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir(state, 0700); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := g.Completeness(); err != nil {
+ t.Fatal("retained descriptor lost its checked store", err)
+ }
+ if _, err := os.Stat(filepath.Join(state, generationCompletenessKey)); !os.IsNotExist(err) {
+ t.Fatal("retained descriptor wrote to replacement directory", err)
+ }
+ replacement, err := rootstate.Open(state, false, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer replacement.Close()
+ if _, err := (&RootGenerations{Store: replacement}).Completeness(); !os.IsNotExist(err) {
+ t.Fatal("replacement directory inherited completeness", err)
+ }
+ if err := os.Chmod(state+".retained", 0777); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := g.Completeness(); err == nil {
+ t.Fatal("accepted writable store directory")
+ }
+}
diff --git a/internal/release/plan14_generation_completeness_test.go b/internal/release/plan14_generation_completeness_test.go
new file mode 100644
index 000000000..81703d53e
--- /dev/null
+++ b/internal/release/plan14_generation_completeness_test.go
@@ -0,0 +1,317 @@
+package release
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "github.com/carlosframework/platform/internal/rootstate"
+ "os"
+ "strings"
+ "testing"
+)
+
+func generationEvidence() GenerationEvidence {
+ return GenerationEvidence{Box: "box", Enrollment: "enrollment", Boot: "boot", Migration: 1, Installations: strings.Repeat("a", 64), Launches: strings.Repeat("b", 64), Kernel: strings.Repeat("c", 64)}
+}
+
+func TestPlan14GenerationCompleteness(t *testing.T) {
+ for _, attack := range []string{"none", "missing-verifier", "kernel-unknown", "empty-evidence", "wrong-box", "wrong-enrollment", "changed-reservations", "corrupt-marker", "sync"} {
+ t.Run(attack, func(t *testing.T) {
+ g, s, b := generationFixture(t)
+ if _, err := g.Reserve(b); err != nil {
+ t.Fatal(err)
+ }
+ records, err := g.Reservations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) {
+ e := generationEvidence()
+ switch attack {
+ case "kernel-unknown":
+ return e, os.ErrPermission
+ case "empty-evidence":
+ e.Kernel = ""
+ case "wrong-box":
+ e.Box = "other"
+ case "wrong-enrollment":
+ e.Enrollment = "other"
+ case "changed-reservations":
+ b.Attempt = "another"
+ if _, err := g.Reserve(b); err != nil {
+ t.Fatal(err)
+ }
+ case "sync":
+ s.fail = 2
+ }
+ return e, nil
+ }
+ if attack == "missing-verifier" {
+ verify = nil
+ }
+ if attack == "corrupt-marker" {
+ if err := s.Write(generationCompletenessKey, []byte(`{}`)); err != nil {
+ t.Fatal(err)
+ }
+ }
+ _, err = g.Certify(t.Context(), records, verify)
+ if attack != "none" {
+ if err == nil {
+ t.Fatal("incomplete sources certified")
+ }
+ return
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ c, err := g.Completeness()
+ if err != nil || !c.Complete || c.Revision != 1 {
+ t.Fatalf("certificate: %+v %v", c, err)
+ }
+ again, err := g.Certify(t.Context(), records, verify)
+ if err != nil || !again.Complete || again.Revision <= c.Revision || again.Reservations != c.Reservations {
+ t.Fatalf("recheck did not recertify: %+v %v", again, err)
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationCompletenessInvalidation(t *testing.T) {
+ for _, change := range []string{"reserve", "started", "closed", "retired"} {
+ t.Run(change, func(t *testing.T) {
+ g, _, b := generationFixture(t)
+ r, err := g.Reserve(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) {
+ return generationEvidence(), nil
+ }
+ records, err := g.Reservations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ first, err := g.Certify(t.Context(), records, verify)
+ if err != nil {
+ t.Fatal(err)
+ }
+ switch change {
+ case "reserve":
+ b.Attempt = "new"
+ _, err = g.Reserve(b)
+ case "started":
+ err = g.Started(r, GenerationProcess{PID: 100, TenantPID: 101, Start: "1", TenantStart: "2", Group: "system.slice/unit", Invocation: "invoke", GroupIdentity: b.Executable})
+ default:
+ err = g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { return nil }, func(context.Context, GenerationReservation) (string, error) {
+ if change == "closed" {
+ return "", os.ErrPermission
+ }
+ return strings.Repeat("d", 64), nil
+ })
+ }
+ if err != nil && change != "closed" {
+ t.Fatal(err)
+ }
+ c, err := g.Completeness()
+ if err != nil || c.Complete || c.Revision <= first.Revision {
+ t.Fatalf("mutation left certificate usable: %+v %v", c, err)
+ }
+ records, err = g.Reservations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ next, err := g.Certify(t.Context(), records, verify)
+ if err != nil || !next.Complete || next.Reservations == first.Reservations || next.Revision <= c.Revision {
+ t.Fatalf("new revision: %+v %v", next, err)
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationCompletenessDurability(t *testing.T) {
+ g, s, b := generationFixture(t)
+ r, err := g.Reserve(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ records, err := g.Reservations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) {
+ return generationEvidence(), nil
+ }
+ c, err := g.Certify(t.Context(), records, verify)
+ if err != nil {
+ t.Fatal(err)
+ }
+ s.fail = 1
+ if _, err = g.Completeness(); err == nil {
+ t.Fatal("visible certificate bypassed sync")
+ }
+ g.Store = &generationCompletenessFault{Store: s}
+ b.Attempt = "new"
+ if _, err = g.Reserve(b); err == nil {
+ t.Fatal("unsynced invalidation allowed reserve")
+ }
+ if _, err = g.Read(b); !errors.Is(err, os.ErrNotExist) {
+ t.Fatal("reservation written before invalidation", err)
+ }
+ raw, err := s.Read(generationCompletenessKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var pending GenerationCompleteness
+ if err = json.Unmarshal(raw, &pending); err != nil || pending.Complete {
+ t.Fatal("invalidation not visible", err)
+ }
+ reopened := &RootGenerations{Store: s}
+ if _, err = reopened.Reserve(b); err != nil {
+ t.Fatal(err)
+ }
+ if got, err := reopened.Read(r.Binding); err != nil || got != r {
+ t.Fatal("old obligation changed", err)
+ }
+ records, err = reopened.Reservations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ next, err := reopened.Certify(t.Context(), records, verify)
+ if err != nil || next.Revision <= c.Revision {
+ t.Fatal("recovery did not advance revision", err)
+ }
+}
+
+type generationCompletenessFault struct{ rootstate.Store }
+
+func (s *generationCompletenessFault) Write(k string, b []byte) error {
+ if err := s.Store.Write(k, b); err != nil {
+ return err
+ }
+ var c GenerationCompleteness
+ if k == generationCompletenessKey && json.Unmarshal(b, &c) == nil && !c.Complete {
+ return os.ErrPermission
+ }
+ return nil
+}
+
+func TestPlan14GenerationCompletenessPinConsumer(t *testing.T) {
+ f := newReuseFixture(t)
+ g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2")
+ fail := false
+ calls := 0
+ source := RootGenerationPins{Ledger: g, Reconcile: func(context.Context, []GenerationReservation) (GenerationEvidence, error) {
+ calls++
+ if fail {
+ return GenerationEvidence{}, os.ErrPermission
+ }
+ return generationEvidence(), nil
+ }}
+ pins, err := source.StagedPins(t.Context(), f.opt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if pinned, err := pins.Refers(r.Binding.Path); err != nil || !pinned {
+ t.Fatal("complete snapshot dropped pending pin", err)
+ }
+ c, err := g.Completeness()
+ if err != nil || !c.Complete || calls != 1 {
+ t.Fatalf("pin reader omitted certificate: %+v %v calls=%d", c, err, calls)
+ }
+ fail = true
+ if _, err := source.StagedPins(t.Context(), f.opt); err == nil {
+ t.Fatal("cached completeness survived unknown kernel")
+ }
+ c, err = g.Completeness()
+ if err != nil || c.Complete || calls != 2 {
+ t.Fatalf("failed scan retained certificate: %+v %v calls=%d", c, err, calls)
+ }
+ fail = false
+ if _, err := source.StagedPins(t.Context(), f.opt); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPlan14GenerationCompletenessCertificateSyncFailure(t *testing.T) {
+ g, s, b := generationFixture(t)
+ if _, err := g.Reserve(b); err != nil {
+ t.Fatal(err)
+ }
+ records, err := g.Reservations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ g.Store = generationPositiveCertificateFault{Store: s}
+ verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) {
+ return generationEvidence(), nil
+ }
+ if _, err := g.Certify(t.Context(), records, verify); err == nil {
+ t.Fatal("unsynced certificate returned success")
+ }
+ raw, err := s.Read(generationCompletenessKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var visible GenerationCompleteness
+ if err := json.Unmarshal(raw, &visible); err != nil || !visible.Complete {
+ t.Fatal("positive rename was not visible", err)
+ }
+ if _, err := g.Completeness(); err == nil {
+ t.Fatal("unsynced visible record bypassed durability retry")
+ }
+ g.Store = s
+ checked := false
+ if _, err := g.Certify(t.Context(), records, func(context.Context, []GenerationReservation) (GenerationEvidence, error) {
+ checked = true
+ return GenerationEvidence{}, os.ErrPermission
+ }); err == nil || !checked {
+ t.Fatal("reopen reused old evidence")
+ }
+ if _, err := g.Certify(t.Context(), records, verify); err != nil {
+ t.Fatal(err)
+ }
+}
+
+type generationPositiveCertificateFault struct{ rootstate.Store }
+
+func (s generationPositiveCertificateFault) Write(k string, b []byte) error {
+ if err := s.Store.Write(k, b); err != nil {
+ return err
+ }
+ var c GenerationCompleteness
+ if k == generationCompletenessKey && json.Unmarshal(b, &c) == nil && c.Complete {
+ return os.ErrPermission
+ }
+ return nil
+}
+
+func TestPlan14GenerationCompletenessUnknownRevisionAdvances(t *testing.T) {
+ g, _, b := generationFixture(t)
+ r, err := g.Reserve(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ records, err := g.Reservations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := g.Certify(t.Context(), records, func(context.Context, []GenerationReservation) (GenerationEvidence, error) {
+ return generationEvidence(), nil
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if err := g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { return nil }, func(context.Context, GenerationReservation) (string, error) { return "", os.ErrPermission }); err == nil {
+ t.Fatal("unknown kernel retired")
+ }
+ before, err := g.Completeness()
+ if err != nil || before.Complete {
+ t.Fatal("missing incomplete state", err)
+ }
+ if err := g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { return nil }, func(context.Context, GenerationReservation) (string, error) { return strings.Repeat("d", 64), nil }); err != nil {
+ t.Fatal(err)
+ }
+ after, err := g.Completeness()
+ if err != nil || after.Complete || after.Revision <= before.Revision {
+ t.Fatalf("retirement failed to advance unknown revision: before=%+v after=%+v err=%v", before, after, err)
+ }
+}
diff --git a/internal/release/plan14_generation_legacy_relay_test.go b/internal/release/plan14_generation_legacy_relay_test.go
new file mode 100644
index 000000000..7d06c9dbd
--- /dev/null
+++ b/internal/release/plan14_generation_legacy_relay_test.go
@@ -0,0 +1,97 @@
+package release
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestPlan14GenerationLegacyRelay(t *testing.T) {
+ for _, mode := range []string{"pinned", "unknown", "unreserved", "legacy", "fresh", "claims-enabled"} {
+ t.Run(mode, func(t *testing.T) {
+ t.Setenv("CARLOS_HOST_CLAIM", "")
+ ctx, s, opt := heldFixture(t)
+ data := t.TempDir()
+ request := writeHeldRequest(t, data, heldTestHost+".json", goodHeldRequest())
+ path := VersionPath(opt, "bab", "v1", "console")
+ if mode != "fresh" {
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ calls, published := 0, false
+ source := PinSourceFunc(func(context.Context, string) (StagedPaths, error) {
+ calls++
+ if published {
+ t.Fatal("pin read inside publication")
+ }
+ switch mode {
+ case "unknown", "fresh":
+ return nil, os.ErrPermission
+ case "pinned":
+ return StagedPaths{filepath.Dir(path): true}, nil
+ }
+ return StagedPaths{}, nil
+ })
+ auth := func(_ HeldStageRequest, then func() error) error {
+ if then != nil {
+ published = true
+ return then()
+ }
+ return nil
+ }
+ var err error
+ if mode == "claims-enabled" {
+ t.Setenv("CARLOS_HOST_CLAIM", "1")
+ }
+ if mode == "legacy" {
+ err = ServeHeldStageRequests(ctx, s, heldPub, nr(t), opt, data, offGate(t), auth)
+ } else {
+ err = ServeHeldStageRequestsWithPins(ctx, s, heldPub, nr(t), opt, data, offGate(t), auth, source)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ refused := mode == "pinned" || mode == "unknown" || mode == "claims-enabled"
+ if refused {
+ if published {
+ t.Fatal("refused stage published")
+ }
+ if _, err := os.Stat(request); err != nil {
+ t.Fatal("refused request not retained", err)
+ }
+ for _, p := range []string{path, LiveLink(opt, heldTestHost), filepath.Join(HeldStagedDir(data), heldTestHost+".json")} {
+ if _, err := os.Lstat(p); !os.IsNotExist(err) {
+ t.Fatal("refused stage wrote artifact/link/ack", p, err)
+ }
+ }
+ if mode != "claims-enabled" && calls == 0 {
+ t.Fatal("relay omitted pin reader")
+ }
+ if mode == "claims-enabled" && calls != 0 {
+ t.Fatal("claims refusal read pins")
+ }
+ } else {
+ if !published {
+ t.Fatal("permitted stage not published")
+ }
+ if _, err := os.Stat(request); !os.IsNotExist(err) {
+ t.Fatal("served request retained", err)
+ }
+ if raw, err := os.ReadFile(path); err != nil || string(raw) != "pinned build" {
+ t.Fatal("permitted artifact missing", err)
+ }
+ if v, err := LiveVersion(opt, heldTestHost); err != nil || v != "v1" {
+ t.Fatal("wrong live target", v, err)
+ }
+ if _, err := os.Stat(filepath.Join(HeldStagedDir(data), heldTestHost+".json")); err != nil {
+ t.Fatal("missing acknowledgement", err)
+ }
+ if mode == "fresh" && calls != 0 {
+ t.Fatal("fresh tree required completeness")
+ }
+ }
+ })
+ }
+}
diff --git a/internal/release/rootgenerationcompleteness.go b/internal/release/rootgenerationcompleteness.go
new file mode 100644
index 000000000..0600f3f0a
--- /dev/null
+++ b/internal/release/rootgenerationcompleteness.go
@@ -0,0 +1,142 @@
+package release
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "os"
+)
+
+const generationCompletenessKey = "completeness.json"
+
+type GenerationEvidence struct {
+ Box, Enrollment, Boot string
+ Migration uint64
+ Installations, Launches, Kernel string
+}
+
+type GenerationCompleteness struct {
+ Schema int
+ Revision uint64
+ Complete bool
+ Reservations string
+ Evidence GenerationEvidence
+}
+
+func generationDigest(v any) string {
+ raw, _ := json.Marshal(v)
+ sum := sha256.Sum256(raw)
+ return hex.EncodeToString(sum[:])
+}
+func generationHash(s string) bool {
+ b, err := hex.DecodeString(s)
+ return err == nil && len(b) == sha256.Size && hex.EncodeToString(b) == s
+}
+func (e GenerationEvidence) valid() bool {
+ return e.Box != "" && e.Enrollment != "" && e.Boot != "" && e.Migration > 0 && generationHash(e.Installations) && generationHash(e.Launches) && generationHash(e.Kernel)
+}
+
+// Completeness is evidence, not a cached permission. A destructive reader must
+// Certify again under release exclusion and retain that exclusion through use.
+func (g *RootGenerations) Completeness() (GenerationCompleteness, error) {
+ var c GenerationCompleteness
+ if g == nil || g.Store == nil {
+ return c, os.ErrPermission
+ }
+ raw, err := g.Store.Read(generationCompletenessKey)
+ if err != nil {
+ return c, err
+ }
+ if len(raw) > rootGenerationCompletenessLimit || strictCanonicalJSON(raw, &c) != nil || c.Schema != 1 || c.Revision == 0 || !generationHash(c.Reservations) || !c.Evidence.valid() {
+ return GenerationCompleteness{}, os.ErrPermission
+ }
+ if err := g.Store.Write(generationCompletenessKey, raw); err != nil {
+ return GenerationCompleteness{}, err
+ }
+ return c, nil
+}
+
+const rootGenerationCompletenessLimit = 4096
+
+func (g *RootGenerations) writeCompleteness(c GenerationCompleteness) error {
+ if c.Revision == 0 {
+ return os.ErrPermission
+ }
+ raw, err := json.Marshal(c)
+ if err != nil {
+ return err
+ }
+ if len(raw) > rootGenerationCompletenessLimit {
+ return os.ErrPermission
+ }
+ return g.Store.Write(generationCompletenessKey, raw)
+}
+
+// Invalidation precedes each reservation transition. A crash between the two
+// writes leaves the old obligation with an unusable certificate.
+func (g *RootGenerations) invalidateCompleteness() error {
+ c, err := g.Completeness()
+ if errors.Is(err, os.ErrNotExist) {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ c.Complete = false
+ c.Revision++
+ return g.writeCompleteness(c)
+}
+
+// Certify accepts only independently reconciled installation, launch and kernel
+// evidence. The verifier must not acquire release exclusion: its caller holds it.
+func (g *RootGenerations) Certify(ctx context.Context, expected []GenerationReservation, verify func(context.Context, []GenerationReservation) (GenerationEvidence, error)) (GenerationCompleteness, error) {
+ var zero GenerationCompleteness
+ if verify == nil {
+ return zero, os.ErrPermission
+ }
+ before, err := g.Reservations(ctx)
+ if err != nil {
+ return zero, err
+ }
+ digest := generationDigest(before)
+ if digest != generationDigest(expected) {
+ return zero, os.ErrPermission
+ }
+ // A failed reconciliation must not leave the previous positive record live.
+ if err := g.invalidateCompleteness(); err != nil {
+ return zero, err
+ }
+ e, err := verify(ctx, before)
+ if err != nil {
+ return zero, err
+ }
+ if !e.valid() {
+ return zero, os.ErrPermission
+ }
+ for _, r := range before {
+ if r.Binding.Target.Box != e.Box || r.Binding.Target.Enrollment != e.Enrollment {
+ return zero, os.ErrPermission
+ }
+ }
+ after, err := g.Reservations(ctx)
+ if err != nil {
+ return zero, err
+ }
+ if generationDigest(after) != digest {
+ return zero, os.ErrPermission
+ }
+ old, err := g.Completeness()
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ return zero, err
+ }
+ c := GenerationCompleteness{Schema: 1, Revision: old.Revision + 1, Complete: true, Reservations: digest, Evidence: e}
+ if err := ctx.Err(); err != nil {
+ return zero, err
+ }
+ if err := g.writeCompleteness(c); err != nil {
+ return zero, err
+ }
+ return c, nil
+}
diff --git a/internal/release/rootgenerationpins.go b/internal/release/rootgenerationpins.go
index fab245112..6c2617d5d 100644
--- a/internal/release/rootgenerationpins.go
+++ b/internal/release/rootgenerationpins.go
@@ -15,8 +15,9 @@ import (
// Verify must reconcile the records with root installation/launch inventory and
// kernel references. An empty directory alone never establishes completeness.
type RootGenerationPins struct {
- Ledger *RootGenerations
- Verify func(context.Context, []GenerationReservation) error
+ Ledger *RootGenerations
+ Verify func(context.Context, []GenerationReservation) error
+ Reconcile func(context.Context, []GenerationReservation) (GenerationEvidence, error)
}
func (s RootGenerationPins) StagedPins(ctx context.Context, optDir string) (StagedPaths, error) {
@@ -44,6 +45,12 @@ func (s RootGenerationPins) StagedPins(ctx context.Context, optDir string) (Stag
pins.add(b.Path)
pins.add(b.Tree)
}
+ if s.Reconcile != nil {
+ if _, err := s.Ledger.Certify(ctx, records, s.Reconcile); err != nil {
+ return nil, err
+ }
+ return pins, nil
+ }
if s.Verify == nil {
return nil, os.ErrPermission
}
@@ -146,6 +153,12 @@ func (g *RootGenerations) Reservations(ctx context.Context) ([]GenerationReserva
sort.Strings(names)
records := make([]GenerationReservation, 0, len(names))
for _, name := range names {
+ if name == generationCompletenessKey {
+ if _, err := g.Completeness(); err != nil {
+ return nil, err
+ }
+ continue
+ }
if err := ctx.Err(); err != nil {
return nil, err
}
diff --git a/internal/release/rootgenerations.go b/internal/release/rootgenerations.go
index 0a9112d08..3b1b07464 100644
--- a/internal/release/rootgenerations.go
+++ b/internal/release/rootgenerations.go
@@ -125,6 +125,9 @@ func (g *RootGenerations) write(r GenerationReservation) error {
if len(raw) > rootstate.MaxRecord {
return os.ErrPermission
}
+ if err := g.invalidateCompleteness(); err != nil {
+ return err
+ }
return g.Store.Write(generationKey(r.Binding), raw)
}