carlos / platform Public

WIP claim Plan 14 track E checkpoint 22 (not for merge): E2 staging-write gaps closed in manifest repair and moves-held staging

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev c6ba25b3b9bd3ce0e980466f4176587771968446 parent 9726694
8 files changed, +380 −32
  • internal/host/movesheldrelease_production.go +5 −4
  • internal/host/movesheldrelease_root.go +28 −6
  • internal/host/plan14_generation_held_test.go +136 −0
  • internal/provision/testdata/plan14-root-reads-e.tsv +20 −4
  • internal/release/adopt.go +12 −12
  • internal/release/materializeheld.go +31 −6
  • internal/release/plan14_generation_siblings_test.go +135 −0
  • internal/release/rootgenerationpins.go +13 −0

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.015s
?   	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	354.473s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	4.305s
ok  	github.com/carlosframework/platform/cmd/moveprobe	94.284s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.742s
ok  	github.com/carlosframework/platform/internal/activator	234.648s
ok  	github.com/carlosframework/platform/internal/agentsock	0.019s
ok  	github.com/carlosframework/platform/internal/alerting	0.007s
ok  	github.com/carlosframework/platform/internal/analytics	0.043s
ok  	github.com/carlosframework/platform/internal/appconfig	2.673s
ok  	github.com/carlosframework/platform/internal/apperr	0.212s
ok  	github.com/carlosframework/platform/internal/applog	0.185s
ok  	github.com/carlosframework/platform/internal/awsinv	0.009s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.043s
ok  	github.com/carlosframework/platform/internal/boxid	0.767s
?   	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.015s
ok  	github.com/carlosframework/platform/internal/caps	0.012s
ok  	github.com/carlosframework/platform/internal/certwarm	0.007s
ok  	github.com/carlosframework/platform/internal/cgroup	0.724s
ok  	github.com/carlosframework/platform/internal/changelog	0.400s
ok  	github.com/carlosframework/platform/internal/clamd	0.058s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	370.801s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	6.189s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.017s
ok  	github.com/carlosframework/platform/internal/dbverify	0.009s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.009s
ok  	github.com/carlosframework/platform/internal/docsite	0.014s
ok  	github.com/carlosframework/platform/internal/domains	2.328s
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	29.238s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.009s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.185s
ok  	github.com/carlosframework/platform/internal/gate	1.757s
ok  	github.com/carlosframework/platform/internal/genselect	0.048s
ok  	github.com/carlosframework/platform/internal/geo	0.030s
ok  	github.com/carlosframework/platform/internal/geoip	0.043s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.257s
ok  	github.com/carlosframework/platform/internal/host	141.916s
ok  	github.com/carlosframework/platform/internal/hostactors	0.851s
ok  	github.com/carlosframework/platform/internal/hostbox	3.245s
ok  	github.com/carlosframework/platform/internal/hostclaim	1.344s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.017s
ok  	github.com/carlosframework/platform/internal/hostexec	8.381s
ok  	github.com/carlosframework/platform/internal/hostfence	0.061s
ok  	github.com/carlosframework/platform/internal/hostfiles	11.632s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.037s
ok  	github.com/carlosframework/platform/internal/hostguard	1.048s
ok  	github.com/carlosframework/platform/internal/hostops	2.419s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.003s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.003s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.904s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.261s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.003s
ok  	github.com/carlosframework/platform/internal/infraview	0.009s
ok  	github.com/carlosframework/platform/internal/instancectl	1.906s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.971s
ok  	github.com/carlosframework/platform/internal/instancefs	0.009s
ok  	github.com/carlosframework/platform/internal/instancekey	0.003s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.009s
ok  	github.com/carlosframework/platform/internal/layout	1.838s
ok  	github.com/carlosframework/platform/internal/lease	9.579s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.014s
ok  	github.com/carlosframework/platform/internal/ledger	0.069s
ok  	github.com/carlosframework/platform/internal/mail	0.509s
ok  	github.com/carlosframework/platform/internal/maintenance	0.121s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	6.078s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.272s
ok  	github.com/carlosframework/platform/internal/metrics	0.021s
ok  	github.com/carlosframework/platform/internal/moves	25.365s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.256s
ok  	github.com/carlosframework/platform/internal/objstore	0.502s
ok  	github.com/carlosframework/platform/internal/ops	0.273s
ok  	github.com/carlosframework/platform/internal/park	0.548s
ok  	github.com/carlosframework/platform/internal/privatefile	0.015s
ok  	github.com/carlosframework/platform/internal/provision	20.475s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.014s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.204s
ok  	github.com/carlosframework/platform/internal/relay	7.301s
ok  	github.com/carlosframework/platform/internal/release	25.741s
ok  	github.com/carlosframework/platform/internal/rollout	4.984s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.015s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.010s
ok  	github.com/carlosframework/platform/internal/rootstate	0.034s
ok  	github.com/carlosframework/platform/internal/s3compat	0.009s
ok  	github.com/carlosframework/platform/internal/safefile	0.008s
ok  	github.com/carlosframework/platform/internal/schedule	2.811s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	22.136s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.957s
ok  	github.com/carlosframework/platform/internal/status	0.101s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.324s
ok  	github.com/carlosframework/platform/internal/svcrole	0.109s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.011s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.006s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.005s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.795s
=== 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 07:55:56 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE5645054EF52E, 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/movesheldrelease_production.go b/internal/host/movesheldrelease_production.go
index 9248578a5..f44b683e7 100644
--- a/internal/host/movesheldrelease_production.go
+++ b/internal/host/movesheldrelease_production.go
@@ -57,10 +57,11 @@ func WithProductionMovesHeldRelease(ctx context.Context, handler http.Handler, r
continues = a.Wake.Continue
}
root.HeldRelease = &movesHeldReleaseRoot{
- OpenRelease: func() (rootstate.Store, error) { return rootstate.Open(rootReleasePrivate, true, false) },
- Authenticate: root.Root.Stop.Authenticate,
- Actor: identity.Verify,
- Store: store,
+ generationPins: productionRootGenerationPins(),
+ OpenRelease: func() (rootstate.Store, error) { return rootstate.Open(rootReleasePrivate, true, false) },
+ Authenticate: root.Root.Stop.Authenticate,
+ Actor: identity.Verify,
+ Store: store,
Claims: func(ctx context.Context, host string) (hostclaim.Observed, error) {
return hostclaim.Read(ctx, store, host)
},
diff --git a/internal/host/movesheldrelease_root.go b/internal/host/movesheldrelease_root.go
index 110071df2..3aaaee554 100644
--- a/internal/host/movesheldrelease_root.go
+++ b/internal/host/movesheldrelease_root.go
@@ -60,10 +60,11 @@ type movesHeldReleaseRoot struct {
// Pub, Ratchet and Proof are §4.1's and D3's evidence, as for root's
// adoption pass: the release key, root's pointer digest ratchet and a
// fresh proof gate per request.
- Pub ed25519.PublicKey
- Ratchet func() *release.DigestRatchet
- Proof func() *release.ProofGate
- OptDir string
+ Pub ed25519.PublicKey
+ Ratchet func() *release.DigestRatchet
+ Proof func() *release.ProofGate
+ OptDir string
+ generationPins release.PinSource
// DataDir is the box's data directory: the render journals and the
// held-stage lock.
DataDir string
@@ -342,7 +343,10 @@ func (h *movesHeldReleaseRoot) stageRelease(ctx context.Context, actor hostbox.A
}
stage := h.stage
if stage == nil {
- stage = release.StageHeldVerified
+ pins := h.heldGenerationPins(ctx, q.Version)
+ stage = func(ctx context.Context, s release.Store, pub ed25519.PublicKey, ratchet *release.DigestRatchet, opt string, rt registry.Route, version string, proof *release.ProofGate) (*release.Manifest, error) {
+ return release.StageHeldVerifiedWithPins(ctx, s, pub, ratchet, opt, rt, version, proof, pins)
+ }
}
if err := withHeldStageLock(h.DataDir, q.Host, func() error {
_, err := stage(ctx, h.Store, h.Pub, h.Ratchet(), h.OptDir, heldReleaseRoute(q), q.Version, h.Proof())
@@ -503,7 +507,10 @@ func (h *movesHeldReleaseRoot) publishRelease(ctx context.Context, actor hostbox
}
materialize := h.materialize
if materialize == nil {
- materialize = release.MaterializeHeldUnder
+ pins := h.heldGenerationPins(ctx, q.Version)
+ materialize = func(ctx context.Context, s release.Store, pub ed25519.PublicKey, ratchet *release.DigestRatchet, opt string, rt registry.Route, version string, proof *release.ProofGate, work hostwrite.Work, guard func(func() error) error) (*release.Manifest, error) {
+ return release.MaterializeHeldUnderWithPins(ctx, s, pub, ratchet, opt, rt, version, proof, work, guard, pins)
+ }
}
lock := func(publish func() error) error { return withHeldStageLock(h.DataDir, q.Host, publish) }
if _, err := materialize(ctx, h.Store, h.Pub, h.Ratchet(), h.OptDir, heldReleaseRoute(q), q.Version, h.Proof(), checked, lock); err != nil {
@@ -513,6 +520,21 @@ func (h *movesHeldReleaseRoot) publishRelease(ctx context.Context, actor hostbox
return heldReleaseResponse{Schema: heldReleaseSchema, RequestID: q.RequestID, Outcome: heldReleaseOutcomePublished}, nil
}
+// Serve holds release exclusion until publication returns. Capture before the
+// leaf held-stage lock: retirement may acquire root-host and legacy-host locks.
+// Keep a failed read restrictive only for writes, so an unchanged artifact can
+// still be used and a wholly absent version tree can still be staged.
+func (h *movesHeldReleaseRoot) heldGenerationPins(ctx context.Context, version string) release.PinSource {
+ if h.generationPins == nil {
+ return nil
+ }
+ if _, err := os.Lstat(filepath.Join(h.OptDir, "versions", version)); errors.Is(err, os.ErrNotExist) {
+ return h.generationPins
+ }
+ pins, err := h.generationPins.StagedPins(ctx, h.OptDir)
+ return release.PinSourceFunc(func(context.Context, string) (release.StagedPaths, error) { return pins, err })
+}
+
type heldReleaseCheckedWork struct {
hostwrite.Work
check func() error
diff --git a/internal/host/plan14_generation_held_test.go b/internal/host/plan14_generation_held_test.go
new file mode 100644
index 000000000..3bac4574b
--- /dev/null
+++ b/internal/host/plan14_generation_held_test.go
@@ -0,0 +1,136 @@
+package host
+
+import (
+ "context"
+ "encoding/base64"
+ "net"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func generationHeldProductionSource(t *testing.T, f *hrFixture) generationPinSource {
+ t.Helper()
+ reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { reg.Close() })
+ a := &ClaimAssembly{Guard: hostguard.Guard{DataDir: f.data, Root: hostguard.NewRootAuthority(&rollbackDisk{dir: t.TempDir()})}, ManagedRoots: map[string]string{"opt": f.opt}}
+ ctx := context.WithValue(t.Context(), claimEntryKey{}, claimEntry{Assembly: a, Store: f.s})
+ root := ClaimRootWithLaunch{Root: ClaimRootHTTP{Stop: ClaimStopHTTP{Authenticate: func(context.Context, net.Conn) (StopPeer, error) { return hrPeer, nil }}}}
+ h, err := WithProductionMovesHeldRelease(ctx, root, reg, func(key string) string {
+ switch key {
+ case "CARLOS_MOVES_PHASE1":
+ return "1"
+ case "CARLOS_RELEASE_PUBKEY":
+ return base64.StdEncoding.EncodeToString(hsPub)
+ }
+ return ""
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ held, ok := h.(ClaimRootWithLaunch).HeldRelease.(*movesHeldReleaseRoot)
+ if !ok {
+ t.Fatal("held root was not assembled")
+ }
+ source, ok := held.generationPins.(generationPinSource)
+ if !ok {
+ t.Fatal("held root has no production generation source")
+ }
+ return source
+}
+
+func TestPlan14GenerationHeldStage(t *testing.T) {
+ for _, action := range []string{heldReleaseStage, heldReleasePublish} {
+ for _, mode := range []string{"pinned", "unknown", "legacy", "legacy-flag", "fresh"} {
+ t.Run(action+"/"+mode, func(t *testing.T) {
+ f := newHRFixture(t)
+ source := generationHeldProductionSource(t, f)
+ lock := &plan14ReleaseStore{}
+ f.root.OpenRelease = func() (rootstate.Store, error) { return lock, nil }
+ if action == heldReleaseStage {
+ f.set(func(f *hrFixture) {
+ f.claim.Op = hostclaim.Op{Kind: hostclaim.KindMoving, ID: p1ID, Exec: "ie-box", Participants: []string{"ie-box", "uk-box"}, Intent: map[string]string{"source": "ie-box", "target": "uk-box", "to": "uk"}}
+ f.rec.Move.State, f.rec.Move.LandingPhase, f.rec.Move.Inputs = provision.MoveReady, "", nil
+ f.row = nil
+ })
+ }
+ path := release.VersionPath(f.opt, "bab", "v7", "app")
+ if mode != "fresh" {
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ reads, opens := 0, 0
+ f.root.generationPins = release.PinSourceFunc(func(context.Context, string) (release.StagedPaths, error) {
+ if !lock.held {
+ t.Fatal("pin read outside release exclusion")
+ }
+ if _, err := os.Lstat(filepath.Join(f.data, ".moves", heldStageLockFile(p1Host))); !os.IsNotExist(err) {
+ t.Fatalf("pin reader runs inside held-stage leaf lock: %v", err)
+ }
+ reads++
+ if mode == "pinned" {
+ return release.StagedPaths{filepath.Dir(path): true}, nil
+ }
+ return nil, os.ErrPermission
+ })
+ if mode == "legacy" || mode == "legacy-flag" {
+ if mode == "legacy-flag" {
+ t.Setenv("CARLOS_HOST_CLAIM", "1")
+ } else {
+ t.Setenv("CARLOS_HOST_CLAIM", "")
+ }
+ source.legacy = func() (bool, error) { return true, nil }
+ source.open = func() (rootstate.Store, error) { opens++; return nil, os.ErrPermission }
+ f.root.generationPins = source
+ }
+ _, err := f.root.Serve(t.Context(), hrPeer, f.request(action))
+ if mode == "pinned" || mode == "unknown" {
+ if err == nil || reads == 0 {
+ t.Fatalf("held write bypassed reservations: %v reads=%d", err, reads)
+ }
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ t.Fatalf("protected path written: %v", err)
+ }
+ if _, err := os.Lstat(release.LiveLink(f.opt, p1Host)); !os.IsNotExist(err) {
+ t.Fatalf("held release published: %v", err)
+ }
+ if entries, err := os.ReadDir(f.receipts); err != nil || len(entries) != 0 {
+ t.Fatalf("refused release receipted: %v %v", entries, err)
+ }
+ } else {
+ if err != nil || opens != 0 || reads != 0 {
+ t.Fatalf("permitted held stage refused: %v opens=%d reads=%d", err, opens, reads)
+ }
+ if _, err := os.Stat(path); err != nil {
+ t.Fatal(err)
+ }
+ live, liveErr := release.LiveVersion(f.opt, p1Host)
+ if liveErr != nil {
+ t.Fatal(liveErr)
+ }
+ if action == heldReleasePublish && live != "v7" || action == heldReleaseStage && live != "" {
+ t.Fatalf("wrong live effect: %q", live)
+ }
+ entries, err := os.ReadDir(f.receipts)
+ if err != nil || action == heldReleasePublish && len(entries) != 1 || action == heldReleaseStage && len(entries) != 0 {
+ t.Fatalf("wrong receipt effect: %v %v", entries, err)
+ }
+ if _, err := os.Stat(release.StagedManifestPath(f.opt, "bab", "v7", "app")); err != nil {
+ t.Fatal(err)
+ }
+ }
+ })
+ }
+ }
+}
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv
index 61fc894f5..4ee88057d 100644
--- a/internal/provision/testdata/plan14-root-reads-e.tsv
+++ b/internal/provision/testdata/plan14-root-reads-e.tsv
@@ -28,7 +28,7 @@ internal/host/rootrollback.go settleRollbackAttempt authority B3,E1 Independent
internal/host/rootrollback_resume.go recoverRootRestarts authority B3 Recover every attempt independently, skip settled writes and remove leftover capabilities. file:Read@819 0eb3221e553183869f907660c74bae544d48da57f1e5592b92400377f5769a0e
internal/host/rootrollback_resume.go resumeRootRestart authority B3,J1,E1 Signed named-unit recovery is independent of release and health history. Root rechecks canonical source, unit, process, signed target and trusted live pointer; captures attempt authority before publication; verifies readiness and resumes without repeated effects. Fixtures: TestPlan14SignedActiveFixIgnoresHistory, TestPlan14SignedActiveFixAuthority, TestPlan14SignedFixSupersedesOlderAttempt, TestPlan14ActiveRecoveryProcessCrash, TestPlan14SignedRestartWithoutHealthHistory, TestPlan14CapturedRollbackWithoutHealthHistory, TestPlan14SignedRestartRequiresTrustedLiveLink. registry:Account@1419,registry:Account@2125,registry:App@1447,registry:App@2153,registry:Unit@1225,whole:Route@1227,whole:Route@1421,whole:Route@1449,whole:Route@2127,whole:Route@2155 f69e185f75b5758b439be86809c2a83dd300e2e1235879c4860b70cfb5cb39ba
internal/release/adopt.go PassFilter authority B3,E3,J1,E1,E2 Signed named-unit recovery is independent of release and health history. Root rechecks canonical source, unit, process, signed target and trusted live pointer; captures attempt authority before publication; verifies readiness and resumes without repeated effects. Fixtures: TestPlan14SignedActiveFixIgnoresHistory, TestPlan14SignedActiveFixAuthority, TestPlan14SignedFixSupersedesOlderAttempt, TestPlan14ActiveRecoveryProcessCrash, TestPlan14SignedRestartWithoutHealthHistory, TestPlan14CapturedRollbackWithoutHealthHistory, TestPlan14SignedRestartRequiresTrustedLiveLink. E7 applies merge ruling 4: unbound no-census routes use the legacy pass and restart; ordinary candidates make no binding store call. Fixtures: TestR2UnguardedERecoveryUpdate, TestR2UnguardedERecoveryIncidentRecipe, TestPlan14ERecoveryBindingRefusalKeepsPromotedFix. 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. registry:App@26278,registry:App@28735,registry:App@29227,registry:App@29464,registry:App@30871,registry:App@6073,registry:App@9098,registry:App@9239,registry:Hibernate@29110,registry:Host@1055,registry:Host@2746,registry:Host@2813,registry:Host@2909,registry:Host@29280,registry:Host@2944,registry:Host@30196,registry:Host@30924,registry:Host@690,registry:UpdateSettings@8976,whole:Route@1090,whole:Route@1251,whole:Route@2360,whole:Route@2384,whole:Route@2415,whole:Route@2472,whole:Route@2558,whole:Route@2580,whole:Route@26273,whole:Route@2680,whole:Route@28730,whole:Route@28988,whole:Route@29010,whole:Route@29057,whole:Route@29075,whole:Route@29085,whole:Route@29128,whole:Route@29149,whole:Route@29162,whole:Route@29222,whole:Route@29248,whole:Route@29322,whole:Route@29361,whole:Route@29377,whole:Route@29405,whole:Route@29459,whole:Route@29485,whole:Route@30238,whole:Route@30292,whole:Route@30308,whole:Route@30815,whole:Route@30866,whole:Route@30892,whole:Route@30966,whole:Route@31001,whole:Route@31017,whole:Route@3763,whole:Route@4410,whole:Route@579,whole:Route@5995,whole:Route@6017,whole:Route@6068,whole:Route@6094,whole:Route@617,whole:Route@675,whole:Route@793,whole:Route@817,whole:Route@8927,whole:Route@9041,whole:Route@9086,whole:Route@9234 fd45e6991e6b3d6695b4b7880e7202f83e87024c04df3d5382d5fd04600ad02d
-internal/release/adopt.go adoptHost authority B3,E1,E2,E3,E4,J1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. registry:App@10895,registry:App@11055,registry:App@12084,registry:App@13010,registry:App@14592,registry:App@19751,registry:App@25227,registry:App@3114,registry:App@32986,registry:App@6717,registry:App@8018,registry:App@8212,registry:Hibernate@33847,registry:Host@10775,registry:Host@10808,registry:Host@10908,registry:Host@11068,registry:Host@11170,registry:Host@11202,registry:Host@11267,registry:Host@11717,registry:Host@11850,registry:Host@12297,registry:Host@12406,registry:Host@12608,registry:Host@13100,registry:Host@13886,registry:Host@14364,registry:Host@14578,registry:Host@14920,registry:Host@15510,registry:Host@19241,registry:Host@19349,registry:Host@19719,registry:Host@20539,registry:Host@20697,registry:Host@2075,registry:Host@21159,registry:Host@21211,registry:Host@21358,registry:Host@21445,registry:Host@22551,registry:Host@2264,registry:Host@22795,registry:Host@2345,registry:Host@23455,registry:Host@23840,registry:Host@23981,registry:Host@25195,registry:Host@25322,registry:Host@26664,registry:Host@27314,registry:Host@30547,registry:Host@31165,registry:Host@3127,registry:Host@31490,registry:Host@31564,registry:Host@31811,registry:Host@31962,registry:Host@33158,registry:Host@33325,registry:Host@33432,registry:Host@33660,registry:Host@33803,registry:Host@34548,registry:Host@34674,registry:Host@34762,registry:Host@4428,registry:Host@4542,registry:Host@5505,registry:Host@5610,registry:Host@6829,registry:Host@6877,registry:Host@6994,registry:Host@7026,registry:Host@7904,registry:Host@8072,registry:Host@8198,registry:Host@8317,registry:Host@8795,registry:Host@9182,registry:Host@9408,registry:Host@9504,registry:Host@9595,registry:Kind@552,registry:Unit@31710,registry:Unit@577,whole:Route@10032,whole:Route@10890,whole:Route@11050,whole:Route@12074,whole:Route@13000,whole:Route@14560,whole:Route@15125,whole:Route@15486,whole:Route@16709,whole:Route@17012,whole:Route@17168,whole:Route@19498,whole:Route@19642,whole:Route@19741,whole:Route@2176,whole:Route@22280,whole:Route@22679,whole:Route@25217,whole:Route@30118,whole:Route@3109,whole:Route@33134,whole:Route@3554,whole:Route@4306,whole:Route@6712 5a5aba93df8af6fa4cda8fc5db5fc7ef9c0ef9f05fd400555eb68b5b06679dd7
+internal/release/adopt.go adoptHost authority B3,E1,E2,E3,E4,J1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 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@10906,registry:App@11066,registry:App@12095,registry:App@13021,registry:App@14603,registry:App@19762,registry:App@25238,registry:App@3114,registry:App@32997,registry:App@6728,registry:App@8029,registry:App@8223,registry:Hibernate@33858,registry:Host@10786,registry:Host@10819,registry:Host@10919,registry:Host@11079,registry:Host@11181,registry:Host@11213,registry:Host@11278,registry:Host@11728,registry:Host@11861,registry:Host@12308,registry:Host@12417,registry:Host@12619,registry:Host@13111,registry:Host@13897,registry:Host@14375,registry:Host@14589,registry:Host@14931,registry:Host@15521,registry:Host@19252,registry:Host@19360,registry:Host@19730,registry:Host@20550,registry:Host@20708,registry:Host@2075,registry:Host@21170,registry:Host@21222,registry:Host@21369,registry:Host@21456,registry:Host@22562,registry:Host@2264,registry:Host@22806,registry:Host@2345,registry:Host@23466,registry:Host@23851,registry:Host@23992,registry:Host@25206,registry:Host@25333,registry:Host@26675,registry:Host@27325,registry:Host@30558,registry:Host@31176,registry:Host@3127,registry:Host@31501,registry:Host@31575,registry:Host@31822,registry:Host@31973,registry:Host@33169,registry:Host@33336,registry:Host@33443,registry:Host@33671,registry:Host@33814,registry:Host@34559,registry:Host@34685,registry:Host@34773,registry:Host@4439,registry:Host@4553,registry:Host@5516,registry:Host@5621,registry:Host@6840,registry:Host@6888,registry:Host@7005,registry:Host@7037,registry:Host@7915,registry:Host@8083,registry:Host@8209,registry:Host@8328,registry:Host@8806,registry:Host@9193,registry:Host@9419,registry:Host@9515,registry:Host@9606,registry:Kind@552,registry:Unit@31721,registry:Unit@577,whole:Route@10043,whole:Route@10901,whole:Route@11061,whole:Route@12085,whole:Route@13011,whole:Route@14571,whole:Route@15136,whole:Route@15497,whole:Route@16720,whole:Route@17023,whole:Route@17179,whole:Route@19509,whole:Route@19653,whole:Route@19752,whole:Route@2176,whole:Route@22291,whole:Route@22690,whole:Route@25228,whole:Route@30129,whole:Route@3109,whole:Route@33145,whole:Route@3565,whole:Route@4317,whole:Route@6723 9a204e318feb65b13f863d5e3f3d8e82e72d63d90253e71b1f5bf1f4841e2daf
internal/release/rollback.go writeGroupRefusals authority E1 E1 group refusal writer persists root denial before its diagnostic projection; pending attempt remains restrictive. registry:App@801,whole:Route@783,whole:Route@882,whole:Route@888,whole:Route@952 301386c9d57f9f13db3c31a6cf3a26a44c7ca53ff8d98d910c10a7e8530b8795
internal/release/rootbake.go RootHistory.LoadBake authority E1 E1 strict root bake and app completeness revisions; backward clocks refuse and visible records are durably recommitted. file:Read@261 bd188ea6f2e76d277e1274043bf0f2a447391b1dc540aa400b36ad2573ccd84e
internal/release/roothistory.go RootHistory.read authority E1 Root per-binding refusal/program completeness and pending attempt state; strict decode and exact durable recommit before authority reads. E1 production integration is selected but incomplete. 4af99259d080d2b4c4ffee7cf2bc324e665cb07416031a495692421bd21878b4
@@ -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 a42baa886227f073282129c57bba480a530022650acc1b0bb070aaf60206bc89
-@coverage internal/release 371a2ab81da755cce5c4e39eb7e250f68f0d644b2441a061d30ce6f366fefee9
+@coverage internal/host fc312f5ee0e7725d5c4949b40a79a97d185cd52ba90232c79eb2f4ae6ba7c301
+@coverage internal/release d29fc60f16dd714f5b0521654666f33bf04165181d813583dc010bde6d863dd2
@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
@@ -216,7 +216,7 @@ internal/host/rootgenerationpins.go productionRootGenerationPins authority E2 E2
internal/host/rootgenerationpins.go type generationPinSource authority E2 E21: production census-only pin reader calls exact retirement before the unchanged mandatory completeness verifier. No census performs no generation open or retirement. Fixtures: TestPlan14GenerationRetirementCensusGate, TestPlan14GenerationProductionModeGate, TestPlan14GenerationClaimsOffRestageMatchesMain. edbc03de4156cf4292a92928e190749d50b317041827b9b83123798882ee4481
internal/host/versionsweep.go sweepStagedVersions authority E2,J3,J4 root-route 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. registry:GoodVersion@898,registry:GoodVersion@930,registry:PrevVersion@711,registry:PrevVersion@743,registry:Version@656,registry:Version@684,whole:Route@1231,whole:Route@435 e7fc68ed8c5ff2dc8a98daeb16c08f4d497b8016a3c459b6e4158721ba109843
internal/host/versionsweep.go sweepStagedVersionsPass 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. efeae77306262f47556aeccb382409933995f80c52f989e469e088e714e7d888
-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. 2d40e6c3ad116e9ab3a97530f8d83404440c7095d5f2868044c2bd22f0190562
+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
@@ -257,3 +257,19 @@ internal/host/rootgeneration_retirement.go generationRetirement.verify authority
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/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
+internal/host/movesheldrelease_root.go movesHeldReleaseRoot.publishRelease authority C,E1,E2 Replay and publication re-enter selected root guard and fresh canonical binding; receipt persistence remains inside guarded callback. 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@1913,whole:Route@3055,whole:Route@470 6120339dac1480f28db008346b97c039968cc41c46002bd275d4d5094584e6e9
+internal/host/movesheldrelease_root.go movesHeldReleaseRoot.heldGenerationPins 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. file:Lstat@167 7a7251c7b10d1c6e57b99af13fbd6b26fda4eeea18fd7991c55b1851ab708c9c
+internal/release/adopt.go syncSibling 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. file:ReadFile@106 164290bace043810d59eca300da194e42adda44d561f78d51bd8f3c393b5b80b
+internal/release/adopt.go repairSibling 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. ba8a95580aef7a2a175aca2b07e3054a41016c50390c9c20cc9976d98a38db98
+internal/release/adopt.go backfillManifest 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. file:Stat@127 38bfd70700579e5c2d8ca91827736f1a1e6d3f22123eb0d9cb26977780d6b872
+internal/release/adopt.go backfillManifestDigest 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. file:Stat@236 6fd2c782c7985758441ccaa239db7d29595170e18a393fd5920ca50155dd916f
+internal/release/materializeheld.go MaterializeHeldUnder 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. whole:Route@308 7dfdc046d6fc4ae5c882d545f590260afb42220f2328f2af87ece35a74793c84
+internal/release/materializeheld.go MaterializeHeldUnderWithPins 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@295,whole:Route@485 952eb2aeb094b2f869d326fba78202c7f55c61085a79f0fa799d2ac9416d5a58
+internal/release/materializeheld.go materializeHeld 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. whole:Route@412,whole:Route@515,whole:Route@606 fcff1930baf217f6913434f00714b80baddf60afa1f7f7bfdc39fa54cf852871
+internal/release/materializeheld.go StageHeldVerified 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. whole:Route@241 1371de1b7bca4799592793453afb1d35c14dd484e2017c178d2973a1c0c4956c
+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
diff --git a/internal/release/adopt.go b/internal/release/adopt.go
index 06ce5a821..9f2707f3b 100644
--- a/internal/release/adopt.go
+++ b/internal/release/adopt.go
@@ -1310,7 +1310,7 @@ func adoptHost(ctx context.Context, work hostwrite.Work, reg *registry.Registry,
// This call never fails the pass: an absent or unreadable sibling is the
// safe leave-alone state, so a repeated failure here is only a log line.
if cur != "" && ptr != nil {
- backfillManifestDigest(ctx, s, optDir, RouteAccount(rt), rt.App, cur, rt.Host, ptr.DigestOf(cur))
+ backfillManifestDigest(ctx, s, optDir, RouteAccount(rt), rt.App, cur, rt.Host, ptr.DigestOf(cur), cfg.stage)
}
// Instance update settings (settings spec §4): one decision per route.
// Everything below (bake for the decisions that take it, the proof gate,
@@ -2186,7 +2186,7 @@ func stageFrom(ctx context.Context, s Store, optDir, acct, app, ver, host string
art, hasBuild := m.Artifacts[adoptPlatform]
if staged {
if v := chk.verified[path]; v != "" && (!bound || hasBuild && strings.EqualFold(v, art.SHA256)) {
- sibling(optDir, acct, app, ver, host, raw)
+ sibling(optDir, acct, app, ver, host, raw, chk)
return m, nil
}
sum, herr := FileSHA256(path)
@@ -2197,7 +2197,7 @@ func stageFrom(ctx context.Context, s Store, optDir, acct, app, ver, host string
if chk.verified != nil {
chk.verified[path] = art.SHA256
}
- sibling(optDir, acct, app, ver, host, raw)
+ sibling(optDir, acct, app, ver, host, raw, chk)
return m, nil
}
// Different bytes under the same name. Overwrite only a copy nothing
@@ -2292,11 +2292,11 @@ func ensureStagedDigest(ctx context.Context, s Store, optDir, acct, app, ver, ho
// alone when it already is, written otherwise, including over a sibling that
// reads but is some other manifest's bytes (an older binary's re-marshal, or
// a release since replaced under the name). Best-effort, like repairSibling.
-func syncSibling(optDir, acct, app, ver, host string, raw []byte) {
+func syncSibling(optDir, acct, app, ver, host string, raw []byte, checks ...stageCheck) {
if cur, err := safefile.ReadFile(StagedManifestPath(optDir, acct, ver, app)); err == nil && bytes.Equal(cur, raw) {
return
}
- if err := StageManifest(optDir, acct, ver, app, raw); err != nil {
+ if err := stageGenerationSibling(optDir, acct, app, ver, raw, checks); err != nil {
fmt.Printf(" %s: stage manifest sibling for %s: %v\n", host, ver, err)
}
}
@@ -2305,11 +2305,11 @@ func syncSibling(optDir, acct, app, ver, host string, raw []byte) {
// sibling is absent or does not read: the back-fill for versions staged
// before the sibling existed, and the repair for one a different binary
// wrote. A sibling that reads is left alone, as before. Best-effort.
-func repairSibling(optDir, acct, app, ver, host string, raw []byte) {
+func repairSibling(optDir, acct, app, ver, host string, raw []byte, checks ...stageCheck) {
if sib, err := ReadStagedManifest(optDir, acct, ver, app); err == nil && sib != nil {
return
}
- if err := StageManifest(optDir, acct, ver, app, raw); err != nil {
+ if err := stageGenerationSibling(optDir, acct, app, ver, raw, checks); err != nil {
fmt.Printf(" %s: stage manifest sibling for %s: %v\n", host, ver, err)
}
}
@@ -2327,7 +2327,7 @@ func repairSibling(optDir, acct, app, ver, host string, raw []byte) {
// converge pass treats it as "unknown", never as "no capabilities"), so a
// repeated failure here is only ever worth a log line, host-prefixed like
// every other line adoptHost prints.
-func backfillManifest(ctx context.Context, s Store, optDir, acct, app, ver, host string) {
+func backfillManifest(ctx context.Context, s Store, optDir, acct, app, ver, host string, checks ...stageCheck) {
if _, err := os.Stat(StagedManifestPath(optDir, acct, ver, app)); err == nil {
return
}
@@ -2336,7 +2336,7 @@ func backfillManifest(ctx context.Context, s Store, optDir, acct, app, ver, host
fmt.Printf(" %s: back-fill manifest for %s: %v\n", host, ver, err)
return
}
- if err := StageManifest(optDir, acct, ver, app, raw); err != nil {
+ if err := stageGenerationSibling(optDir, acct, app, ver, raw, checks); err != nil {
fmt.Printf(" %s: stage manifest sibling for %s: %v\n", host, ver, err)
}
}
@@ -2347,9 +2347,9 @@ func backfillManifest(ctx context.Context, s Store, optDir, acct, app, ver, host
// (CheckManifestDigest) and the checked canonical bytes are what is written;
// a mismatch is a log line and no sibling, the same leave-alone state as any
// other failure here.
-func backfillManifestDigest(ctx context.Context, s Store, optDir, acct, app, ver, host, want string) {
+func backfillManifestDigest(ctx context.Context, s Store, optDir, acct, app, ver, host, want string, checks ...stageCheck) {
if want == "" {
- backfillManifest(ctx, s, optDir, acct, app, ver, host)
+ backfillManifest(ctx, s, optDir, acct, app, ver, host, checks...)
return
}
if _, err := os.Stat(StagedManifestPath(optDir, acct, ver, app)); err == nil {
@@ -2360,7 +2360,7 @@ func backfillManifestDigest(ctx context.Context, s Store, optDir, acct, app, ver
fmt.Printf(" %s: back-fill manifest for %s: %v\n", host, ver, err)
return
}
- if err := StageManifest(optDir, acct, ver, app, raw); err != nil {
+ if err := stageGenerationSibling(optDir, acct, app, ver, raw, checks); err != nil {
fmt.Printf(" %s: stage manifest sibling for %s: %v\n", host, ver, err)
}
}
diff --git a/internal/release/materializeheld.go b/internal/release/materializeheld.go
index 56124588e..9e98503d0 100644
--- a/internal/release/materializeheld.go
+++ b/internal/release/materializeheld.go
@@ -285,6 +285,12 @@ func MaterializeHeld(ctx context.Context, s Store, pub ed25519.PublicKey, r *Dig
// authorizes is ErrHeldNotAuthorized, before anything is downloaded. The
// claim's selection authorizes the write, not the build.
func MaterializeHeldUnder(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir string, rt registry.Route, version string, proof *ProofGate, work hostwrite.Work, guard func(publish func() error) error) (*Manifest, error) {
+ return MaterializeHeldUnderWithPins(ctx, s, pub, r, optDir, rt, version, proof, work, guard)
+}
+
+// MaterializeHeldUnderWithPins adds the root reservation check under the caller's
+// release exclusion. Agent callers retain the original entry point.
+func MaterializeHeldUnderWithPins(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir string, rt registry.Route, version string, proof *ProofGate, work hostwrite.Work, guard func(publish func() error) error, pins ...PinSource) (*Manifest, error) {
if err := heldShape(rt, version); err != nil {
return nil, err
}
@@ -299,12 +305,12 @@ func MaterializeHeldUnder(ctx context.Context, s Store, pub ed25519.PublicKey, r
}
return guard(publish)
})
- })
+ }, pins...)
}
// materializeHeld is MaterializeHeld and MaterializeHeldUnder after their
// admissions: D3's admit, the authorized stage (stageHeld), then publication.
-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) {
+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, pins ...PinSource) (*Manifest, error) {
if guard == nil {
return nil, errHeldNoGuard
}
@@ -316,7 +322,7 @@ func materializeHeld(ctx context.Context, s Store, pub ed25519.PublicKey, r *Dig
if err != nil {
return nil, err
}
- m, err := stageHeld(ctx, s, pub, r, optDir, rt, version)
+ m, err := stageHeld(ctx, s, pub, r, optDir, rt, version, pins...)
if err != nil {
return nil, err
}
@@ -362,6 +368,12 @@ func StageHeld(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRat
// which the agent answers by asking root to run this same stage (moves Task
// 7b, internal/host movesheldrelease_root.go). A nil proof is refused.
func StageHeldVerified(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir string, rt registry.Route, version string, proof *ProofGate) (*Manifest, error) {
+ return StageHeldVerifiedWithPins(ctx, s, pub, r, optDir, rt, version, proof)
+}
+
+// StageHeldVerifiedWithPins adds the root reservation check under the caller's
+// release exclusion. Agent callers retain the original entry point.
+func StageHeldVerifiedWithPins(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir string, rt registry.Route, version string, proof *ProofGate, pins ...PinSource) (*Manifest, error) {
if err := heldShape(rt, version); err != nil {
return nil, err
}
@@ -373,7 +385,7 @@ func StageHeldVerified(ctx context.Context, s Store, pub ed25519.PublicKey, r *D
if err != nil {
return nil, err
}
- m, err := stageHeld(ctx, s, pub, r, optDir, rt, version)
+ m, err := stageHeld(ctx, s, pub, r, optDir, rt, version, pins...)
if err != nil {
return nil, err
}
@@ -393,7 +405,7 @@ func StageHeldVerified(ctx context.Context, s Store, pub ed25519.PublicKey, r *D
// to the digest it returned, whose manifest is checked before any artifact
// byte is written (ensureStagedDigest). Authorization lives here, below
// every entry point, so no held path stages a build it has not authorized.
-func stageHeld(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir string, rt registry.Route, version string) (*Manifest, error) {
+func stageHeld(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRatchet, optDir string, rt registry.Route, version string, pins ...PinSource) (*Manifest, error) {
if err := heldShape(rt, version); err != nil {
return nil, err
}
@@ -401,7 +413,20 @@ func stageHeld(ctx context.Context, s Store, pub ed25519.PublicKey, r *DigestRat
if err != nil {
return nil, err
}
- m, err := materializeHeldStage(ctx, s, optDir, RouteAccount(rt), rt.App, version, rt.Host, want)
+ var m *Manifest
+ if len(pins) == 0 {
+ m, err = materializeHeldStage(ctx, s, optDir, RouteAccount(rt), rt.App, version, rt.Host, want)
+ } else {
+ check := stageCheck{beforeWrite: func(path string) error {
+ for _, source := range pins {
+ if err := (passConfig{generationPins: source}).generationWriteCheck(ctx, optDir, path); err != nil {
+ return err
+ }
+ }
+ return nil
+ }}
+ m, err = ensureStagedDigest(ctx, s, optDir, RouteAccount(rt), rt.App, version, rt.Host, want, check)
+ }
if err != nil {
if StageBlocked(err) {
return nil, fmt.Errorf("%w: %w", ErrStageBlocked, err)
diff --git a/internal/release/plan14_generation_siblings_test.go b/internal/release/plan14_generation_siblings_test.go
new file mode 100644
index 000000000..2a3704a60
--- /dev/null
+++ b/internal/release/plan14_generation_siblings_test.go
@@ -0,0 +1,135 @@
+package release
+
+import (
+ "bytes"
+ "context"
+ "os"
+ "testing"
+)
+
+func TestPlan14GenerationSiblingWrites(t *testing.T) {
+ for _, mode := range []string{"repair", "sync", "backfill", "bound-backfill"} {
+ type outcome struct {
+ sibling, live, version, previous, output string
+ restarts int
+ }
+ outcomes := map[string]outcome{}
+ for _, authority := range []string{"pinned", "unknown", "unreserved", "legacy"} {
+ t.Run(mode+"/"+authority, func(t *testing.T) {
+ f := newReuseFixture(t)
+ version := "v2"
+ if mode == "backfill" || mode == "bound-backfill" {
+ version = "v1"
+ if err := Promote(t.Context(), f.s, f.priv, "bab", "myapp", "canary/x", version); err != nil {
+ t.Fatal(err)
+ }
+ } else if err := StageArtifact(f.opt, "bab", version, "myapp", []byte("new v2")); err != nil {
+ t.Fatal(err)
+ }
+ _, raw, err := GetManifestRaw(t.Context(), f.s, "bab", "myapp", version)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ptr, err := Channel(t.Context(), f.s, f.pub, "bab", "myapp", "canary/x")
+ if err != nil {
+ t.Fatal(err)
+ }
+ ptr.Digest, ptr.HistoryDigests = "", nil
+ if mode == "sync" || mode == "bound-backfill" {
+ ptr.Digest = ManifestDigest(raw)
+ }
+ signed, err := signPointer(RootSigner(f.priv), ptr)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, etag, err := GetQualified(t.Context(), f.s, ChannelKey("bab", "myapp", "canary/x"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := f.s.Put(t.Context(), ChannelKey("bab", "myapp", "canary/x"), signed, etag); err != nil {
+ t.Fatal(err)
+ }
+ path := StagedManifestPath(f.opt, "bab", version, "myapp")
+ original := []byte("{\"stale\":true}")
+ if mode == "sync" {
+ if err := StageManifest(f.opt, "bab", version, "myapp", original); err != nil {
+ t.Fatal(err)
+ }
+ } else if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
+ t.Fatal(err)
+ }
+ var before os.FileInfo
+ if mode == "sync" {
+ before, err = os.Stat(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ g, _ := generationPinFixture(t, f.opt, "bab", "myapp", version)
+ var source PinSource = RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }}
+ if authority == "unknown" {
+ source = RootGenerationPins{Ledger: g}
+ }
+ if authority == "unreserved" {
+ source = PinSourceFunc(func(context.Context, string) (StagedPaths, error) { return StagedPaths{}, nil })
+ }
+ var opts []PassOption
+ if authority != "legacy" {
+ opts = append(opts, WithRootGenerationPins(source))
+ }
+ restarts := 0
+ var output bytes.Buffer
+ withCapturedStdout(t, &output, func() {
+ if err := Pass(t.Context(), f.reg, f.s, f.pub, f.opt, func(string) error { restarts++; return nil }, opts...); err != nil {
+ t.Fatal(err)
+ }
+ })
+ got, readErr := os.ReadFile(path)
+ live, err := LiveVersion(f.opt, "h1.example.com")
+ if err != nil {
+ t.Fatal(err)
+ }
+ row, err := f.reg.Get("h1.example.com")
+ if err != nil || row == nil {
+ t.Fatal(err)
+ }
+ outcomes[authority] = outcome{string(got), live, row.Version, row.PrevVersion, output.String(), restarts}
+ if authority == "pinned" || authority == "unknown" {
+ if mode == "sync" {
+ after, err := os.Stat(path)
+ if readErr != nil || err != nil || !bytes.Equal(got, original) || !os.SameFile(before, after) {
+ t.Fatalf("reserved sibling replaced: %q %v %v", got, readErr, err)
+ }
+ } else if !os.IsNotExist(readErr) {
+ t.Fatalf("reserved tree gained sibling: %q %v", got, readErr)
+ }
+ } else if readErr != nil || !bytes.Equal(got, raw) {
+ t.Fatalf("permitted sibling not written: %q %v", got, readErr)
+ }
+ })
+ }
+ if outcomes["unreserved"] != outcomes["legacy"] {
+ t.Fatalf("%s changed legacy effects: branch=%+v main=%+v", mode, outcomes["unreserved"], outcomes["legacy"])
+ }
+ }
+}
+
+func TestPlan14GenerationVerifiedSiblingWrites(t *testing.T) {
+ f := newReuseFixture(t)
+ if err := StageArtifact(f.opt, "bab", "v2", "myapp", []byte("new v2")); err != nil {
+ t.Fatal(err)
+ }
+ m, raw, err := GetManifestRaw(t.Context(), f.s, "bab", "myapp", "v2")
+ if err != nil {
+ t.Fatal(err)
+ }
+ path := VersionPath(f.opt, "bab", "v2", "myapp")
+ calls := 0
+ chk := stageCheck{verified: map[string]string{path: m.Artifacts[adoptPlatform].SHA256}, beforeWrite: func(string) error { calls++; return os.ErrPermission }}
+ if _, err := stageFrom(t.Context(), f.s, f.opt, "bab", "myapp", "v2", "h1.example.com", chk, true, m, raw, true); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := os.Stat(StagedManifestPath(f.opt, "bab", "v2", "myapp")); !os.IsNotExist(err) || calls != 1 {
+ t.Fatalf("verified cache bypassed sibling check: %v calls=%d", err, calls)
+ }
+}
diff --git a/internal/release/rootgenerationpins.go b/internal/release/rootgenerationpins.go
index 693866075..fab245112 100644
--- a/internal/release/rootgenerationpins.go
+++ b/internal/release/rootgenerationpins.go
@@ -82,6 +82,19 @@ func WithRootGenerationPins(source PinSource) PassOption {
return func(c *passConfig) { c.generationPins = source }
}
+// Sibling repair can change a reserved tree even when its binary already
+// matches. Check only when a write is needed, preserving the legacy no-op path.
+func stageGenerationSibling(optDir, acct, app, ver string, raw []byte, checks []stageCheck) error {
+ for _, check := range checks {
+ if check.beforeWrite != nil {
+ if err := check.beforeWrite(StagedManifestPath(optDir, acct, ver, app)); err != nil {
+ return err
+ }
+ }
+ }
+ return StageManifest(optDir, acct, ver, app, raw)
+}
+
func (c passConfig) generationWriteCheck(ctx context.Context, optDir, path string) error {
if c.generationPins == nil {
return nil