carlos / platform Public

Plan 14 integrated: lifecycle E2E passes (not for merge)

Production-wired lifecycle reboot pair passes all four cases (ordinary, crash after reservation, after handover, mid-retirement). Five more production defects fixed: admission-loss evidence blocking retirement; agent preflight reading root-private drop-ins; stale local cleanup after retirement; root settlement requiring vanished containment; boot-zero identities in the final generation proof.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 6c44b0dfba659e6a62f6ad3608f6a8b33d2d5e17 parent 888fb0b
21 files changed, +796 −20
  • internal/host/claimactivation_preflight.go +7 −0
  • internal/host/claimparticipation.go +5 −0
  • internal/host/claimparticipation_retired.go +44 −0
  • internal/host/claimparticipation_retired_test.go +64 −0
  • internal/host/plan14_lifecycle_retire_root_linux_test.go +10 −0
  • internal/host/plan14_lifecycle_root_linux_test.go +1 −1
  • internal/host/rootexecution_retired_test.go +51 −0
  • internal/host/rootexecution_settle.go +22 −0
  • internal/host/rootgeneration_kernel_linux.go +15 −7
  • internal/host/rootgeneration_retired_linux.go +185 −0
  • internal/host/rootgeneration_retired_linux_test.go +104 −0
  • internal/host/rootgeneration_retired_other.go +17 −0
  • internal/host/rootgeneration_retirement.go +15 −4
  • internal/host/rootgeneration_retirement_lock_test.go +62 −0
  • internal/host/rootgenerationpins.go +1 −1
  • internal/hostclaim/claim.go +6 −2
  • internal/hostclaim/delete.go +1 −1
  • internal/hostclaim/retainedloss.go +45 −0
  • internal/hostclaim/retainedloss_test.go +112 −0
  • internal/provision/plan14_p3_census_test.go +5 −0
  • internal/provision/testdata/plan14-root-reads-p3.tsv +24 −4

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	299.259s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.712s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.214s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.704s
ok  	github.com/carlosframework/platform/internal/activator	251.310s
ok  	github.com/carlosframework/platform/internal/agentsock	0.015s
ok  	github.com/carlosframework/platform/internal/alerting	0.005s
ok  	github.com/carlosframework/platform/internal/analytics	0.018s
ok  	github.com/carlosframework/platform/internal/appconfig	2.480s
ok  	github.com/carlosframework/platform/internal/apperr	0.083s
ok  	github.com/carlosframework/platform/internal/applog	0.127s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.005s
ok  	github.com/carlosframework/platform/internal/boxid	0.898s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.008s
ok  	github.com/carlosframework/platform/internal/caps	0.012s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.704s
ok  	github.com/carlosframework/platform/internal/changelog	0.303s
ok  	github.com/carlosframework/platform/internal/clamd	0.067s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	391.609s
?   	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.202s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.008s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.002s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.378s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	26.762s
?   	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.132s
ok  	github.com/carlosframework/platform/internal/gate	1.657s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.017s
ok  	github.com/carlosframework/platform/internal/geoip	0.029s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.151s
ok  	github.com/carlosframework/platform/internal/host	174.699s
ok  	github.com/carlosframework/platform/internal/hostactors	0.349s
ok  	github.com/carlosframework/platform/internal/hostbox	2.623s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.589s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.006s
ok  	github.com/carlosframework/platform/internal/hostexec	4.820s
ok  	github.com/carlosframework/platform/internal/hostfence	0.039s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.441s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.015s
ok  	github.com/carlosframework/platform/internal/hostguard	1.040s
ok  	github.com/carlosframework/platform/internal/hostops	1.577s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.004s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.003s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.466s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.150s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.005s
ok  	github.com/carlosframework/platform/internal/infraview	0.004s
ok  	github.com/carlosframework/platform/internal/instancectl	5.221s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.731s
ok  	github.com/carlosframework/platform/internal/instancefs	0.008s
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.543s
ok  	github.com/carlosframework/platform/internal/lease	9.787s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.005s
ok  	github.com/carlosframework/platform/internal/ledger	0.017s
ok  	github.com/carlosframework/platform/internal/mail	0.173s
ok  	github.com/carlosframework/platform/internal/maintenance	0.085s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.485s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.067s
ok  	github.com/carlosframework/platform/internal/metrics	0.006s
ok  	github.com/carlosframework/platform/internal/moves	21.038s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.780s
ok  	github.com/carlosframework/platform/internal/objstore	0.181s
ok  	github.com/carlosframework/platform/internal/ops	0.182s
ok  	github.com/carlosframework/platform/internal/park	0.230s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	33.467s
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	5.439s
ok  	github.com/carlosframework/platform/internal/relay	7.228s
ok  	github.com/carlosframework/platform/internal/release	21.645s
ok  	github.com/carlosframework/platform/internal/rollout	3.709s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.057s
ok  	github.com/carlosframework/platform/internal/rootstate	0.028s
ok  	github.com/carlosframework/platform/internal/runs	0.577s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.499s
ok  	github.com/carlosframework/platform/internal/seeds	0.013s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.259s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.068s
ok  	github.com/carlosframework/platform/internal/status	0.042s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.317s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
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.577s
=== 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
linux/arm64 carlos binary: 48562338 bytes (ceiling 52000000)
--- 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/16 22:58:08 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DF24A9D6BE91FB, 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.020s
minio contract: PASS
diff --git a/internal/host/claimactivation_preflight.go b/internal/host/claimactivation_preflight.go
index 7e35f8bdd..0d34853fc 100644
--- a/internal/host/claimactivation_preflight.go
+++ b/internal/host/claimactivation_preflight.go
@@ -6,6 +6,7 @@ import (
"errors"
"github.com/carlosframework/platform/internal/hostops"
"github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/instancectl"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
@@ -136,6 +137,12 @@ func claimActivationLocalUnits(r ClaimRuntimeResources) error {
return hostwrite.ErrUnavailable
}
if entry.IsDir() {
+ if r.Mode == "agent" && filepath.Dir(path) == "/run/systemd/system" && strings.HasSuffix(name, ".d") {
+ role, _, ordinary := instancectl.ParseUnitName(strings.TrimSuffix(name, ".d"))
+ if ordinary && role == instancectl.RoleRun {
+ return fs.SkipDir
+ }
+ }
return nil
}
if dir == "/etc/carlos/apps" {
diff --git a/internal/host/claimparticipation.go b/internal/host/claimparticipation.go
index 1d33e6a76..6b25cc45f 100644
--- a/internal/host/claimparticipation.go
+++ b/internal/host/claimparticipation.go
@@ -234,6 +234,11 @@ func (p *productionClaimLocal) cleanup(ctx context.Context, o hostclaim.Observed
return hostwrite.ErrUnavailable
}
p.cancelWork(o.Host)
+ if !p.legacyParticipation {
+ if done, err := a.completedRetirementDischarge(ctx, o, binding); done || err != nil {
+ return err
+ }
+ }
// Claim ask C10 (region HA rulings 56, 66): a relocation that completed
// on this box is discharged by the persisted participation binding and
// its exact completed probe evidence, before any ordinary cleanup, so
diff --git a/internal/host/claimparticipation_retired.go b/internal/host/claimparticipation_retired.go
new file mode 100644
index 000000000..87b597e48
--- /dev/null
+++ b/internal/host/claimparticipation_retired.go
@@ -0,0 +1,44 @@
+package host
+
+import (
+ "context"
+ "slices"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+ execlease "github.com/carlosframework/platform/internal/hostexec"
+ "github.com/carlosframework/platform/internal/hostguard"
+)
+
+func (a ClaimAssembly) completedRetirementDischarge(ctx context.Context, o hostclaim.Observed, binding execlease.Binding) (bool, error) {
+ if o.Claim == nil || o.Claim.Holder == nil || binding.Host != o.Host || binding.Incarnation != o.Claim.Holder.Incarnation || binding.Op == "" || binding.Op != o.Claim.Op.ID ||
+ (o.Claim.Op.Kind != hostclaim.KindDeleting && o.Claim.Op.Kind != hostclaim.KindParking) {
+ return false, nil
+ }
+ h := o.Claim.Holder
+ captured := hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}
+ var view hostguard.FenceView
+ var err error
+ if a.Guard.Root != nil {
+ view, err = a.Guard.Root.View(ctx, o.Host, captured)
+ } else if a.Guard.FenceView != nil {
+ view, err = a.Guard.FenceView(ctx, o.Host, captured)
+ } else {
+ return false, nil
+ }
+ if err != nil {
+ return true, err
+ }
+ if !slices.Contains(view.Retired, captured) {
+ return false, nil
+ }
+ if view.Barrier != nil || view.Installation.Host != o.Host || view.Installation.Incarnation != h.Incarnation || view.Installation.Box != binding.Box || view.Installation.Enrollment != binding.Enrollment {
+ return true, ErrClaimStopIdentity
+ }
+ for _, receipt := range view.Receipts {
+ r := receipt.Target
+ if r.Host == binding.Host && r.Incarnation == binding.Incarnation && r.Stamp == h.Stamp && r.Session == h.Session && r.Op == binding.Op && r.ObligationID != "" && r.Box == binding.Box && r.Enrollment == binding.Enrollment {
+ return true, verifyRetirementReceipt(ctx, receipt)
+ }
+ }
+ return true, ErrClaimStopReceipt
+}
diff --git a/internal/host/claimparticipation_retired_test.go b/internal/host/claimparticipation_retired_test.go
new file mode 100644
index 000000000..109952478
--- /dev/null
+++ b/internal/host/claimparticipation_retired_test.go
@@ -0,0 +1,64 @@
+package host
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+ execlease "github.com/carlosframework/platform/internal/hostexec"
+ "github.com/carlosframework/platform/internal/hostguard"
+)
+
+func TestPlan14RetiredParticipationDischarge(t *testing.T) {
+ for _, mode := range []string{"retired", "live", "foreign box", "foreign enrollment", "foreign session", "foreign op", "foreign stamp", "no receipt", "corrupt receipt", "barrier", "read failure"} {
+ t.Run(mode, func(t *testing.T) {
+ binding := execlease.Binding{Host: "shop.example", Incarnation: "inc", Op: "delete", Box: "box", Enrollment: "enrollment"}
+ o := hostclaim.Observed{Host: binding.Host, Claim: &hostclaim.Claim{Host: binding.Host, Holder: &hostclaim.Holder{Incarnation: "inc", Stamp: 3, Session: 4}, Op: hostclaim.Op{Kind: hostclaim.KindParking, ID: "delete"}}}
+ target := hostguard.FenceTarget{Host: binding.Host, Incarnation: "inc", Stamp: 3, Session: 4, Box: "box", Enrollment: "enrollment", Op: "delete", Phase: "park", ObligationID: "park-local", FenceID: "fence", BootID: "boot", ControllerID: "controller", ClaimETag: "etag", ClaimGen: 5, DirectoryRevision: "1", Region: "ie"}
+ at := time.Now().UTC()
+ s := StopReceipt{V: 1, Kind: "stopped", Host: target.Host, Incarnation: target.Incarnation, InstallationStamp: target.Stamp, WriterSession: target.Session, Box: target.Box, Enrollment: target.Enrollment, OperationID: target.Op, Phase: target.Phase, ObligationID: target.ObligationID, FenceID: target.FenceID, BootID: target.BootID, ControllerID: target.ControllerID, ClaimETag: target.ClaimETag, ClaimGen: target.ClaimGen, DirectoryRevision: target.DirectoryRevision, Region: target.Region, EvidenceAt: at}
+ captured := hostguard.Captured{Incarnation: "inc", Stamp: 3}
+ view := hostguard.FenceView{Installation: hostguard.RootInstallation{Host: binding.Host, Incarnation: "inc", Stamp: 3, Box: "box", Enrollment: "enrollment"}, Retired: []hostguard.Captured{captured}, Receipts: []hostguard.FenceReceipt{{Target: target, Bytes: mustMarshalStopReceipt(s), EvidenceAt: at}}}
+ switch mode {
+ case "live":
+ view.Retired = nil
+ case "foreign box":
+ binding.Box = "other"
+ case "foreign enrollment":
+ binding.Enrollment = "other"
+ case "foreign session":
+ o.Claim.Holder.Session++
+ case "foreign op":
+ view.Receipts[0].Target.Op = "other"
+ case "foreign stamp":
+ view.Receipts[0].Target.Stamp++
+ case "no receipt":
+ view.Receipts = nil
+ case "corrupt receipt":
+ view.Receipts[0].Bytes = []byte("{}")
+ case "barrier":
+ view.Barrier = &target
+ }
+ a := ClaimAssembly{Guard: hostguard.Guard{FenceView: func(context.Context, string, hostguard.Captured) (hostguard.FenceView, error) {
+ if mode == "read failure" {
+ return hostguard.FenceView{}, errors.New("unreadable")
+ }
+ return view, nil
+ }}}
+ done, err := a.completedRetirementDischarge(t.Context(), o, binding)
+ if mode == "retired" {
+ if !done || err != nil {
+ t.Fatalf("discharge: %v %v", done, err)
+ }
+ } else if mode == "live" {
+ if done || err != nil {
+ t.Fatalf("live cleanup bypass: %v %v", done, err)
+ }
+ } else if err == nil {
+ t.Fatalf("unsafe discharge: %v", done)
+ }
+ })
+ }
+}
diff --git a/internal/host/plan14_lifecycle_retire_root_linux_test.go b/internal/host/plan14_lifecycle_retire_root_linux_test.go
index 0d1080b81..88b331e80 100644
--- a/internal/host/plan14_lifecycle_retire_root_linux_test.go
+++ b/internal/host/plan14_lifecycle_retire_root_linux_test.go
@@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"os"
+ "os/exec"
"path/filepath"
"strconv"
"strings"
@@ -130,6 +131,15 @@ func lifecycleVerifyRetiredPins(t *testing.T, store release.Store, key ed25519.P
lock, err := rootstate.Open(rootReleasePrivate, false, false)
lifecycleCheck(t, "release lock", err)
defer lock.Close()
+ unknown := "plan14-pin-unknown.service"
+ stopUnknown := func() { exec.Command("systemctl", "stop", unknown).Run() }
+ t.Cleanup(stopUnknown)
+ out, err := exec.Command("systemd-run", "--unit="+unknown, "--slice=carlos-instances.slice", "--property=Type=exec", "/bin/sleep", "60").CombinedOutput()
+ lifecycleCheck(t, "unrecorded containment probe: "+string(out), err)
+ if err := rootstate.WithRelease(ctx, lock, func() error { _, err := source.StagedPins(ctx, release.OptDir()); return err }); err == nil {
+ t.Fatal("unrecorded managed process did not prevent generation certification")
+ }
+ stopUnknown()
lifecycleCheck(t, "production pins after actual retirement", rootstate.WithRelease(ctx, lock, func() error {
pins, err := source.StagedPins(ctx, release.OptDir())
if err != nil {
diff --git a/internal/host/plan14_lifecycle_root_linux_test.go b/internal/host/plan14_lifecycle_root_linux_test.go
index 9464808ca..2c85b694c 100644
--- a/internal/host/plan14_lifecycle_root_linux_test.go
+++ b/internal/host/plan14_lifecycle_root_linux_test.go
@@ -362,7 +362,7 @@ func lifecycleStartAgent(t *testing.T, endpoint string, completeCreate func() er
for time.Now().Before(deadline) {
if _, err := os.Stat(lifecycleCrashPath); err == nil && !recovered {
recoverControl()
- until := time.Now().Add(40 * time.Second)
+ until := time.Now().Add(110 * time.Second)
for {
state, _ := exec.Command("systemctl", "show", "carlos-edge.service", "-p", "ActiveState", "--value").Output()
if strings.TrimSpace(string(state)) == "failed" || strings.TrimSpace(string(state)) == "inactive" {
diff --git a/internal/host/rootexecution_retired_test.go b/internal/host/rootexecution_retired_test.go
new file mode 100644
index 000000000..3562f7d79
--- /dev/null
+++ b/internal/host/rootexecution_retired_test.go
@@ -0,0 +1,51 @@
+package host
+
+import (
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/instancectl"
+)
+
+func TestPlan14RetiredExecutionClosure(t *testing.T) {
+ for _, mode := range []string{"retired", "live", "foreign host", "foreign stamp", "barrier", "open writers", "unsettled unit", "foreign unit", "unfinished role", "bad completion"} {
+ t.Run(mode, func(t *testing.T) {
+ target := hostguard.FenceTarget{Host: "shop.example", Incarnation: "inc", Stamp: 3, Session: 4, Box: "box", Enrollment: "en", BootID: "boot", ControllerID: "controller"}
+ view := hostguard.FenceView{Host: target.Host, Retired: []hostguard.Captured{{Incarnation: target.Incarnation, Stamp: target.Stamp}}}
+ launch := hostbox.LaunchInventory{Target: target, WritersClosed: true, Reservations: []hostbox.LaunchReservation{{Target: target, State: hostbox.UnitSettled, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(target.Host)}}}}
+ switch mode {
+ case "live":
+ view.Retired = nil
+ case "foreign host":
+ view.Host = "other.example"
+ case "foreign stamp":
+ view.Retired[0].Stamp++
+ case "barrier":
+ view.Barrier = &target
+ case "open writers":
+ launch.WritersClosed = false
+ case "unsettled unit":
+ launch.Reservations[0].State = hostbox.UnitStarted
+ case "foreign unit":
+ launch.Reservations[0].Target.Session++
+ case "unfinished role":
+ launch.Reservations[0].State = "started"
+ case "bad completion":
+ launch.Completed.Count = 1
+ }
+ done, err := retiredExecutionLaunch(view, launch)
+ if mode == "retired" {
+ if !done || err != nil {
+ t.Fatalf("retired closure: %v %v", done, err)
+ }
+ } else if mode == "live" || mode == "foreign stamp" {
+ if done || err != nil {
+ t.Fatalf("live closure bypassed: %v %v", done, err)
+ }
+ } else if err == nil {
+ t.Fatal("unproven closure accepted")
+ }
+ })
+ }
+}
diff --git a/internal/host/rootexecution_settle.go b/internal/host/rootexecution_settle.go
index b35e7c1cc..e89766c18 100644
--- a/internal/host/rootexecution_settle.go
+++ b/internal/host/rootexecution_settle.go
@@ -154,6 +154,12 @@ func (h ClaimRootHTTP) cleanupExecution(ctx context.Context, r execlease.Executi
if err != nil {
return err
}
+ if retired, err := retiredExecutionLaunch(view, launch); retired || err != nil {
+ if err != nil {
+ return err
+ }
+ continue
+ }
ack := DrainClaimAcknowledgement(receipt.Target.FenceID, launch.Target.ControllerID, launch.ProcessStart, launch.ProducerIDs, launch.Completed)
if _, err := h.Stop.Coordinator.Kernel.VerifyStopped(ctx, receipt.Target, inventory, ack); err != nil {
return err
@@ -162,6 +168,22 @@ func (h ClaimRootHTTP) cleanupExecution(ctx context.Context, r execlease.Executi
return nil
}
+func retiredExecutionLaunch(view hostguard.FenceView, launch hostbox.LaunchInventory) (bool, error) {
+ t := launch.Target
+ if !slices.Contains(view.Retired, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}) {
+ return false, nil
+ }
+ if view.Host != t.Host || view.Barrier != nil || !launch.WritersClosed || !launch.Completed.Valid() {
+ return true, ErrClaimStopInventory
+ }
+ for _, r := range launch.Reservations {
+ if !hostbox.SameLaunchOwner(r.Target, t) || (hostbox.IsUnitReservation(r) && r.State != hostbox.UnitSettled) || (!hostbox.IsUnitReservation(r) && r.State != "settled") {
+ return true, ErrClaimStopInventory
+ }
+ }
+ return true, nil
+}
+
func (h ClaimRootHTTP) transitionExecution(ctx context.Context, old execlease.ExecutionRecord, q execlease.ExecutionRequest, next execlease.ExecutionAuthority) error {
a := old.Authority
if old.Request.Host != q.Host || old.Request.Incarnation != q.Incarnation || old.Request.Op != q.Op || a.Box != next.Box || a.Enrollment != next.Enrollment || a.Account != next.Account || a.App != next.App || a.Migration != next.Migration {
diff --git a/internal/host/rootgeneration_kernel_linux.go b/internal/host/rootgeneration_kernel_linux.go
index f86df98e8..49053ee66 100644
--- a/internal/host/rootgeneration_kernel_linux.go
+++ b/internal/host/rootgeneration_kernel_linux.go
@@ -54,8 +54,8 @@ func generationStart(raw []byte) (string, error) {
if len(fields) < 20 {
return "", os.ErrPermission
}
- start, err := strconv.ParseUint(fields[19], 10, 64)
- if err != nil || start == 0 {
+ _, err := strconv.ParseUint(fields[19], 10, 64)
+ if err != nil {
return "", os.ErrPermission
}
return fields[19], nil
@@ -504,17 +504,25 @@ func productionGenerationAbsence(ctx context.Context, r release.GenerationReserv
if r.Binding.Path != release.VersionPath(release.OptDir(), r.Binding.Binding.Account, r.Binding.Version, r.Binding.Binding.App) {
return "", os.ErrPermission
}
- current, err := generationCurrentFiles(r.Binding, rootstate.OpenTrustedFile)
+ current, err := generationRecordedAbsence(ctx, r)
if err != nil {
return "", err
}
+ return (generationKernel{proc: "/proc"}).absent(ctx, current)
+}
+
+func generationRecordedAbsence(ctx context.Context, r release.GenerationReservation) (release.GenerationReservation, error) {
+ current, err := generationCurrentFiles(r.Binding, rootstate.OpenTrustedFile)
+ if err != nil {
+ return r, err
+ }
r.Binding = current
if err := generationGroupGone(ctx, "/sys/fs/cgroup", r); err != nil {
- return "", err
+ return r, err
}
k := generationKernel{proc: "/proc"}
if err := k.controllerGone(ctx, r.Binding); err != nil {
- return "", err
+ return r, err
}
// Missing cgroups alone never discharge a reservation. With its launch
// authority closed, both recorded process identities must also be gone.
@@ -528,10 +536,10 @@ func productionGenerationAbsence(ctx context.Context, r release.GenerationReserv
b := r.Binding
b.ControllerPID, b.ControllerStart = p.pid, p.start
if err := k.controllerGone(ctx, b); err != nil {
- return "", err
+ return r, err
}
}
- return k.absent(ctx, r)
+ return r, nil
}
func generationGroupGone(ctx context.Context, root string, r release.GenerationReservation) error {
diff --git a/internal/host/rootgeneration_retired_linux.go b/internal/host/rootgeneration_retired_linux.go
new file mode 100644
index 000000000..6c8327e68
--- /dev/null
+++ b/internal/host/rootgeneration_retired_linux.go
@@ -0,0 +1,185 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "errors"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "slices"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func isolatedGeneration(r release.GenerationReservation) bool {
+ return (r.Binding.Unit == "" || r.Binding.Unit == instancectl.UnitName(instancectl.RoleRun, r.Binding.Target.Host)) &&
+ (r.Process == (release.GenerationProcess{}) || r.Process.Containment == 1)
+}
+
+func retiredGenerationReceipt(ctx context.Context, view hostguard.FenceView, owner release.GenerationOwner) error {
+ if view.Host != owner.Host || view.Barrier != nil || !slices.Contains(view.Retired, hostguard.Captured{Incarnation: owner.Incarnation, Stamp: owner.Stamp}) {
+ return os.ErrPermission
+ }
+ for _, receipt := range view.Receipts {
+ if generationOwner(receipt.Target) == owner && verifyRetirementReceipt(ctx, receipt) == nil {
+ return nil
+ }
+ }
+ return os.ErrPermission
+}
+
+func productionRetiredGenerationAbsence(ctx context.Context, r release.GenerationReservation, view hostguard.FenceView) (string, error) {
+ if r.Binding.Path != release.VersionPath(release.OptDir(), r.Binding.Binding.Account, r.Binding.Version, r.Binding.Binding.App) {
+ return "", os.ErrPermission
+ }
+ if !isolatedGeneration(r) {
+ return productionGenerationAbsence(ctx, r)
+ }
+ t := r.Binding.Target
+ if !slices.Contains(view.Retired, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}) {
+ return productionGenerationAbsence(ctx, r)
+ }
+ if err := retiredGenerationReceipt(ctx, view, t); err != nil {
+ return "", err
+ }
+ if os.Geteuid() != 0 {
+ return "", os.ErrPermission
+ }
+ if err := verifyStopKernelMounts("/sys/fs/cgroup", "/proc"); err != nil {
+ return "", err
+ }
+ current, err := generationRecordedAbsence(ctx, r)
+ if err != nil {
+ return "", err
+ }
+ boot, err := (generationKernel{proc: "/proc"}).boot()
+ if err != nil {
+ return "", err
+ }
+ return generationInventoryDigest([]any{"retired-isolated", current, boot, view.Receipts}), nil
+}
+
+func retiredGenerationSnapshot(ctx context.Context, inv generationInventory, records []release.GenerationReservation) bool {
+ if len(inv.Installations) == 0 || len(inv.Launches) == 0 {
+ return false
+ }
+ views := map[string]hostguard.FenceView{}
+ for _, view := range inv.Installations {
+ in := view.Installation
+ if view.Barrier != nil || !slices.Contains(view.Retired, hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp}) {
+ return false
+ }
+ views[view.Host] = view
+ }
+ for _, r := range records {
+ if r.Retirement == "" || !r.Closed || !isolatedGeneration(r) || retiredGenerationReceipt(ctx, views[r.Binding.Target.Host], r.Binding.Target) != nil {
+ return false
+ }
+ }
+ for _, launch := range inv.Launches {
+ if !launch.WritersClosed || !launch.Completed.Valid() || retiredGenerationReceipt(ctx, views[launch.Target.Host], generationOwner(launch.Target)) != nil {
+ return false
+ }
+ for _, r := range launch.Reservations {
+ if !hostbox.IsUnitReservation(r) || r.State != hostbox.UnitSettled || !hostbox.SameLaunchOwner(r.Target, launch.Target) {
+ return false
+ }
+ matches := 0
+ for _, pin := range records {
+ if pin.Binding.Target == generationOwner(launch.Target) && pin.Binding.Attempt == r.ID {
+ matches++
+ }
+ }
+ if matches != 1 {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+func emptyGenerationGroups(ctx context.Context, root string) error {
+ count := 0
+ return filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
+ if path == root && errors.Is(err, os.ErrNotExist) {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ count++
+ if count > 8192 || entry.Type()&os.ModeSymlink != 0 {
+ return os.ErrPermission
+ }
+ if !entry.IsDir() {
+ return nil
+ }
+ raw, err := os.ReadFile(filepath.Join(path, "cgroup.procs"))
+ if err != nil {
+ return err
+ }
+ if strings.TrimSpace(string(raw)) != "" {
+ return os.ErrPermission
+ }
+ return nil
+ })
+}
+
+func productionRetiredGenerationKernelCensus(ctx context.Context) (string, string, error) {
+ records, ok := ctx.Value(generationCensusReservationsKey{}).([]release.GenerationReservation)
+ if !ok {
+ return "", "", os.ErrPermission
+ }
+ inv, err := productionLocalGenerationInventory(ctx)
+ if err != nil {
+ return "", "", err
+ }
+ if !retiredGenerationSnapshot(ctx, inv, records) {
+ return productionGenerationKernelCensus(ctx)
+ }
+ if os.Geteuid() != 0 {
+ return "", "", os.ErrPermission
+ }
+ if err := verifyStopKernelMounts("/sys/fs/cgroup", "/proc"); err != nil {
+ return "", "", err
+ }
+ k := generationKernel{proc: "/proc"}
+ boot, err := k.boot()
+ if err != nil {
+ return "", "", err
+ }
+ for _, launch := range inv.Launches {
+ b := release.GenerationBinding{Target: generationOwner(launch.Target), ControllerPID: launch.ControllerPID, ControllerStart: launch.ProcessStart}
+ if err := k.controllerGone(ctx, b); err != nil {
+ return "", "", err
+ }
+ for _, group := range launch.Groups {
+ if !filepath.IsLocal(group.RelativePath) || group.RelativePath == "." || filepath.Clean(group.RelativePath) != group.RelativePath {
+ return "", "", os.ErrPermission
+ }
+ if err := emptyGenerationGroups(ctx, filepath.Join("/sys/fs/cgroup", group.RelativePath)); err != nil {
+ return "", "", err
+ }
+ }
+ }
+ if err := emptyGenerationGroups(ctx, "/sys/fs/cgroup/carlos.slice/carlos-instances.slice"); err != nil {
+ return "", "", err
+ }
+ if err := checkedGenerationTree(ctx, filepath.Join(release.OptDir(), "versions")); err != nil {
+ return "", "", err
+ }
+ after, err := k.boot()
+ if err != nil || after != boot {
+ return "", "", os.ErrPermission
+ }
+ return boot, generationInventoryDigest([]any{"retired-isolated-census", inv, records, boot}), nil
+}
diff --git a/internal/host/rootgeneration_retired_linux_test.go b/internal/host/rootgeneration_retired_linux_test.go
new file mode 100644
index 000000000..b613d39e7
--- /dev/null
+++ b/internal/host/rootgeneration_retired_linux_test.go
@@ -0,0 +1,104 @@
+//go:build linux
+
+package host
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func TestPlan14RetiredGenerationBoundary(t *testing.T) {
+ for _, mode := range []string{"retired", "live installation", "pending pin", "open pin", "named", "legacy process", "open writers", "pending unit", "foreign receipt", "bad receipt", "barrier", "missing installation", "missing launches", "missing reservation"} {
+ t.Run(mode, func(t *testing.T) {
+ target := hostguard.FenceTarget{Host: "shop.example", Incarnation: "inc", Stamp: 3, Session: 4, Box: "box", Enrollment: "en", BootID: "boot", ControllerID: "controller", FenceID: "fence", Op: "delete", ObligationID: "park"}
+ at := time.Now().UTC()
+ s := StopReceipt{Kind: "stopped", Host: target.Host, Incarnation: target.Incarnation, InstallationStamp: target.Stamp, WriterSession: target.Session, Box: target.Box, Enrollment: target.Enrollment, BootID: target.BootID, ControllerID: target.ControllerID, FenceID: target.FenceID, OperationID: target.Op, ObligationID: target.ObligationID, EvidenceAt: at}
+ view := hostguard.FenceView{Host: target.Host, Installation: hostguard.RootInstallation{Host: target.Host, Incarnation: target.Incarnation, Stamp: target.Stamp}, Retired: []hostguard.Captured{{Incarnation: target.Incarnation, Stamp: target.Stamp}}, Receipts: []hostguard.FenceReceipt{{Target: target, Bytes: mustMarshalStopReceipt(s), EvidenceAt: at}}}
+ inv := generationInventory{Installations: []hostguard.FenceView{view}, Launches: []hostbox.LaunchInventory{{Target: target, WritersClosed: true, Reservations: []hostbox.LaunchReservation{{Target: target, State: hostbox.UnitSettled, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(target.Host)}}}}}}
+ records := []release.GenerationReservation{{Binding: release.GenerationBinding{Target: generationOwner(target)}, Closed: true, Retirement: strings.Repeat("a", 64)}}
+ switch mode {
+ case "live installation":
+ inv.Installations[0].Retired = nil
+ case "pending pin":
+ records[0].Retirement = ""
+ case "open pin":
+ records[0].Closed = false
+ case "named":
+ records[0].Binding.Unit = "console.service"
+ case "legacy process":
+ records[0].Process.PID = 10
+ case "open writers":
+ inv.Launches[0].WritersClosed = false
+ case "pending unit":
+ inv.Launches[0].Reservations[0].State = hostbox.UnitIntended
+ case "foreign receipt":
+ inv.Installations[0].Receipts[0].Target.Session++
+ case "bad receipt":
+ inv.Installations[0].Receipts[0].Bytes = []byte("{}")
+ case "barrier":
+ inv.Installations[0].Barrier = &target
+ case "missing installation":
+ inv.Installations = nil
+ case "missing launches":
+ inv.Launches = nil
+ case "missing reservation":
+ records = nil
+ }
+ if got := retiredGenerationSnapshot(t.Context(), inv, records); got != (mode == "retired") {
+ t.Fatalf("boundary = %v", got)
+ }
+ })
+ }
+}
+
+func TestPlan14RetiredGenerationGroups(t *testing.T) {
+ root := filepath.Join(t.TempDir(), "groups")
+ if err := emptyGenerationGroups(t.Context(), root); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.MkdirAll(filepath.Join(root, "unknown"), 0700); err != nil {
+ t.Fatal(err)
+ }
+ for _, group := range []string{root, filepath.Join(root, "unknown")} {
+ if err := os.WriteFile(filepath.Join(group, "cgroup.procs"), nil, 0600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := emptyGenerationGroups(t.Context(), root); err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(root, "unknown", "cgroup.procs")
+ if err := os.WriteFile(path, []byte("42\n"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := emptyGenerationGroups(t.Context(), root); err == nil {
+ t.Fatal("unrecorded managed process ignored")
+ }
+ if err := os.Remove(path); err != nil {
+ t.Fatal(err)
+ }
+ if err := emptyGenerationGroups(t.Context(), root); err == nil {
+ t.Fatal("missing kernel evidence accepted")
+ }
+}
+
+func TestPlan14GenerationEarlyBootStart(t *testing.T) {
+ for _, start := range []string{"0", "1", "-1", "not-a-number", "18446744073709551616"} {
+ got, err := generationStart([]byte("1 (init) S " + strings.Repeat("0 ", 18) + start))
+ if start == "0" || start == "1" {
+ if err != nil || got != start {
+ t.Fatalf("start %s: %s %v", start, got, err)
+ }
+ } else if err == nil {
+ t.Fatalf("invalid start accepted: %s", start)
+ }
+ }
+}
diff --git a/internal/host/rootgeneration_retired_other.go b/internal/host/rootgeneration_retired_other.go
new file mode 100644
index 000000000..56b4414fd
--- /dev/null
+++ b/internal/host/rootgeneration_retired_other.go
@@ -0,0 +1,17 @@
+//go:build !linux
+
+package host
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func productionRetiredGenerationAbsence(ctx context.Context, r release.GenerationReservation, _ hostguard.FenceView) (string, error) {
+ return productionGenerationAbsence(ctx, r)
+}
+
+func productionRetiredGenerationKernelCensus(ctx context.Context) (string, string, error) {
+ return productionGenerationKernelCensus(ctx)
+}
diff --git a/internal/host/rootgeneration_retirement.go b/internal/host/rootgeneration_retirement.go
index 59d3ee5ce..c1091924d 100644
--- a/internal/host/rootgeneration_retirement.go
+++ b/internal/host/rootgeneration_retirement.go
@@ -11,9 +11,10 @@ import (
)
type generationRetirement struct {
- guard hostguard.Guard
- journal *hostbox.LaunchJournal
- kernel func(context.Context, release.GenerationReservation) (string, error)
+ guard hostguard.Guard
+ journal *hostbox.LaunchJournal
+ kernel func(context.Context, release.GenerationReservation) (string, error)
+ retiredKernel func(context.Context, release.GenerationReservation, hostguard.FenceView) (string, error)
}
func (v generationRetirement) closure(ctx context.Context, r release.GenerationReservation) (bool, error) {
@@ -109,6 +110,16 @@ func (v generationRetirement) verify(ctx context.Context, r release.GenerationRe
// The caller holds release exclusion; host exclusion remains held through both
// ledger commits and the kernel check, in the same order used by launch.
func (v generationRetirement) retire(ctx context.Context, g *release.RootGenerations, r release.GenerationReservation) error {
+ if v.retiredKernel != nil {
+ t := r.Binding.Target
+ view, err := v.guard.Root.View(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp})
+ if err != nil {
+ return err
+ }
+ v.kernel = func(ctx context.Context, current release.GenerationReservation) (string, error) {
+ return v.retiredKernel(ctx, current, view)
+ }
+ }
return v.guard.WithLocal(ctx, r.Binding.Binding.Host, func() error {
closed, err := v.closure(ctx, r)
if err != nil || !closed {
@@ -137,7 +148,7 @@ func productionRetireGenerations(ctx context.Context, g *release.RootGenerations
return err
}
defer journal.Close()
- verifier := generationRetirement{guard: hostguard.Guard{DataDir: dataDir(), Root: hostguard.NewRootAuthority(state)}, journal: journal, kernel: productionGenerationAbsence}
+ verifier := generationRetirement{guard: hostguard.Guard{DataDir: dataDir(), Root: hostguard.NewRootAuthority(state)}, journal: journal, kernel: productionGenerationAbsence, retiredKernel: productionRetiredGenerationAbsence}
return verifier.retireAll(ctx, g, records)
}
diff --git a/internal/host/rootgeneration_retirement_lock_test.go b/internal/host/rootgeneration_retirement_lock_test.go
new file mode 100644
index 000000000..f329ee2e2
--- /dev/null
+++ b/internal/host/rootgeneration_retirement_lock_test.go
@@ -0,0 +1,62 @@
+package host
+
+import (
+ "context"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+type retiredReceiptLockStore struct {
+ *rollbackDisk
+ held bool
+}
+
+func (s *retiredReceiptLockStore) Lock(context.Context, string) (func(), error) {
+ if s.held {
+ return nil, os.ErrPermission
+ }
+ s.held = true
+ return func() { s.held = false }, nil
+}
+
+func TestPlan14GenerationRetirementReceiptLockOrder(t *testing.T) {
+ f, disk, _ := generationLaunchFixture(t)
+ in, err := f.root.Guard.Root.Installation(t.Context(), f.target.Host, hostguard.Captured{Incarnation: f.target.Incarnation, Stamp: f.target.Stamp})
+ if err != nil {
+ t.Fatal(err)
+ }
+ state := &retiredReceiptLockStore{rollbackDisk: &rollbackDisk{dir: t.TempDir()}}
+ f.root.Guard.Root = hostguard.NewRootAuthority(state)
+ if err := f.root.Guard.EstablishRoot(t.Context(), in, func(*hostclaim.Claim) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil {
+ t.Fatal(err)
+ }
+ r := readGenerationLaunch(t, disk)
+ if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil {
+ t.Fatal(err)
+ }
+ if err := f.journal.CloseWriters(t.Context(), f.target); err != nil {
+ t.Fatal(err)
+ }
+ calls := 0
+ v := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { return "", os.ErrPermission }, retiredKernel: func(_ context.Context, _ release.GenerationReservation, view hostguard.FenceView) (string, error) {
+ if !state.held || view.Host != f.target.Host {
+ t.Fatal("retirement verification escaped exclusion")
+ }
+ calls++
+ return strings.Repeat("a", 64), nil
+ }}
+ if err := v.retire(t.Context(), &release.RootGenerations{Store: disk}, r); err != nil {
+ t.Fatal(err)
+ }
+ if calls != 1 || state.held {
+ t.Fatalf("verification calls=%d lock=%v", calls, state.held)
+ }
+}
diff --git a/internal/host/rootgenerationpins.go b/internal/host/rootgenerationpins.go
index 31ba8526a..80664a138 100644
--- a/internal/host/rootgenerationpins.go
+++ b/internal/host/rootgenerationpins.go
@@ -20,7 +20,7 @@ func productionRootGenerationPins() release.PinSource {
s := generationPinSource{legacy: legacyRootRestart, retire: productionRetireGenerations, open: func() (rootstate.Store, error) {
return rootstate.Open(release.RootGenerationsDir, false, false)
}}
- s.reconcile = (generationReconciler{read: productionGenerationInventory, kernel: productionGenerationKernelCensus}).reconcile
+ s.reconcile = (generationReconciler{read: productionGenerationInventory, kernel: productionRetiredGenerationKernelCensus}).reconcile
return s
}
diff --git a/internal/hostclaim/claim.go b/internal/hostclaim/claim.go
index 56ff9abdf..ee4caffcd 100644
--- a/internal/hostclaim/claim.go
+++ b/internal/hostclaim/claim.go
@@ -155,8 +155,9 @@ type CancelledCreate struct {
}
type Claim struct {
- Origins *OriginHead `json:"admitted_origins,omitempty"`
- originAdmission *originTuple
+ RetainedAdmissionLosses []RetainedAdmissionLoss `json:"retained_admission_losses,omitempty"`
+ Origins *OriginHead `json:"admitted_origins,omitempty"`
+ originAdmission *originTuple
RetiredLocations map[string]string `json:"retired_locations,omitempty"`
EvidenceRefs []AutomaticLoss `json:"evidence_refs,omitempty"`
@@ -234,6 +235,9 @@ func (c *Claim) Validate() error {
if c == nil || c.V != schemaVersion || !controlname.ValidHostShape(c.Host) || c.Gen == 0 {
return fmt.Errorf("%w: header", ErrMalformed)
}
+ if err := validateRetainedAdmissionLosses(c); err != nil {
+ return err
+ }
if err := validateRelocating(c); err != nil {
return err
}
diff --git a/internal/hostclaim/delete.go b/internal/hostclaim/delete.go
index bc547f644..cb1b8657d 100644
--- a/internal/hostclaim/delete.go
+++ b/internal/hostclaim/delete.go
@@ -122,7 +122,7 @@ func FreeAfterPark(opID string) func(*Claim) error {
if c.Op.Kind != KindParking || len(c.Obligations) > 0 {
return fmt.Errorf("%w: kind %q, %d obligations left", ErrWrongState, c.Op.Kind, len(c.Obligations))
}
- if err := RequireLossEvidenceArchived(c); err != nil {
+ if err := retainNativeAdmissionLosses(c); err != nil {
return err
}
c.Holder, c.Op, c.Touched, c.Inventory = nil, Op{}, Touched{}, nil
diff --git a/internal/hostclaim/retainedloss.go b/internal/hostclaim/retainedloss.go
new file mode 100644
index 000000000..43024581a
--- /dev/null
+++ b/internal/hostclaim/retainedloss.go
@@ -0,0 +1,45 @@
+package hostclaim
+
+type RetainedAdmissionLoss struct {
+ Incarnation string `json:"incarnation"`
+ Location string `json:"location"`
+ Account string `json:"account"`
+ App string `json:"app"`
+ Admission AdmissionRecord `json:"admission"`
+}
+
+func retainNativeAdmissionLosses(c *Claim) error {
+ if c.Holder == nil {
+ return nil
+ }
+ h := c.Holder
+ var retained []RetainedAdmissionLoss
+ for _, a := range h.Admissions {
+ if admissionLossArchived(a) {
+ continue
+ }
+ if a.LossID != "" {
+ return ErrLossEvidencePending
+ }
+ retained = append(retained, RetainedAdmissionLoss{
+ Incarnation: h.Incarnation, Location: h.Location, Account: h.Account, App: h.App, Admission: a,
+ })
+ }
+ c.RetainedAdmissionLosses = append(c.RetainedAdmissionLosses, retained...)
+ return validateRetainedAdmissionLosses(c)
+}
+
+func validateRetainedAdmissionLosses(c *Claim) error {
+ seen := map[uint64]bool{}
+ for _, r := range c.RetainedAdmissionLosses {
+ a := r.Admission
+ if !isHex64(r.Incarnation) || r.Account == "" || r.App == "" ||
+ (r.Location != "instances/"+c.Host && r.Location != "instances/"+c.Host+"/"+r.Incarnation) ||
+ a.Session == 0 || a.Session > c.Gen || seen[a.Session] || a.Region == "" || a.LossID != "" ||
+ !admissionHasLoss(a) || !validWriteLosses(a.WritesLost, a.AdditionalWritesLost) || a.LossArchivedClaimGen != 0 {
+ return ErrMalformed
+ }
+ seen[a.Session] = true
+ }
+ return nil
+}
diff --git a/internal/hostclaim/retainedloss_test.go b/internal/hostclaim/retainedloss_test.go
new file mode 100644
index 000000000..0305085f7
--- /dev/null
+++ b/internal/hostclaim/retainedloss_test.go
@@ -0,0 +1,112 @@
+package hostclaim
+
+import (
+ "context"
+ "errors"
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+type retainedLossStore struct {
+ release.Store
+ after bool
+}
+
+func (s retainedLossStore) Put(ctx context.Context, key string, body []byte, match string) (string, error) {
+ if s.after {
+ if _, err := s.Store.Put(ctx, key, body, match); err != nil {
+ return "", err
+ }
+ }
+ return "", release.ErrCASConflict
+}
+
+func TestNativeAdmissionLossSurvivesRetirement(t *testing.T) {
+ for _, failure := range []string{"none", "before-CAS", "lost-reply"} {
+ t.Run(failure, func(t *testing.T) {
+ ctx := t.Context()
+ s := release.NewDirStore(t.TempDir())
+ o := servingClaim(t, ctx, s)
+ for _, generation := range []string{"first", "second"} {
+ h := o.Claim.Holder
+ var err error
+ o, err = Update(ctx, s, o, AdmitWriter(Admission{Region: "ie", Box: "adi-knitting", Mode: "cold", NewGeneration: generation, ObservedSession: h.Session, ObservedGeneration: h.Generation, RemoteMaxTXID: 42}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ h := o.Claim.Holder
+ want := RetainedAdmissionLoss{Incarnation: h.Incarnation, Location: h.Location, Account: h.Account, App: h.App, Admission: h.Admissions[1]}
+ if want.Admission.WritesLost == nil {
+ t.Fatal("cold wake did not declare predecessor loss")
+ }
+ var err error
+ o, err = Update(ctx, s, o, BeginDelete("delete", "test", "etag", time.Now(), nil, nil, ""))
+ if err != nil {
+ t.Fatal(err)
+ }
+ o, err = Update(ctx, s, o, ToParking("delete", nil, nil, counter()))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err = Update(ctx, s, o, FreeAfterPark("delete")); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("outstanding parks: %v", err)
+ }
+ for _, ob := range o.Claim.Obligations {
+ o, err = Update(ctx, s, o, CompleteObligation(ob.Op, ob.ID))
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ if failure != "none" {
+ _, err = Update(ctx, retainedLossStore{s, failure == "lost-reply"}, o, FreeAfterPark("delete"))
+ if !errors.Is(err, ErrConflict) {
+ t.Fatalf("fault: %v", err)
+ }
+ o, err = Read(ctx, s, testHost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if failure == "before-CAS" && (o.Claim.Holder == nil || len(o.Claim.RetainedAdmissionLosses) != 0) {
+ t.Fatal("failed CAS changed evidence")
+ }
+ }
+ if !o.Claim.Free() {
+ o, err = Update(ctx, s, o, FreeAfterPark("delete"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ o, err = Read(ctx, s, testHost)
+ if err != nil || !o.Claim.Free() || !reflect.DeepEqual(o.Claim.RetainedAdmissionLosses, []RetainedAdmissionLoss{want}) {
+ t.Fatalf("retained loss: %+v, %v", o.Claim, err)
+ }
+ for i := 0; i < retiredCap+1; i++ {
+ holder := newHolder()
+ holder.CreatedAt = holder.CreatedAt.Add(time.Duration(i+1) * time.Hour)
+ o, err = Update(ctx, s, o, BeginCreate(holder, []byte(strings.Replace(string(record), "2026-10-04T01:00:00Z", holder.CreatedAt.Format(time.RFC3339), 1)), "create", "test", time.Now()))
+ if err != nil {
+ t.Fatal(err)
+ }
+ o, err = Update(ctx, s, o, CancelCreate("create", time.Now()))
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ if !reflect.DeepEqual(o.Claim.RetainedAdmissionLosses, []RetainedAdmissionLoss{want}) {
+ t.Fatal("holder replacement or retired cap erased loss")
+ }
+ })
+ }
+}
+
+func TestNativeRetentionDoesNotReplaceSignedLossArchive(t *testing.T) {
+ c := &Claim{Holder: &Holder{Admissions: []AdmissionRecord{{Session: 3, LossID: "loss_1", WritesLost: &WritesLost{Box: "box"}}}}}
+ if err := retainNativeAdmissionLosses(c); !errors.Is(err, ErrLossEvidencePending) || len(c.RetainedAdmissionLosses) != 0 {
+ t.Fatalf("signed archive bypass: %v", err)
+ }
+}
diff --git a/internal/provision/plan14_p3_census_test.go b/internal/provision/plan14_p3_census_test.go
index 6284b9293..2f90ba805 100644
--- a/internal/provision/plan14_p3_census_test.go
+++ b/internal/provision/plan14_p3_census_test.go
@@ -23,6 +23,9 @@ func plan14P3WriteCensusInventory() []censusEntry {
entries := plan14EWriteCensusInventory()
kept := make([]censusEntry, 0, len(entries))
for _, e := range entries {
+ if e.File == "internal/hostclaim/claim.go" && e.Unit == "Claim.Validate" {
+ continue
+ }
if e.File == "internal/host/rootexecution_authority.go" && e.Unit == "ClaimRootHTTP.executionAuthority" {
continue
}
@@ -50,6 +53,8 @@ func plan14P3WriteCensusInventory() []censusEntry {
kept = append(kept, e)
}
return append(kept,
+ censusEntry{"internal/hostclaim/claim.go", "Claim.Validate", "key-site", "lit:instances", "ffeb10f34954f5b4c8566ee1d66a89a99eb303a8611e4671696dd312c5296c29"},
+ censusEntry{"internal/hostclaim/retainedloss.go", "validateRetainedAdmissionLosses", "reference", "lit:instances", "b32ddaed69411feaa06c45e84287fb1c09b0168498231a5386e3acd937d335e0"},
censusEntry{"internal/host/rootexecution_authority.go", "ClaimRootHTTP.executionAuthority", "key-site", "key:RecordKey,lit:instances", "eff2c5806bff1c3d4beec4f54513b71e3890a4970b4b03d1f6a200db0146109e"},
censusEntry{"internal/host/claimproduction.go", "productionClaimRegional.View", "reference", "lit:instances", "bcefe9f8fd6420e795ef95a6170fbad1030e29f2261bcc2d677f76b6f6d577ce"},
censusEntry{"internal/host/claimordinaryparks.go", "productionClaimRegional.ordinaryHome", "reference", "lit:instances", "fa96f75f8af5fd1e3ba3dff81e27ac0fb7c6029663472292d698bfddaec99fe8"},
diff --git a/internal/provision/testdata/plan14-root-reads-p3.tsv b/internal/provision/testdata/plan14-root-reads-p3.tsv
index 1a33c1993..fda92f7d6 100644
--- a/internal/provision/testdata/plan14-root-reads-p3.tsv
+++ b/internal/provision/testdata/plan14-root-reads-p3.tsv
@@ -324,7 +324,7 @@ internal/host/rootmigration_production_linux.go quiesceLegacyRootMutators author
internal/host/rootmigration_startup.go readRootMigrationState authority G rev3=PROPOSED-p3;run=13;identity=root startup/recovery uid 0;basis=validates retained census box/enrollment/generation and complete migration record before restricted recovery; grants no launch file:Read@425 9a8ddf2b605833b7fa4c0908ade98a6fc2efbe87c26e92ede289bbcc1d05beec
internal/host/rootmovesfloor_boot.go sameRootFloorImage authority F rev3=PROPOSED-p3;run=13;identity=root boot/recovery uid 0;basis=compares freshly verified signed digest, release and protocols, not saved boot-local file numbers 532078238827331f9282d85d1e17e146ed4842a215db057238aa5f6cb40ba137
internal/host/rootreleaselock.go withClaimReleaseCensus authority E2 rev3=PROPOSED-p3;run=15;identity=root adopt/guard uid 0;basis=no census invokes main effect directly without opening release state, creating a file or waiting on any migration lock; participating effects retain release exclusion 35bcc5e18e89f9755cf1f749bbaa4cbf493380b75500183687476abd71c946b6
-internal/host/claimactivation_preflight.go claimActivationLocalUnits authority G rev3=PROPOSED-p3;run=15;identity=agent uid carlos; root migration/adopt/control uid 0;basis=refusal-only inventory of backing unit instances, local named services and drop-ins, legacy app environment configuration and both systemd backing state directories; registry omission cannot confer eligibility file:ReadDir@215,file:ReadFile@1206,file:ReadFile@1289,file:WalkDir@603 bfe1faa17fcbe6d2ea25252698e925e26616b6cdaef55759de0c291b1b30749b
+internal/host/claimactivation_preflight.go claimActivationLocalUnits authority G rev3=PROPOSED-p3;run=22;identity=agent uid carlos; root migration/adopt/control uid 0;basis=refusal-only local named-unit inventory; agent skips only ordinary claim runtime drop-in directories under /run/systemd/system, whose private contents root still inventories and verifies before launch; legacy and named units remain checked file:ReadDir@215,file:ReadFile@1484,file:ReadFile@1567,file:WalkDir@603 6d7e6a2228a88e7058e70a0d1cd04606eb75f52e008e5fd20efb7563335f4258
internal/host/claimassembly.go ClaimAssembly.Build authority C,G rev3=PROPOSED-p3;run=18;identity=agent uid carlos; root instance-control uid 0;basis=captures provisioning only after participation hooks are bound; settles the previous operation before inventory activation and checks again before selected writes file:Prepare@3817,file:Prepare@3850,file:Prepare@3866,file:Prepare@3883,file:Prepare@4632,file:Read@4562,whole:Timer@2044 f75fa7f91404c318a30755cd7935e7e404bbe8f41b9cfd71184cd12518f15e0e
internal/host/rootgeneration_inventory.go readProductionGenerationInventory authority E2 rev3=PROPOSED-p3;run=16;identity=root claim/adoption controller uid 0;basis=verifies retained completed serving-handover journals against both root payloads, retired predecessor, exact stop receipt, ownership and persistent path identity; follows transfers through terminal retirement without removing launch reservations; TestPlan14ServingHandoverPins and TestPlan14ServingHandoverPinsRootRunner 8810b61878f66ec0c1e31e13918454e5b696129639b8512091f883a038c940fd
internal/host/rootmigration_production_linux.go verifyOfflineRootMigration authority G rev3=PROPOSED-p3;run=15;identity=offline root migration uid 0;basis=requires offline masked inactive adopt, stage, config-sync, restart, update and recovery services and absence of concurrent legacy root mutators before creating census state 300a81411020b33318dd8e15418acc95654c3980eb03c8581276d40a951a1a15
@@ -365,7 +365,7 @@ internal/hostguard/recoveryfence.go Guard.PendingStopFences authority C,J4 rev3=
internal/hostguard/recoveryfence.go Guard.pendingFences authority C,J2,J4,J1 rev3=PROPOSED-p3;run=19;identity=root;basis=reads root-owned records under host lock and keeps exact barrier classification separate from ordinary stop recovery; legacy behavior retained 20be9535fd54587a54e3027f5388581ae5a64e5fcfced5a6031ffc9a4fc76a3b
internal/host/claimroleassembly_linux.go verifiedClaimExecutable authority C,G,J4 rev3=PROPOSED-p3;run=19;identity=agent uid carlos;basis=pins root-owned executable bytes and keeps the verified descriptor while supplying its absolute path to the root launcher file:Fstat@1122,file:Fstat@579,file:OpenFile@376,file:Read@949 7a7e9af520e4157e60ae93d12c3268a629a9b0bd1e83b263cdd27103cf2e227c
internal/host/claimroleassembly_linux.go claimExecutableName authority C,G,J4 rev3=PROPOSED-p3;run=19;identity=agent uid carlos;basis=duplicates the existing descriptor with CLOEXEC and its validated absolute name; never reopens a potentially replaced path e887880731a16c9e7934aef33a08169b34e7236f6d3b85a195602d35f8b69107
-@coverage internal/host 42ea2ed51b8dea0e178285250d5eec164f83ae39614b5c686e19346f1945fb85
+@coverage internal/host 3a4ed551293be2dba348f018a1907df5d81d5e9e54413276e18c8b2927332e6c
@coverage internal/hostbox 23dce8b43c105e281a8e93f2c70c969e48e1db448632c8c72d21b0ed273cc246
@coverage internal/hostguard 1b3341e3f1a3b56c46b1a85ec5e12cb08e0c26128b1b5c865a562ab5aa0994fb
internal/host/claimstop.go ClaimStopCoordinator.FenceAndStop authority C,J4 rev3=PROPOSED-p3;run=20;identity=root instance-control uid 0;basis=authenticated session stop retains root fence before independent unjournaled absence check; proves no journaled installation, containment or open files before bypassing nonexistent writer closure; normal drain remains for recorded launches; no-census unchanged file:Read@1610 58147f3abbf278037fc660392aeec0b6e76b667e3fe8435845ebaedc62d2ead1
@@ -418,9 +418,29 @@ internal/park/sweep.go protected authority C,E2,J4 rev3=PROPOSED-p3;run=21;ident
internal/park/remote.go Engine.selected authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=claim regional worker service UID under root stop or signed retirement evidence;basis=ordinary generation retirement excludes exact live file versions from fresh selection and revalidates on manifest replay; replay selection mismatch refuses deletion 43dd4b71ef97868599cf56560a458026ac77bada65f75b314630c436e57112ca
@coverage internal/park b611c74d7f75c985a1546f0b79b2f7db79312a3d3d2450981c5184c04c8e324e
internal/host/rootexecution_authority.go ClaimRootHTTP.executionAuthority authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=root authenticated execution broker uid 0;basis=ordinary home delete and parking participation only, exact claim root installation inventory and record version or verified absence; completed stops never confer execution file:Exec@1657,file:Read@553 eff2c5806bff1c3d4beec4f54513b71e3890a4970b4b03d1f6a200db0146109e
-internal/host/rootexecution_settle.go ClaimRootHTTP.cleanupExecution authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=root authenticated execution broker uid 0;basis=retained exact root installation authorizes participation settlement after retirement; canonical stop receipts and kernel checks retained file:Read@2928,file:Read@4251 5f185b6f1bc2ba75d63d5bbd865c983a92b98cdaa8d1f23110f60661ad24b9a9
+internal/host/rootexecution_settle.go ClaimRootHTTP.cleanupExecution authority C,E2,J4 rev3=PROPOSED-p3;run=22;identity=root execution broker uid 0;basis=canonical root stop receipt and irreversible retired installation plus closed launch journal discharge already-completed cleanup after cgroups vanish; live sessions retain fresh kernel checks and remote claim settlement remains mandatory file:Read@2928,file:Read@4251 3a7091afec06f61182a80bd59475d5005296ac1ac06a1e9c9ab6d48bd9a24810
internal/hostclaim/delete.go ToParking authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=production console or local regional worker using authenticated operation;basis=console ordinary delete transition under claim CAS; sets the previously empty ordinary deletion phase to park so exact root stop tuples can retire it; existing nonempty phases retained 9d5d3cf3fd1255e4a3cda50cedcd65197f65155ea4bfa57d50670ebad0e5492a
internal/hostbox/renewing.go renewingHeld.Release authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=production console or local regional worker using authenticated operation;basis=regional park worker release of held lease; cancels work immediately then issues exact conditional lease deletion under detached request bounded cleanup context; grants no new work 0cfd55e1a0f40851dd27be382187fc01cfab5dc2bca54262966f6a66a2799e39
-@coverage internal/hostclaim cd49f5e0ac3870767e3b6caba3159063150e7940cbd183f47451fa86e3b29979
+@coverage internal/hostclaim 5fc93a54af58686466e4f5f61f42aed8bb3e7d75038ecf48dc5faafa9e3ee83c
internal/host/claimstop_producers.go ClaimProducers.DrainClaim authority C,E2,J4 rev3=PROPOSED-p3;run=21;identity=authenticated local writer controller answering root stop;basis=only a fully joined and compacted drain may acknowledge a new fence for the same exact writer; unfinished drains and foreign tuples refuse; producer admission remains closed a3633b7f153cf84f565d3d6d19db1e38eba5f578de57b9a65900fd74a837c5cf
internal/hostbox/typedpark_path.go TypedParker.ParkedArtifactRoot authority C,J2,J4 rev3=PROPOSED-p3;run=21;identity=root retirement or generation verifier uid 0;basis=only configured root or recorded strict descendant mount containing artifact; malformed or unrelated mount paths refuse;basis=recorded mount relocation only for CapturePersistent root-protected artifacts; legacy destinations unchanged e4fe742ac8e421856f66729d888e81f828f8679fee6df93a310e0948d34cca48
+internal/host/claimparticipation.go productionClaimLocal.cleanup authority C,D rev3=PROPOSED-p3;run=22;identity=agent uid carlos;basis=census-only exact retired-installation discharge precedes stale-session stop; authenticated root retirement receipt permits local close, root broker independently checks cleanup and settlement d77ed95879a026037604ce9024dc9c614d5d36597b26ff2503d6858d41c0da71
+internal/host/claimparticipation_retired.go ClaimAssembly.completedRetirementDischarge authority C,D rev3=PROPOSED-p3;run=22;identity=agent uid carlos;basis=authenticated root fence view and exact canonical retirement receipt bind host incarnation stamp session operation box and enrollment; missing or substituted receipt refuses discharge and root settlement retains kernel checks 75b827cf499f524ead84c6824c328f7ac702854c53a8570dfdc0df0bbc168df8
+internal/hostclaim/delete.go FreeAfterPark authority C rev3=PROPOSED-p3;run=22;identity=authenticated console or lifecycle worker through existing claim CAS;basis=conditional holder removal after all parking obligations finish; preserves native loss evidence atomically before removal and keeps the signed-declaration archive gate 76f88d4f0f3bab8094a9426d8d809d8c8b25d8aede5915aaa43f78b0767eaea7
+internal/hostclaim/retainedloss.go retainNativeAdmissionLosses authority C rev3=PROPOSED-p3;run=22;identity=authenticated console or lifecycle worker through existing claim CAS;basis=copies exact native admission evidence and original holder identity into claim-retained state; does not certify losses or relax signed-declaration archival 278b599c5e822d63a2c50bb2e0ba563371564cbdfb9c05531d7efe223aa26d3f
+internal/host/rootexecution_settle.go retiredExecutionLaunch authority C,E2,J4 rev3=PROPOSED-p3;run=22;identity=root execution broker uid 0;basis=exact retired capture and root launch closure only; caller first verifies canonical root receipt and exact inventory; no new stop effect or admission 6ccc8586537ab228f95543307e20caa6023101ffe54e72155e57b36f9d4c3d34
+internal/host/rootgenerationpins.go productionRootGenerationPins authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks file:Open@195 2cf7afda7ffe06c85030ff80c1d1d44b3f67b1589267ca0c0dfde5c81d4827fc
+internal/host/rootgeneration_retirement.go productionRetireGenerations authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks file:Open@292 f1eca8626049f13e079b6fe34f5f38117c7abf97a11105250bc1c4760bbd8f0d
+internal/host/rootgeneration_kernel_linux.go generationStart authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks a10512a1faad1675d091227b19feade6f65a0f834c19be472014fd6f77f7f389
+internal/host/rootgeneration_kernel_linux.go productionGenerationAbsence authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks c5cb757bffece96fa33c1055529427a61bf52fec9213c4bf12d1f6402d2e5a69
+internal/host/rootgeneration_kernel_linux.go generationRecordedAbsence authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks 758367037ce72577707a346b38e3544d851ceb57018eea773a878fb698b14d33
+internal/host/rootgeneration_retired_linux.go isolatedGeneration authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks 4030cee805e5e3c4b08e9e606548cc7c23c4a13898ecd6bffa43e1dc7cf56cbc
+internal/host/rootgeneration_retired_linux.go retiredGenerationReceipt authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks 258b9f88fe29b944d2dfafebcbb383ac9742b4920909eda57c4588d254939377
+internal/host/rootgeneration_retired_linux.go productionRetiredGenerationAbsence authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks;receipt snapshot is read before host exclusion, exact installation and closed journal are freshly rechecked under exclusion before and after verification 48937ef0466c36e18f86e5bb5f3acaa57c88f4fe55b040d73d2b9a096c675fc2
+internal/host/rootgeneration_retired_linux.go retiredGenerationSnapshot authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks 050b409f4b3e21cb71a9343e5bdd20b3c23cc9676ad96291cc3e2093aad6f0e8
+internal/host/rootgeneration_retired_linux.go emptyGenerationGroups authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks file:ReadFile@481,file:WalkDir@89 37e5760c2c9d38725ec8ad24ef3d18830dcbb8c10b0a861e97e486844a4a334d
+internal/host/rootgeneration_retired_linux.go productionRetiredGenerationKernelCensus authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release exclusion;basis=retired ordinary isolated cohort only: canonical root stop receipts, irreversible retired captures, closed journals, exact original process identities gone and all managed instance cgroups empty; live pending named or legacy state retains prior verifier; generic process identity accepts valid Linux zero boot ticks 4574da707f3c09c424b061b879f06ad367d093ce373c418f893c02d932b8aba3
+internal/host/rootgeneration_retired_other.go productionRetiredGenerationAbsence authority E2 rev3=PROPOSED-p3;run=22;identity=non-Linux root caller;basis=delegates to existing unavailable generation verifier; no new non-Linux authority;receipt snapshot is read before host exclusion, exact installation and closed journal are freshly rechecked under exclusion before and after verification a281b05dae1d2b65b0fbcffe6b7cb65002da5dabfdc4f2596bd05511594b3546
+internal/host/rootgeneration_retired_other.go productionRetiredGenerationKernelCensus authority E2 rev3=PROPOSED-p3;run=22;identity=non-Linux root caller;basis=delegates to existing unavailable generation verifier; no new non-Linux authority f61b35d6064ff8a5a4fa757e229167d1b8e71b0109f8d116e0f288c533552a5c
+internal/host/rootgeneration_retirement.go generationRetirement.retire authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 holding release exclusion;basis=read immutable root retirement receipt snapshot before host exclusion; existing exact installation and journal closure checks run under exclusion before and after kernel verification; prevents recursive host locking aad12e0c897c3abab5dd04fc0f9377f56076a6341e42e92a15c4382e3bf22831
+internal/host/rootgeneration_retirement.go type generationRetirement authority E2 rev3=PROPOSED-p3;run=22;identity=root uid 0 under release and host exclusion;basis=retirement verifier carries a separate root-retired proof callback; caller reads canonical receipt before host locking while closure and kernel checks remain inside 132edefce1c8427220dcfee4d8bdcfc8ffa2bd21012cb97b3d330a73af13a93b