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>
8 files changed,
+380
−32
internal/host/movesheldrelease_production.go+5 −4internal/host/movesheldrelease_root.go+28 −6internal/host/plan14_generation_held_test.go+136 −0internal/provision/testdata/plan14-root-reads-e.tsv+20 −4internal/release/adopt.go+12 −12internal/release/materializeheld.go+31 −6internal/release/plan14_generation_siblings_test.go+135 −0internal/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 |