carlos / platform Public

WIP claim Plan 14 track E checkpoint 19 (not for merge): E2 reservations connected to restaging and the staged-version sweep

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 6a1a25e9c8d46a51166b28009a76a00027f4ed79 parent a96e55d
8 files changed, +611 −11
  • internal/host/agent.go +3 −2
  • internal/host/plan14_generation_consumers_test.go +161 −0
  • internal/host/rootgenerationpins.go +48 −0
  • internal/host/versionsweep.go +22 −3
  • internal/provision/testdata/plan14-root-reads-e.tsv +17 −4
  • internal/release/adopt.go +10 −2
  • internal/release/plan14_generation_pins_test.go +212 −0
  • internal/release/rootgenerationpins.go +138 −0

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	348.500s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.086s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.294s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.713s
ok  	github.com/carlosframework/platform/internal/activator	233.317s
ok  	github.com/carlosframework/platform/internal/agentsock	0.032s
ok  	github.com/carlosframework/platform/internal/alerting	0.013s
ok  	github.com/carlosframework/platform/internal/analytics	0.026s
ok  	github.com/carlosframework/platform/internal/appconfig	2.312s
ok  	github.com/carlosframework/platform/internal/apperr	0.081s
ok  	github.com/carlosframework/platform/internal/applog	0.135s
ok  	github.com/carlosframework/platform/internal/awsinv	0.011s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.010s
ok  	github.com/carlosframework/platform/internal/boxid	0.874s
?   	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.013s
ok  	github.com/carlosframework/platform/internal/caps	0.014s
ok  	github.com/carlosframework/platform/internal/certwarm	0.006s
ok  	github.com/carlosframework/platform/internal/cgroup	0.701s
ok  	github.com/carlosframework/platform/internal/changelog	0.307s
ok  	github.com/carlosframework/platform/internal/clamd	0.062s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	367.840s
?   	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.307s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.004s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.098s
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	25.936s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.121s
ok  	github.com/carlosframework/platform/internal/gate	1.654s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.015s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.153s
ok  	github.com/carlosframework/platform/internal/host	130.892s
ok  	github.com/carlosframework/platform/internal/hostactors	0.327s
ok  	github.com/carlosframework/platform/internal/hostbox	2.096s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.488s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.015s
ok  	github.com/carlosframework/platform/internal/hostexec	4.864s
ok  	github.com/carlosframework/platform/internal/hostfence	0.031s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.565s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.015s
ok  	github.com/carlosframework/platform/internal/hostguard	1.010s
ok  	github.com/carlosframework/platform/internal/hostops	1.408s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.015s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.016s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.451s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.150s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.016s
ok  	github.com/carlosframework/platform/internal/infraview	0.015s
ok  	github.com/carlosframework/platform/internal/instancectl	1.813s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.742s
ok  	github.com/carlosframework/platform/internal/instancefs	0.021s
ok  	github.com/carlosframework/platform/internal/instancekey	0.021s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.022s
ok  	github.com/carlosframework/platform/internal/layout	1.415s
ok  	github.com/carlosframework/platform/internal/lease	9.444s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.021s
ok  	github.com/carlosframework/platform/internal/ledger	0.010s
ok  	github.com/carlosframework/platform/internal/mail	0.166s
ok  	github.com/carlosframework/platform/internal/maintenance	0.080s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.537s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.067s
ok  	github.com/carlosframework/platform/internal/metrics	0.004s
ok  	github.com/carlosframework/platform/internal/moves	21.027s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.751s
ok  	github.com/carlosframework/platform/internal/objstore	0.158s
ok  	github.com/carlosframework/platform/internal/ops	0.157s
ok  	github.com/carlosframework/platform/internal/park	0.189s
ok  	github.com/carlosframework/platform/internal/privatefile	0.005s
ok  	github.com/carlosframework/platform/internal/provision	13.421s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.006s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.107s
ok  	github.com/carlosframework/platform/internal/relay	7.217s
ok  	github.com/carlosframework/platform/internal/release	20.633s
ok  	github.com/carlosframework/platform/internal/rollout	3.767s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.009s
ok  	github.com/carlosframework/platform/internal/rootstate	0.025s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.003s
ok  	github.com/carlosframework/platform/internal/schedule	2.306s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.450s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.846s
ok  	github.com/carlosframework/platform/internal/status	0.041s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.157s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.003s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.533s
=== 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.023s
=== RUN   TestT4StorageProbeMinio
2026/10/14 06:28:40 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE5182154F2498, 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/agent.go b/internal/host/agent.go
index 6ba845156..52ce55f5b 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -4373,8 +4373,9 @@ func runAdoptUnderRootRelease(args []string, buildClaims ClaimFactory, ensureUni
// minute per loop, and walking the version store that often to free a
// day's worth of shipping is work for no gain. -config-only and
// -restart-only are narrow triggers that must do their one job.
+ generationPins := productionRootGenerationPins()
if passLockErr == nil && !*stageOnly && !*configOnly && !*restartOnly {
- sweepStagedVersionsPass(reg)
+ sweepStagedVersionsPass(reg, generationPins)
}
ctx := context.Background()
@@ -4538,7 +4539,7 @@ func runAdoptUnderRootRelease(args []string, buildClaims ClaimFactory, ensureUni
linkRootHistory(history, restartChecks, release.OptDir(), os.Getenv)
}
recovery := productionNamedUnitRecovery(store, pub, release.OptDir(), claims.Writes)
- passOpts := []release.PassOption{release.WithRootHistory(history), recovery.passOption(reg, restartChecks, os.Getenv), release.WithCapabilities(caps.Ops{}), release.BakeDir(dataDir()), release.WithRestartAttemptSettled(restartChecks.finish),
+ passOpts := []release.PassOption{release.WithRootGenerationPins(generationPins), release.WithRootHistory(history), recovery.passOption(reg, restartChecks, os.Getenv), release.WithCapabilities(caps.Ops{}), release.BakeDir(dataDir()), release.WithRestartAttemptSettled(restartChecks.finish),
release.WithRunningVersion(restartChecks.runningVersion),
release.WithSelectedRestarts(
rootSelectedRestart(reg, systemdUnits{}, release.OptDir(), os.Getenv, settled, restartChecks, false),
diff --git a/internal/host/plan14_generation_consumers_test.go b/internal/host/plan14_generation_consumers_test.go
new file mode 100644
index 000000000..ab490fb85
--- /dev/null
+++ b/internal/host/plan14_generation_consumers_test.go
@@ -0,0 +1,161 @@
+package host
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func TestPlan14GenerationDeletionCannotUnpin(t *testing.T) {
+ for _, attack := range []string{"delete", "empty", "rename", "missing-completeness", "deleted-executable", "renamed-parent", "registry-deleted", "wrong-root", "sync-failure"} {
+ t.Run(attack, func(t *testing.T) {
+ f, disk, _ := generationLaunchFixture(t)
+ permit, err := f.root.generations.reserve(t.Context(), f.root, f.peer, f.req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ b := permit.reservation.Binding
+ opt := f.root.generations.optDir
+ stageSweepVersion(t, opt, "bap", "v2", "other", 48*time.Hour)
+ stageSweepVersion(t, opt, "bap", "v3", "pending", 48*time.Hour)
+ pending := b
+ pending.Attempt, pending.Invocation = "pending-v3", "pending-v3"
+ pending.Binding.Account, pending.Binding.App, pending.Version = "bap", "pending", "v3"
+ pending.Path = release.VersionPath(opt, "bap", "v3", "pending")
+ pending.Tree = filepath.Dir(pending.Path)
+ for path, dst := range map[string]*rootstate.FileIdentity{pending.Path: &pending.Executable, pending.Tree: &pending.TreeIdentity} {
+ file, err := os.Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ *dst, err = rootstate.Identity(file)
+ file.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ if _, err := permit.ledger.Reserve(pending); err != nil {
+ t.Fatal(err)
+ }
+ when := time.Now().Add(-48 * time.Hour)
+ if err := os.Chtimes(b.Path, when, when); err != nil {
+ t.Fatal(err)
+ }
+ opened, err := os.Open(b.Path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer opened.Close()
+ before, err := opened.Stat()
+ if err != nil {
+ t.Fatal(err)
+ }
+ dir := t.TempDir()
+ path := release.GenerationsPath(dir)
+ if err := release.WriteGenerations(path, release.Generations{Paths: map[string][]string{b.Binding.Host: {b.Path}}}); err != nil {
+ t.Fatal(err)
+ }
+ source := generationPinSource{open: func() (rootstate.Store, error) { return disk, nil }, verify: func(context.Context, []release.GenerationReservation) error { return nil }}
+ switch attack {
+ case "empty":
+ if err := release.WriteGenerations(path, release.Generations{}); err != nil {
+ t.Fatal(err)
+ }
+ case "rename":
+ if err := os.Rename(filepath.Dir(path), filepath.Dir(path)+".old"); err != nil {
+ t.Fatal(err)
+ }
+ default:
+ if err := os.Remove(path); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Remove(release.GenerationsSeqPath(path)); err != nil {
+ t.Fatal(err)
+ }
+ }
+ switch attack {
+ case "missing-completeness":
+ source.verify = nil
+ case "deleted-executable":
+ if err := os.Remove(b.Path); err != nil {
+ t.Fatal(err)
+ }
+ case "renamed-parent":
+ if err := os.Rename(b.Tree, b.Tree+".old"); err != nil {
+ t.Fatal(err)
+ }
+ case "wrong-root":
+ opt = t.TempDir()
+ stageSweepVersion(t, opt, "bab", "v1", "shop", 48*time.Hour)
+ case "sync-failure":
+ disk.fail = 1
+ }
+ // No registry row or live link carries the root reservation.
+ sweepStagedVersions(fakeLister{}, opt, path, 6*time.Hour, time.Now(), source)
+ if attack == "renamed-parent" {
+ if _, err := os.Stat(b.Tree + ".old"); err != nil {
+ t.Fatalf("renamed reserved tree removed: %v", err)
+ }
+ } else if !staged(t, opt, "v1") {
+ t.Fatal("sweep removed reserved version tree")
+ }
+ if attack != "wrong-root" && !staged(t, opt, "v3") {
+ t.Fatal("pending V3 lost its independent pin")
+ }
+ after, err := opened.Stat()
+ if err != nil || !os.SameFile(before, after) {
+ t.Fatalf("held inode changed: %v", err)
+ }
+ if attack == "delete" || attack == "empty" || attack == "rename" || attack == "registry-deleted" {
+ if staged(t, opt, "v2") {
+ t.Fatal("complete fixture could not sweep unrelated version")
+ }
+ } else if attack != "wrong-root" && !staged(t, opt, "v2") {
+ t.Fatal("incomplete snapshot permitted any deletion")
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationConsumerMode(t *testing.T) {
+ for _, legacy := range []bool{true, false} {
+ calls := 0
+ source := generationPinSource{legacy: func() (bool, error) { return legacy, nil }, open: func() (rootstate.Store, error) { calls++; return nil, os.ErrPermission }}
+ _, err := source.StagedPins(t.Context(), t.TempDir())
+ if legacy && (err != nil || calls != 0) {
+ t.Fatalf("legacy read root generations: %v calls %d", err, calls)
+ }
+ if !legacy && (err == nil || calls != 1) {
+ t.Fatalf("protected fallback: %v calls %d", err, calls)
+ }
+ }
+ source := generationPinSource{legacy: func() (bool, error) { return true, os.ErrPermission }}
+ if _, err := source.StagedPins(t.Context(), t.TempDir()); err == nil {
+ t.Fatal("census error chose legacy")
+ }
+}
+
+func TestPlan14GenerationProductionModeGate(t *testing.T) {
+ for _, flag := range []string{"", "1"} {
+ t.Run(flag, func(t *testing.T) {
+ t.Setenv("CARLOS_HOST_CLAIM", flag)
+ source := productionRootGenerationPins().(generationPinSource)
+ if source.legacy != nil {
+ source.legacy = func() (bool, error) { return true, nil }
+ }
+ calls := 0
+ source.open = func() (rootstate.Store, error) { calls++; return nil, os.ErrPermission }
+ opt := t.TempDir()
+ stageSweepVersion(t, opt, "bab", "v1", "shop", 48*time.Hour)
+ sweepStagedVersions(fakeLister{}, opt, noGenerations(t), 6*time.Hour, time.Now(), source)
+ if calls != 0 || staged(t, opt, "v1") {
+ t.Fatalf("no-census sweep changed: opens=%d survives=%v", calls, staged(t, opt, "v1"))
+ }
+ })
+ }
+}
diff --git a/internal/host/rootgenerationpins.go b/internal/host/rootgenerationpins.go
new file mode 100644
index 000000000..5dd3d58e2
--- /dev/null
+++ b/internal/host/rootgenerationpins.go
@@ -0,0 +1,48 @@
+package host
+
+import (
+ "context"
+ "os"
+
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+type generationPinSource struct {
+ legacy func() (bool, error)
+ open func() (rootstate.Store, error)
+ verify func(context.Context, []release.GenerationReservation) error
+}
+
+func productionRootGenerationPins() release.PinSource {
+ s := generationPinSource{legacy: legacyRootRestart, open: func() (rootstate.Store, error) {
+ return rootstate.Open(release.RootGenerationsDir, false, false)
+ }}
+ // Until root reconciles installation, launch and kernel inventory, Verify
+ // stays absent and destructive writes on census boxes remain unavailable.
+ return s
+}
+
+func (s generationPinSource) StagedPins(ctx context.Context, optDir string) (release.StagedPaths, error) {
+ if s.legacy != nil {
+ legacy, err := s.legacy()
+ if err != nil {
+ return nil, err
+ }
+ if legacy {
+ return release.StagedPaths{}, nil
+ }
+ }
+ if s.open == nil {
+ return nil, os.ErrPermission
+ }
+ store, err := s.open()
+ if err != nil {
+ return nil, err
+ }
+ if store == nil {
+ return nil, os.ErrPermission
+ }
+ defer store.Close()
+ return (release.RootGenerationPins{Ledger: &release.RootGenerations{Store: store}, Verify: s.verify}).StagedPins(ctx, optDir)
+}
diff --git a/internal/host/versionsweep.go b/internal/host/versionsweep.go
index a64771128..9dd3eeedf 100644
--- a/internal/host/versionsweep.go
+++ b/internal/host/versionsweep.go
@@ -1,8 +1,11 @@
package host
import (
+ "context"
"log"
"os"
+ "path/filepath"
+ "strings"
"time"
"github.com/carlosframework/platform/internal/registry"
@@ -46,7 +49,7 @@ type routeLister interface {
// returns: an unreadable live dir, an unlistable registry or an incomplete
// generations file means the pin set is incomplete, and an incomplete pin set is how a running app loses its
// binary. Freeing disk an hour later is cheap; deleting a live version is not.
-func sweepStagedVersions(reg routeLister, optDir, genPath string, grace time.Duration, now time.Time) {
+func sweepStagedVersions(reg routeLister, optDir, genPath string, grace time.Duration, now time.Time, sources ...release.PinSource) {
pinned, err := release.LivePins(optDir)
if err != nil {
log.Printf("adopt: staged-version sweep skipped: %v", err)
@@ -99,6 +102,22 @@ func sweepStagedVersions(reg routeLister, optDir, genPath string, grace time.Dur
for v := range gens {
pinned[v] = true
}
+ for _, source := range sources {
+ paths, err := source.StagedPins(context.Background(), optDir)
+ if err != nil {
+ log.Printf("adopt: staged-version sweep skipped: generations: %v", err)
+ return
+ }
+ for path := range paths {
+ rel, err := filepath.Rel(filepath.Join(optDir, "versions"), path)
+ version, _, _ := strings.Cut(rel, string(filepath.Separator))
+ if err != nil || !filepath.IsLocal(rel) || !release.ValidVersion(version) {
+ log.Printf("adopt: staged-version sweep skipped: generations: %v", os.ErrPermission)
+ return
+ }
+ pinned[version] = true
+ }
+ }
res, err := release.SweepStaged(optDir, pinned, grace, now)
if err != nil {
log.Printf("adopt: staged-version sweep: %v", err)
@@ -121,8 +140,8 @@ func sweepStagedVersions(reg routeLister, optDir, genPath string, grace time.Dur
// sweepStagedVersionsPass is the RunAdopt entry point: the grace from the
// environment, the clock from the wall.
-func sweepStagedVersionsPass(reg routeLister) {
- sweepStagedVersions(reg, release.OptDir(), release.GenerationsPath(dataDir()), versionSweepGrace(os.Getenv), time.Now())
+func sweepStagedVersionsPass(reg routeLister, sources ...release.PinSource) {
+ sweepStagedVersions(reg, release.OptDir(), release.GenerationsPath(dataDir()), versionSweepGrace(os.Getenv), time.Now(), sources...)
}
// settingsPins is every build the rows' update settings and build-state
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv
index b49cfde76..068f646fb 100644
--- a/internal/provision/testdata/plan14-root-reads-e.tsv
+++ b/internal/provision/testdata/plan14-root-reads-e.tsv
@@ -1,5 +1,5 @@
# Track E integration after merge3. Earlier baselines, overlays and legacy fingerprints are immutable.
-internal/host/agent.go runAdoptUnderRootRelease authority B3,E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. whole:Route@10700,whole:Route@18080,whole:Route@18116,whole:Route@8002 9fc9698cee7c2b88e3d38203a17e7a41512e800fa172ce5b19272e75c2c83ca5
+internal/host/agent.go runAdoptUnderRootRelease authority B3,E1,E2 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. 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. whole:Route@10766,whole:Route@18194,whole:Route@18230,whole:Route@8068 71b09c1fdcab8e9dbb9c6f255cfa72acbf8a17f4ab2d959d8d2b7d7d0afe3ca9
internal/host/agent.go runAdoptWithEnsureSystemUnits authority B3,C,D,E1,E2,E3,E4,F,J1 E1 selects the checked private history, canonical named-unit provider and root mint inventory under release exclusion with claims off or on; reconstruction and restart consumption remain incomplete. file:Open@191 9cdd0a958f5924c9c8d8c5613fd665f2c96a194cd11322adb6f0050fb183b541
internal/host/namedrecovery.go namedUnitRecovery.repair authority E1,J1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. file:Open@2642,registry:Channel@1814,whole:Route@1060,whole:Route@1822 566bc61eafb118d9f6fba29228c372b18c8729549ed9a0bccf32c61b360eb9c3
internal/host/namedrecovery_active.go namedUnitRecovery.recover authority B3,E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. file:Open@5504,registry:Channel@2558,registry:Unit@216,registry:Unit@233,registry:Version@3618,registry:Version@3724,whole:Route@1478,whole:Route@202,whole:Route@2560,whole:Route@3544 dabec20a8ea93c7d72b6d6c3a30a1ae7c99ffcee4de4c0416fba699734739b92
@@ -27,7 +27,7 @@ internal/host/rootrollback.go loadRollback authority B3,J1,E1 Signed named-unit
internal/host/rootrollback.go settleRollbackAttempt authority B3,E1 Independent signed recovery authority and retained attempt outcomes. Fixtures: TestPlan14SignedRestartWithoutHealthHistory, TestPlan14CapturedRollbackWithoutHealthHistory, TestPlan14IndependentRecoveryRetainsMintBarrier, TestPlan14ActiveRecoveryProcessCrash. 0a3e03771e9205e546e1b91371f8b17afba93525a5009afaf15736ca1c7bfa80
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 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. registry:App@26180,registry:App@28637,registry:App@29129,registry:App@29366,registry:App@30773,registry:App@5975,registry:App@9000,registry:App@9141,registry:Hibernate@29012,registry:Host@1055,registry:Host@2746,registry:Host@2813,registry:Host@2909,registry:Host@29182,registry:Host@2944,registry:Host@30098,registry:Host@30826,registry:Host@690,registry:UpdateSettings@8878,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@26175,whole:Route@2680,whole:Route@28632,whole:Route@28890,whole:Route@28912,whole:Route@28959,whole:Route@28977,whole:Route@28987,whole:Route@29030,whole:Route@29051,whole:Route@29064,whole:Route@29124,whole:Route@29150,whole:Route@29224,whole:Route@29263,whole:Route@29279,whole:Route@29307,whole:Route@29361,whole:Route@29387,whole:Route@30140,whole:Route@30194,whole:Route@30210,whole:Route@30717,whole:Route@30768,whole:Route@30794,whole:Route@30868,whole:Route@30903,whole:Route@30919,whole:Route@3665,whole:Route@4312,whole:Route@579,whole:Route@5897,whole:Route@5919,whole:Route@5970,whole:Route@5996,whole:Route@617,whole:Route@675,whole:Route@793,whole:Route@817,whole:Route@8829,whole:Route@8943,whole:Route@8988,whole:Route@9136 49427f5f398cf7df2b0ac2f63bfacfee3d3c630a253b176493ad87e0f7a49f00
+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/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
@@ -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 aa5107fd5f62f16db48f9f788c22c2df3279b5ba0740b8d3978bd1593ff42f92
-@coverage internal/release b3659b929285d525c77f4e298fc1eb588d696b37f16c5dd3a77e231012bc29ce
+@coverage internal/host 255d2b64782c34c73f12965a1738eefcef8387ea769670d475efcd802821905e
+@coverage internal/release 07038b66266e63a9be3ce85b954d4955223ce280e1c2e3f93ae508ae8ffeb1dc
@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
@@ -211,3 +211,16 @@ internal/release/rootgenerations.go type GenerationProcess authority E2 E18: cla
internal/release/rootgenerations.go type GenerationReservation authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. 4d1b20a5f99117b6bc2e16c6a60033496f99796c47e682ab19bbddbdb6cc35e4
internal/release/rootgenerations.go type RootGenerations authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. 240860014efc91f51ca7e3e920ffb5cfa98076340dc0c9e9972c35051e5eca84
internal/release/rootgenerations.go validGeneration authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. 7fa482be0515f5b94acd6fe5e3072b2a45e89c043961e30ab764d0f719b9a7cd
+internal/host/rootgenerationpins.go generationPinSource.StagedPins 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. 8484f0fc50fc55644ec8af81a3d89b6338cdbfb125ba40cd5cedf63a2b41a852
+internal/host/rootgenerationpins.go productionRootGenerationPins authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. file:Open@158 fa0ccb940d808f9443e19aa4837cd36e9278add4210ee4b021b471b3bfb9dd04
+internal/host/rootgenerationpins.go type generationPinSource 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. ccc17aa1e2436da6ae8518c3e2bce2d73a8b2a181d0de8bf33733ea49d12a974
+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 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 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. file:Read@486,file:Read@842 3d76e660f2a5302f01d31f608b9d145e2b891794cde97eee586f9a30afb2d688
+internal/release/rootgenerationpins.go WithRootGenerationPins authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. befc123585ff625901fd8b0516b2fa73b51968db21a28fa0d64c417382d54f79
+internal/release/rootgenerationpins.go generationFileIdentity authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. file:Lstat@96,file:Open@222 20a8f6c7c1c20fc2007dd89f2244b7797faede8d43f2206cd534a8033183b6ff
+internal/release/rootgenerationpins.go passConfig.generationWriteCheck authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. file:Lstat@537 f32259e8d00c8a196c9326fdaec2de5727211bd61117a2e1620339f68791e6ee
+internal/release/rootgenerationpins.go type RootGenerationPins authority E2 E19: both destructive readers use the root reservation snapshot under existing release exclusion. Exact artifact and containing-tree identities are retained; missing completeness refuses destruction. Production installation/launch/kernel completeness and verified retirement remain unfinished. Fixtures: TestPlan14GenerationDeletionCannotUnpin, TestPlan14GenerationSnapshotCompleteness, TestPlan14GenerationSnapshotPermitsUnreservedRestage, TestPlan14GenerationSnapshotPinsContainingTree, TestPlan14GenerationSnapshotEmptyIsUnknown, TestPlan14GenerationConsumerMode, TestPlan14GenerationProductionModeGate, TestPlan14GenerationSnapshotPermitsFreshTree. c680202be840bd6e420cc0c9ae192f6f73d259498e564a1c4dbaf413e3e944a1
diff --git a/internal/release/adopt.go b/internal/release/adopt.go
index 5f167e8f0..06ce5a821 100644
--- a/internal/release/adopt.go
+++ b/internal/release/adopt.go
@@ -104,7 +104,8 @@ type passConfig struct {
// pins is WithPinSource's provider: staged copies that something beyond
// the registry rows, the live links and the generations file still
// refers to. nil adds nothing.
- pins PinSource
+ pins PinSource
+ generationPins PinSource
// stage is what ensureStagedChecked needs to judge a staged copy it is
// about to reuse (spec §4.2). PassFilter sets it once per pass.
@@ -546,7 +547,8 @@ func PassFilter(ctx context.Context, reg *registry.Registry, s Store, pub ed2551
// release, so a healthy pass never pays for it, and it reads fresh then,
// so a host this pass has already repointed counts.
cfg.stage = stageCheck{
- verified: map[string]string{},
+ beforeWrite: func(path string) error { return cfg.generationWriteCheck(ctx, optDir, path) },
+ verified: map[string]string{},
refs: func() (StagedPaths, error) {
rows, err := reg.List()
if err != nil {
@@ -2064,6 +2066,7 @@ func capabilityPlan(rt registry.Route, m *Manifest, canRestart bool) (caps.Plan,
// stageCheck is what ensureStagedChecked needs to judge a staged copy it is
// about to reuse (spec §4.2).
type stageCheck struct {
+ beforeWrite func(string) error
// refs reports the staged copies something on this box still refers to.
// nil, or an error, means unknown, and unknown never permits a restage.
refs func() (StagedPaths, error)
@@ -2232,6 +2235,11 @@ func stageFrom(ctx context.Context, s Store, optDir, acct, app, ver, host string
}
// Streamed to disk and hashed on the way (#269): a build read whole
// outgrew the stage pass's memory cap.
+ if chk.beforeWrite != nil {
+ if err := chk.beforeWrite(path); err != nil {
+ return nil, err
+ }
+ }
err := stageArtifactFrom(optDir, acct, ver, app, art.SHA256, func() (io.ReadCloser, error) {
rc, _, err := OpenQualified(ctx, s, ArtifactKey(acct, app, ver, art.File))
return rc, err
diff --git a/internal/release/plan14_generation_pins_test.go b/internal/release/plan14_generation_pins_test.go
new file mode 100644
index 000000000..85ee9c0e6
--- /dev/null
+++ b/internal/release/plan14_generation_pins_test.go
@@ -0,0 +1,212 @@
+package release
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+type generationListingStore struct{ *healthyRetryStore }
+
+func (s generationListingStore) Names() ([]string, error) {
+ entries, err := os.ReadDir(s.dir)
+ var names []string
+ for _, e := range entries {
+ names = append(names, e.Name())
+ }
+ return names, err
+}
+
+func generationPinFixture(t *testing.T, opt, account, app, version string) (*RootGenerations, GenerationReservation) {
+ t.Helper()
+ g, s, b := generationFixture(t)
+ g.Store = generationListingStore{s}
+ b.Binding.Account, b.Binding.App, b.Version = account, app, version
+ b.Path = VersionPath(opt, account, version, app)
+ b.Tree = filepath.Dir(b.Path)
+ for path, dst := range map[string]*rootstate.FileIdentity{b.Path: &b.Executable, b.Tree: &b.TreeIdentity} {
+ f, err := os.Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ *dst, err = rootstate.Identity(f)
+ f.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ r, err := g.Reserve(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return g, r
+}
+
+func TestPlan14GenerationDeletionCannotUnpin(t *testing.T) {
+ for _, attack := range []string{"delete", "empty", "rename", "missing-completeness", "deleted-executable", "renamed-parent", "changed-inode"} {
+ t.Run(attack, func(t *testing.T) {
+ f := newReuseFixture(t)
+ g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2")
+ dir := t.TempDir()
+ path := GenerationsPath(dir)
+ if err := WriteGenerations(path, Generations{Paths: map[string][]string{"erased.example": {r.Binding.Path}}}); err != nil {
+ t.Fatal(err)
+ }
+ before, err := os.Stat(r.Binding.Path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ opened, err := os.Open(r.Binding.Path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer opened.Close()
+ source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }}
+ switch attack {
+ case "empty":
+ if err := WriteGenerations(path, Generations{}); err != nil {
+ t.Fatal(err)
+ }
+ case "rename":
+ if err := os.Rename(filepath.Dir(path), filepath.Dir(path)+".old"); err != nil {
+ t.Fatal(err)
+ }
+ default:
+ if err := os.Remove(path); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Remove(GenerationsSeqPath(path)); err != nil {
+ t.Fatal(err)
+ }
+ }
+ switch attack {
+ case "missing-completeness":
+ source.Verify = nil
+ case "deleted-executable":
+ if err := os.Remove(r.Binding.Path); err != nil {
+ t.Fatal(err)
+ }
+ case "renamed-parent":
+ if err := os.Rename(r.Binding.Tree, r.Binding.Tree+".old"); err != nil {
+ t.Fatal(err)
+ }
+ case "changed-inode":
+ if err := os.Rename(r.Binding.Path, r.Binding.Path+".old"); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(r.Binding.Path, []byte("replacement"), 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ f.pass(t, BakeDir(dir), WithRootGenerationPins(source))
+ got := make([]byte, len("old v2"))
+ if _, err := opened.ReadAt(got, 0); err != nil || string(got) != "old v2" {
+ t.Fatalf("reserved bytes changed: %q %v", got, err)
+ }
+ if attack == "deleted-executable" {
+ if _, err := os.Stat(r.Binding.Path); !os.IsNotExist(err) {
+ t.Fatalf("missing pinned path restaged: %v", err)
+ }
+ } else if attack != "changed-inode" && attack != "renamed-parent" {
+ after, err := os.Stat(r.Binding.Path)
+ if err != nil || !os.SameFile(before, after) {
+ t.Fatalf("reserved inode replaced: %v", err)
+ }
+ }
+ if attack == "renamed-parent" {
+ after, err := os.Stat(filepath.Join(r.Binding.Tree+".old", filepath.Base(r.Binding.Path)))
+ if err != nil || !os.SameFile(before, after) {
+ t.Fatalf("renamed reservation destroyed: %v", err)
+ }
+ return
+ }
+ if v, _ := LiveVersion(f.opt, "h1.example.com"); v != "v1" {
+ t.Fatalf("refused staging adopted %s", v)
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationSnapshotCompleteness(t *testing.T) {
+ f := newReuseFixture(t)
+ g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2")
+ source := RootGenerationPins{Ledger: g}
+ if _, err := source.StagedPins(t.Context(), f.opt); err == nil {
+ t.Fatal("missing completeness granted pins")
+ }
+ called := false
+ source.Verify = func(_ context.Context, records []GenerationReservation) error {
+ called = true
+ if len(records) != 1 || records[0] != r {
+ t.Fatalf("wrong root inventory: %+v", records)
+ }
+ return nil
+ }
+ pins, err := source.StagedPins(t.Context(), f.opt)
+ if err != nil || !called || !pins.Has(r.Binding.Path) {
+ t.Fatalf("snapshot: %v %v", pins, err)
+ }
+ s := g.Store.(generationListingStore)
+ s.fail = 1
+ if _, err := source.StagedPins(t.Context(), f.opt); err == nil {
+ t.Fatal("snapshot ignored failed directory sync")
+ }
+ if _, err := source.StagedPins(t.Context(), f.opt); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(s.dir, "foreign.json"), []byte(`{}`), 0600); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := source.StagedPins(t.Context(), f.opt); err == nil {
+ t.Fatal("malformed inventory granted snapshot")
+ }
+}
+
+func TestPlan14GenerationSnapshotPermitsUnreservedRestage(t *testing.T) {
+ f := newReuseFixture(t)
+ g, _ := generationPinFixture(t, f.opt, "bab", "myapp", "v1")
+ source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }}
+ f.pass(t, WithRootGenerationPins(source))
+ if got := f.stagedV2(t); got != "new v2" {
+ t.Fatalf("unreserved copy not restaged: %q", got)
+ }
+}
+
+func TestPlan14GenerationSnapshotPinsContainingTree(t *testing.T) {
+ f := newReuseFixture(t)
+ if err := StageArtifact(f.opt, "bab", "v2", "sibling", []byte("sibling")); err != nil {
+ t.Fatal(err)
+ }
+ g, _ := generationPinFixture(t, f.opt, "bab", "sibling", "v2")
+ source := RootGenerationPins{Ledger: g, Verify: func(context.Context, []GenerationReservation) error { return nil }}
+ f.pass(t, WithRootGenerationPins(source))
+ if got := f.stagedV2(t); got != "old v2" {
+ t.Fatalf("reserved containing tree changed: %q", got)
+ }
+}
+
+func TestPlan14GenerationSnapshotEmptyIsUnknown(t *testing.T) {
+ g, _, _ := generationFixture(t)
+ s := g.Store.(*healthyRetryStore)
+ g.Store = generationListingStore{s}
+ if _, err := (RootGenerationPins{Ledger: g}).StagedPins(t.Context(), t.TempDir()); err == nil {
+ t.Fatal("empty reservation directory established completeness")
+ }
+}
+
+func TestPlan14GenerationSnapshotPermitsFreshTree(t *testing.T) {
+ f := newReuseFixture(t)
+ tree := filepath.Join(f.opt, "versions", "v2")
+ if err := os.RemoveAll(tree); err != nil {
+ t.Fatal(err)
+ }
+ reads := 0
+ source := PinSourceFunc(func(context.Context, string) (StagedPaths, error) { reads++; return nil, os.ErrPermission })
+ f.pass(t, WithRootGenerationPins(source))
+ if got := f.stagedV2(t); got != "new v2" || reads != 0 {
+ t.Fatalf("fresh signed build blocked: %q reads=%d", got, reads)
+ }
+}
diff --git a/internal/release/rootgenerationpins.go b/internal/release/rootgenerationpins.go
new file mode 100644
index 000000000..7f1635e44
--- /dev/null
+++ b/internal/release/rootgenerationpins.go
@@ -0,0 +1,138 @@
+package release
+
+import (
+ "context"
+ "errors"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+// RootGenerationPins is read under release exclusion, held through destruction.
+// Verify must reconcile the records with root installation/launch inventory and
+// kernel references. An empty directory alone never establishes completeness.
+type RootGenerationPins struct {
+ Ledger *RootGenerations
+ Verify func(context.Context, []GenerationReservation) error
+}
+
+func (s RootGenerationPins) StagedPins(ctx context.Context, optDir string) (StagedPaths, error) {
+ if s.Ledger == nil || s.Ledger.Store == nil || !filepath.IsAbs(optDir) {
+ return nil, os.ErrPermission
+ }
+ names, err := s.Ledger.Store.Names()
+ if err != nil {
+ return nil, err
+ }
+ sort.Strings(names)
+ records := make([]GenerationReservation, 0, len(names))
+ pins := StagedPaths{}
+ for _, name := range names {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ raw, err := s.Ledger.Store.Read(name)
+ if err != nil {
+ return nil, err
+ }
+ var r GenerationReservation
+ if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &r) != nil || !validGeneration(r) || generationKey(r.Binding) != name {
+ return nil, os.ErrPermission
+ }
+ // Read retries the directory sync before this record can inform a grant.
+ r, err = s.Ledger.Read(r.Binding)
+ if err != nil {
+ return nil, err
+ }
+ b := r.Binding
+ if b.Path != VersionPath(optDir, b.Binding.Account, b.Version, b.Binding.App) {
+ return nil, os.ErrPermission
+ }
+ for path, expected := range map[string]rootstate.FileIdentity{b.Path: b.Executable, b.Tree: b.TreeIdentity} {
+ if err := generationFileIdentity(path, expected); err != nil {
+ return nil, err
+ }
+ }
+ pins.add(b.Path)
+ pins.add(b.Tree)
+ records = append(records, r)
+ }
+ if s.Verify == nil {
+ return nil, os.ErrPermission
+ }
+ if err := s.Verify(ctx, records); err != nil {
+ return nil, err
+ }
+ return pins, nil
+}
+
+func generationFileIdentity(path string, expected rootstate.FileIdentity) error {
+ info, err := os.Lstat(path)
+ if err != nil {
+ return err
+ }
+ if info.Mode()&os.ModeSymlink != 0 {
+ return os.ErrPermission
+ }
+ f, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ actual, err := rootstate.Identity(f)
+ if err != nil {
+ return err
+ }
+ if actual != expected {
+ return os.ErrPermission
+ }
+ return nil
+}
+
+// WithRootGenerationPins protects new writes as well as replacement of a
+// differing artifact: a removed executable can still be held by a generation.
+func WithRootGenerationPins(source PinSource) PassOption {
+ return func(c *passConfig) { c.generationPins = source }
+}
+
+func (c passConfig) generationWriteCheck(ctx context.Context, optDir, path string) error {
+ if c.generationPins == nil {
+ return nil
+ }
+ // Creating a wholly absent version tree cannot destroy an existing pin.
+ // Keep fresh signed recovery available while pin completeness is unknown.
+ rel, err := filepath.Rel(filepath.Join(optDir, "versions"), path)
+ version, _, ok := strings.Cut(rel, string(filepath.Separator))
+ if err != nil || !filepath.IsLocal(rel) || !ok || !ValidVersion(version) {
+ return os.ErrPermission
+ }
+ if _, err := os.Lstat(filepath.Join(optDir, "versions", version)); errors.Is(err, os.ErrNotExist) {
+ return nil
+ } else if err != nil {
+ return err
+ }
+ pins, err := c.generationPins.StagedPins(ctx, optDir)
+ if err != nil {
+ return err
+ }
+ pinned, err := pins.Refers(path)
+ if err != nil {
+ return err
+ }
+ if pinned {
+ return ErrVersionReused
+ }
+ for parent := filepath.Dir(path); parent != optDir && parent != filepath.Dir(parent); parent = filepath.Dir(parent) {
+ pinned, err := pins.Refers(parent)
+ if err != nil {
+ return err
+ }
+ if pinned {
+ return ErrVersionReused
+ }
+ }
+ return nil
+}