carlos / platform Public

Plan 14 integrated: review round 3 fixes P3-R11 items 1-4 (not for merge)

Root settings per exact installation across handover; retired obligations from retained root evidence without a live record; root-verified cancellation at the reserve-before-intention boundary; no-census proof observation as main. Adds the narrowed claim-activation scope (Paul, 2026-10-16) to docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 2b52f331d4ac691bd33644fe6ee6a87d872c4d55 parent 305866a
24 files changed, +1427 −47
  • docs/superpowers/plans/2026-10-16-claim-activation-scope.md +45 −0
  • internal/host/claimservingpark_transport_linux_test.go +3 −1
  • internal/host/plan14_generation_exact_retirement_root_linux_test.go +18 −0
  • internal/host/plan14_generation_parked_root_linux_test.go +33 −0
  • internal/host/plan14_r10_root_linux_test.go +14 −1
  • internal/host/plan14_r11_handover_test.go +194 −0
  • internal/host/plan14_r11_process_test.go +96 −0
  • internal/host/plan14_r11_root_linux_test.go +272 −0
  • internal/host/plan14_r11_test.go +331 −0
  • internal/host/plan14_recovery_without_health_test.go +1 −1
  • internal/host/plan14_root_failures_test.go +2 −2
  • internal/host/proofbinding.go +20 −18
  • internal/host/rootgeneration_handover.go +188 −0
  • internal/host/rootgeneration_inventory.go +11 −4
  • internal/host/rootgeneration_parked.go +53 −1
  • internal/host/rootgeneration_requirements.go +18 −0
  • internal/host/rootgeneration_retirement.go +7 −3
  • internal/host/rootgeneration_rows.go +7 −0
  • internal/host/rootinventory_test.go +20 −1
  • internal/host/rootupdates.go +23 −2
  • internal/hostbox/generationhandover.go +39 −0
  • internal/provision/plan14_p3_census_test.go +10 −2
  • internal/provision/testdata/plan14-root-reads-p3.tsv +21 −10
  • internal/release/adopt.go +1 −1

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	290.194s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.912s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.221s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.704s
ok  	github.com/carlosframework/platform/internal/activator	250.384s
ok  	github.com/carlosframework/platform/internal/agentsock	0.016s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.020s
ok  	github.com/carlosframework/platform/internal/appconfig	2.447s
ok  	github.com/carlosframework/platform/internal/apperr	0.072s
ok  	github.com/carlosframework/platform/internal/applog	0.134s
ok  	github.com/carlosframework/platform/internal/awsinv	0.003s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.646s
?   	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.686s
ok  	github.com/carlosframework/platform/internal/changelog	0.303s
ok  	github.com/carlosframework/platform/internal/clamd	0.055s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	388.044s
?   	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.573s
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.377s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	26.468s
?   	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.130s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.014s
ok  	github.com/carlosframework/platform/internal/geo	0.014s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.150s
ok  	github.com/carlosframework/platform/internal/host	173.390s
ok  	github.com/carlosframework/platform/internal/hostactors	0.365s
ok  	github.com/carlosframework/platform/internal/hostbox	2.570s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.484s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.005s
ok  	github.com/carlosframework/platform/internal/hostexec	4.845s
ok  	github.com/carlosframework/platform/internal/hostfence	0.037s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.449s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.014s
ok  	github.com/carlosframework/platform/internal/hostguard	1.031s
ok  	github.com/carlosframework/platform/internal/hostops	1.547s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.002s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.002s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.437s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.135s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.002s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	5.240s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.736s
ok  	github.com/carlosframework/platform/internal/instancefs	0.007s
ok  	github.com/carlosframework/platform/internal/instancekey	0.002s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.017s
ok  	github.com/carlosframework/platform/internal/layout	1.524s
ok  	github.com/carlosframework/platform/internal/lease	9.787s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.006s
ok  	github.com/carlosframework/platform/internal/ledger	0.013s
ok  	github.com/carlosframework/platform/internal/mail	0.174s
ok  	github.com/carlosframework/platform/internal/maintenance	0.093s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.529s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.069s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	20.981s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.749s
ok  	github.com/carlosframework/platform/internal/objstore	0.193s
ok  	github.com/carlosframework/platform/internal/ops	0.178s
ok  	github.com/carlosframework/platform/internal/park	0.200s
ok  	github.com/carlosframework/platform/internal/privatefile	0.008s
ok  	github.com/carlosframework/platform/internal/provision	34.185s
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.466s
ok  	github.com/carlosframework/platform/internal/relay	7.216s
ok  	github.com/carlosframework/platform/internal/release	21.801s
ok  	github.com/carlosframework/platform/internal/rollout	3.785s
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.057s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/runs	0.574s
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.475s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.423s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.767s
ok  	github.com/carlosframework/platform/internal/status	0.038s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.130s
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.006s
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.545s
=== 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: 48496802 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 08:00:25 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEF3ACE65856A3, 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/docs/superpowers/plans/2026-10-16-claim-activation-scope.md b/docs/superpowers/plans/2026-10-16-claim-activation-scope.md
new file mode 100644
index 000000000..ac406a595
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-16-claim-activation-scope.md
@@ -0,0 +1,45 @@
+# Claim activation scope, NARROWED (Paul's decision, 2026-10-16). BINDING on every builder.
+
+Source: `minimal-boundary-result.md` (Astra). Paul chose to narrow the activation gate, and to activate in cohorts: dev and boxes without named units first, the flagship eu-west-1 console box last. This supersedes the parts of the spec rev 6.34 / Plan 14 activation gate listed below as FREEZE or DROP. Those items are finished AFTER activation, not before it.
+
+## The boundary that must hold before activation (KEEP)
+1. **Root independently authorizes every enabled claim effect** (launch, stop completion, cold handover, retirement, publication and replay): the host, installation, session, operation and authenticated controller are bound. Fences, retirement decisions and lease uncertainty are held outside agent control and rechecked at execution and receipt replay.
+2. **The whole execution configuration is authorized:** executable bytes, the effective unit plus its drop-ins, privileged arguments and environment, identity, mounts, capabilities and containment. Every restart route uses the same reservation boundary.
+3. **Root derives and revalidates destructive targets.** It stops the recorded containment, retires the recorded installation, and removes only exact root-recorded artifacts. Pending and running generation pins stay independent of registry deletion. (Destructive covers deletion, wrong-target stop, overwrite, rename, chown, mounts and publication.)
+4. **A verified, recoverable migration:** admission exclusion; drain and stop existing work; old-session denies retained; a real installation census; no imported legacy "allow".
+5. **Boxes survive ordinary lifecycle events:** seed, cold wake, stop, second wake, reboot, and signed repair or update. Device numbers are boot-local. Floors keep a usable repair executor. Nothing turns routine operation into a permanent, box-wide trap.
+6. **Proven at the real boundaries,** with hostile service-UID inputs, substitution, durability failures at AUTHORITY TRANSITIONS (fence before effect, reservation before launch, retirement intent before mutation, permission and receipt before success), and permitted positive cases through production constructors. No-census boxes keep exactly main's behaviour.
+
+## Per-track decisions
+| Work | Decision |
+|---|---|
+| 4a named units plus console always-running (boot, adoption, restart, recover) | KEEP, but it gates only the eu-west-1 cohort, not the first activation |
+| Unit containment compatibility with the REAL console and woodstar services | KEEP (for eu-west-1) |
+| Containment population on the selected boxes | KEEP |
+| Device audit of active launch, inventory, retirement and unit bindings | KEEP. Other families: FREEZE |
+| E3: canonical attribution and the inputs that authorize claim effects; retry and recovery for root denials already shipped | KEEP |
+| E3: remaining failure/intent producers; exhaustive served/build/completion history | FREEZE. But sticky root state must never block legitimate operation: either complete its delivery and settlement, or gate the sticky state off |
+| E4: protection of claim-bound units on BOTH convergence paths (fixed root configuration, or refusal) | KEEP |
+| E4: the full dynamic `route --grant` workflow | FREEZE |
+| J4: the five E4 slots as mandatory metadata | DROP |
+| J4: exhaustive mapping of about 1,100 authority rows | FREEZE |
+| H2: production boundary witnesses for enabled effects and their shared recovery | KEEP |
+| H2: the whole matrix as one indivisible gate | FREEZE |
+| Floors: enforcement plus a usable boot, update and recovery path | KEEP. Further moves and seeds floor machinery: FREEZE |
+| Historical-record conversion for records that cannot exist at first activation | FREEZE (explicit refusal is enough) |
+| Files seed: the reboot race (fixed in run 31), FS-R11 (done in run 31), and unverifiable retained parks blocking later retirement | KEEP (the last one is an availability trap) |
+| Files seed: full readback, repair, sizing and automatic rehabilitation | FREEZE |
+| #250: C8 provider, admitted provenance, Plan 15 writer, scoped root guard, claim-on launch/isolation/two cold wakes, reader→home→enable order, release key, IAM, scoped rollout | KEEP |
+| #250: cmd role, workers, sidecars, seeded cold install; moves Plans 4/5; moves secrets; #272 | FREEZE |
+| Whole-registry conversion; fixtures for unchanged legacy families | DROP |
+
+## Census and J4 under the narrowed gate
+- The runtime installation census stays.
+- The source-read census test (`TestPlan14RootReadCensus`) stays as it is: it freezes changes.
+- J4 (`TestPlan14RootInputInventoryCoverage`) must check ONLY the families of the ENABLED effects listed in KEEP, plus the exemption list. Every other authority row is reported (t.Log, plus a generated report file), not failed. Do not weaken the check for the enabled families.
+
+## Risks to resolve before shipping
+- No named-unit replacement before always-running startup works on the target box.
+- The unit sandbox must be proven against the real console and woodstar services before eu-west-1.
+- No sticky E3 state without delivery and settlement.
+- Retained parks must not block retirement.
diff --git a/internal/host/claimservingpark_transport_linux_test.go b/internal/host/claimservingpark_transport_linux_test.go
index c5194ca09..907c06bdc 100644
--- a/internal/host/claimservingpark_transport_linux_test.go
+++ b/internal/host/claimservingpark_transport_linux_test.go
@@ -54,7 +54,9 @@ func servingTransportRoot(t *testing.T, f *servingRootFixture) ClaimRootHTTP {
ctx, cancel := context.WithCancel(context.Background())
p := &servingParkRuntime{authority: f.p, state: servingBorrowedState{f.p.ledger.state}, ctx: ctx, cancel: cancel, idle: make(chan struct{})}
t.Cleanup(func() {
- if err := p.Close(t.Context()); err != nil {
+ cleanup, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ if err := p.Close(cleanup); err != nil {
t.Error(err)
}
})
diff --git a/internal/host/plan14_generation_exact_retirement_root_linux_test.go b/internal/host/plan14_generation_exact_retirement_root_linux_test.go
index 3bfa6ced6..1c5c0a9dd 100644
--- a/internal/host/plan14_generation_exact_retirement_root_linux_test.go
+++ b/internal/host/plan14_generation_exact_retirement_root_linux_test.go
@@ -295,6 +295,24 @@ func TestPlan14GenerationExactRetirementRootRunner(t *testing.T) {
if got, err := ledger.Read(v3.Binding); err != nil || got != v3 {
t.Fatalf("pending V3 changed: %+v %v", got, err)
}
+ if err := journal.CloseWriters(ctx, hostguard.FenceTarget{Host: v3.Binding.Target.Host, Incarnation: v3.Binding.Target.Incarnation, Stamp: v3.Binding.Target.Stamp, Session: v3.Binding.Target.Session, Box: v3.Binding.Target.Box, Enrollment: v3.Binding.Target.Enrollment, BootID: v3.Binding.Target.BootID, ControllerID: v3.Binding.Target.ControllerID}); err != nil {
+ t.Fatal(err)
+ }
+ if err := rootstate.WithRelease(ctx, exclusion, func() error {
+ pins, err := source.StagedPins(ctx, dir)
+ if err != nil {
+ return err
+ }
+ if pinned, err := pins.Refers(v3.Binding.Path); err != nil || pinned {
+ t.Fatalf("cancelled prelaunch pin: %v %v", pinned, err)
+ }
+ return nil
+ }); err != nil {
+ t.Fatal("prelaunch cancellation with production kernel", err)
+ }
+ if _, err := ledger.Reserve(v3.Binding); err == nil {
+ t.Fatal("cancelled prelaunch replayed")
+ }
if readV1().Retirement == "" {
t.Fatal("missing retirement tombstone")
}
diff --git a/internal/host/plan14_generation_parked_root_linux_test.go b/internal/host/plan14_generation_parked_root_linux_test.go
index 7020c8228..ba6a32b98 100644
--- a/internal/host/plan14_generation_parked_root_linux_test.go
+++ b/internal/host/plan14_generation_parked_root_linux_test.go
@@ -11,7 +11,9 @@ import (
"testing"
"github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
"github.com/carlosframework/platform/internal/rootstate"
"golang.org/x/sys/unix"
)
@@ -67,6 +69,18 @@ func TestPlan14GenerationParkedRootRunner(t *testing.T) {
}
})
snapshot, j, root := generationParkedFixture(t)
+ if os.Getenv("PLAN14_R11_DELETION") == "1" {
+ j.Target.ClaimETag = "fixture-record"
+ var receipt StopReceipt
+ if err := json.Unmarshal(j.StopReceipt, &receipt); err != nil {
+ t.Fatal(err)
+ }
+ receipt.ClaimETag = j.Target.ClaimETag
+ j.StopReceipt = mustMarshalStopReceipt(receipt)
+ snapshot.Installations[0].Receipts[0].Bytes = j.StopReceipt
+ snapshot.Installations[0].Receipts[0].Target = *j.Target
+ snapshot.Launches[0].Target = *j.Target
+ }
t.Setenv("CARLOS_HOST_CLAIM", "true")
t.Setenv("CARLOS_OPT_DIR", dir)
if err := os.Mkdir(filepath.Join(dir, "versions"), 0755); err != nil {
@@ -91,6 +105,21 @@ func TestPlan14GenerationParkedRootRunner(t *testing.T) {
}
// The installation, closed launch and stop receipt are fixtures. Parking,
// conditional row retirement, checked readback and kernel scanning are real.
+ var deletedStore release.Store
+ if os.Getenv("PLAN14_R11_DELETION") == "1" {
+ deployment := filepath.Join(dir, "deployment")
+ t.Setenv("CARLOS_DEPLOYMENT_DIR", deployment)
+ t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "")
+ deletedStore = release.NewDirStore(deployment)
+ in := snapshot.Installations[0].Installation
+ record := provision.Record{Schema: 1, Host: in.Host, Account: in.Account, App: in.App, Channel: "stable"}
+ if err := provision.CreateRecord(t.Context(), deletedStore, record); err != nil {
+ t.Fatal(err)
+ }
+ if err := deletedStore.Delete(t.Context(), in.Record); err != nil {
+ t.Fatal(err)
+ }
+ }
generationRunPark(t, root, j, reg, "")
if err := reg.Close(); err != nil {
t.Fatal(err)
@@ -115,6 +144,10 @@ func TestPlan14GenerationParkedRootRunner(t *testing.T) {
if err := state.Write(j.ID+".json", raw); err != nil {
t.Fatal(err)
}
+ if deletedStore != nil {
+ r11DeletedRecordPins(t, snapshot, deletedStore)
+ return
+ }
read := func(ctx context.Context) (generationInventory, error) {
journals, err := productionGenerationRetirements(ctx)
if err != nil {
diff --git a/internal/host/plan14_r10_root_linux_test.go b/internal/host/plan14_r10_root_linux_test.go
index 02cf3ef6f..420e91e22 100644
--- a/internal/host/plan14_r10_root_linux_test.go
+++ b/internal/host/plan14_r10_root_linux_test.go
@@ -274,7 +274,7 @@ func TestPlan14LocalBackingStateRootRunner(t *testing.T) {
}
func TestRootRebootPlan14FinalSuitePhase1(t *testing.T) {
- cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run="+"^(TestClaimRestoreOwnWriteMaterializes|TestClaimRestoreWriteOnlyRetagsPair|TestFilesSeedCLIStopFixture|TestFilesSeedExecutorLifetime|TestFilesSeedLiveFixtureShape|TestPlan14ActivationNamedUnitPreflight|TestPlan14BootLocalRecoveryRootRunner|TestPlan14BoundNetworkEdgeRootRunner|TestPlan14BoundRouteProgramRootRunner|TestPlan14BoundStartupRetirementRootRunner|TestPlan14CombinedBoundRetirementRootRunner|TestPlan14ConsoleLibraryRootRunner|TestPlan14ExecutionHostileCensusRootRunner|TestPlan14FloorRiseWithoutPlatformUpdateRootRunner|TestPlan14GenerationBackendInstalledRootRunner|TestPlan14GenerationContainedRunningRootRunner|TestPlan14GenerationDescriptorTransferRootRunner|TestPlan14GenerationExactRetirementRootRunner|TestPlan14GenerationInstalledUnitRootRunner|TestPlan14GenerationInventoryCheckedRootReader|TestPlan14GenerationKernelCensusRootRunner|TestPlan14GenerationKernelNamespaceMountRootRunner|TestPlan14GenerationKernelRetirementRootRunner|TestPlan14GenerationParkedRootRunner|TestPlan14GenerationPayloadPhysicalRootRunner|TestPlan14GenerationPrivateJournalRootRunner|TestPlan14GenerationRelocationUIDRootRunner|TestPlan14GenerationRequiredArtifactsRootRunner|TestPlan14GenerationRunningTenantPinRootRunner|TestPlan14GuardExecEdgeRootRunner|TestPlan14GuardRecoveryLegacyPolicyRootRunner|TestPlan14InitialMigrationExclusionRootRunner|TestPlan14InstalledClaimLaunchRootRunner|TestPlan14LaunchVerifierRootRunner|TestPlan14LocalBackingStateRootRunner|TestPlan14MaterializedBoundRouteRootRunner|TestPlan14MigratedBoundRouteRootRunner|TestPlan14NamedClaimLaunchRootRunner|TestPlan14NamedStartupDefersReplacementRootRunner|TestPlan14NoCensusReleaseRootRunner|TestPlan14OrdinaryUnitAuthorizationRootRunner|TestPlan14PendingAcquisitionStartupRootRunner|TestPlan14PendingInventoryStartupRootRunner|TestPlan14RealConsoleRootRunner|TestPlan14RealWoodstarRootRunner|TestPlan14RebootStopDescriptorIdentity|TestPlan14RepairExecutorEndToEndRootRunner|TestPlan14RetiredInstallationStartupRootRunner|TestPlan14RetiredMigrationPayloadRootRunner|TestPlan14RootCapabilitiesBothPaths|TestPlan14RootFailureProductionOpener|TestPlan14RootFloorAdoptRefreshRootRunner|TestPlan14RootFloorBootSelectors|TestPlan14RootFloorCrashOrderingRootRunner|TestPlan14RootFloorHealthyAdoptRootRunner|TestPlan14RootFloorRecoveryRollbackRootRunner|TestPlan14RootFloorSignedTargetRootRunner|TestPlan14RootFloorSyscallCrashOrdering|TestPlan14RootFloorUnknownCrashRecoveryRootRunner|TestPlan14RootInventoryPersistentProducer|TestPlan14RootInventoryRejectsOldBootMaterialization|TestPlan14RootJournalNamespaceRootRunner|TestPlan14RootMigrationBootstrapRootRunner|TestPlan14RootMigrationFreshStopRootRunner|TestPlan14RootMigrationHistoricRootRunner|TestPlan14RootMigrationLedgerExclusionRootRunner|TestPlan14RootMigrationMaskRootRunner|TestPlan14RootMigrationRootRunner|TestPlan14RootMigrationTimerAdoptionRootRunner|TestPlan14RootMigrationUnarmedRootRunner|TestPlan14RootRetryHardenedController|TestPlan14RootRetryProjectionDelivery|TestPlan14RootRuntimeInstalledCompletion|TestPlan14RootRuntimeInstalledFailure|TestPlan14RootSeedsFloorRootRunner|TestPlan14RootSeedsFloorSyscallCrashOrdering|TestPlan14SwapFailureHostileRootRunner|TestPlan14SwapFailureNoCensusRootRunner|TestPlan14TimerIntentionRootRunner|TestPlan14TimerRetirementRootRunner|TestPlan14UnitBindingRootRunner|TestRootFilesSeedCombinedRecoveryAllowsUpdate|TestRootFilesSeedDelegatedController|TestRootFilesSeedDelegatedOneShot|TestRootFilesSeedInstanceControlConstruction|TestRootFilesSeedIsolatedTenant|TestRootFilesSeedIsolationNegativeControl|TestRootFilesSeedKilledControllerRerun|TestRootFilesSeedLiveTransport|TestRootFilesSeedRealOriginPreparation|TestRootFilesSeedRealOriginTenant|TestRootFilesSeedSocketCompleteExecutor|TestRootFilesSeedSocketDisconnectExecutor|TestRootFilesSeedSocketShutdownExecutor|TestRootFilesSeedTwoHostIsolation|TestRootFilesSeedWholeExecutorKilledRerun|TestRootServingHandoverDirectoryProof|TestRootServingNamespaceStartup|TestRootServingNamespaceUnrecordedStartup|TestRootServingParkAuthenticatedHandover|TestRootServingParkAuthenticatedPeer|TestRootServingParkColdHandoverBoundary|TestRootServingParkCollection|TestRootServingParkDisplacedRoot|TestRootServingParkInstalledUnit|TestRootServingParkNestedBindRestore|TestRootServingParkQuarantine|TestRootServingParkRetainedDoesNotBlockRetirement|TestRootServingParkSnapshotClone|TestRootServingParkStoppedWriterHandover|TestRootServingParkUnrecordedReplacement)$", "-test.v", "-test.timeout=25m")
+ cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run="+"^(TestPlan14ServingHandoverPinsRootRunner|TestPlan14DeletedRecordRetirementRootRunner|TestPlan14SettingsSuccessorProductionRootRunner|TestClaimRestoreOwnWriteMaterializes|TestClaimRestoreWriteOnlyRetagsPair|TestFilesSeedCLIStopFixture|TestFilesSeedExecutorLifetime|TestFilesSeedLiveFixtureShape|TestPlan14ActivationNamedUnitPreflight|TestPlan14BootLocalRecoveryRootRunner|TestPlan14BoundNetworkEdgeRootRunner|TestPlan14BoundRouteProgramRootRunner|TestPlan14BoundStartupRetirementRootRunner|TestPlan14CombinedBoundRetirementRootRunner|TestPlan14ConsoleLibraryRootRunner|TestPlan14ExecutionHostileCensusRootRunner|TestPlan14FloorRiseWithoutPlatformUpdateRootRunner|TestPlan14GenerationBackendInstalledRootRunner|TestPlan14GenerationContainedRunningRootRunner|TestPlan14GenerationDescriptorTransferRootRunner|TestPlan14GenerationExactRetirementRootRunner|TestPlan14GenerationInstalledUnitRootRunner|TestPlan14GenerationInventoryCheckedRootReader|TestPlan14GenerationKernelCensusRootRunner|TestPlan14GenerationKernelNamespaceMountRootRunner|TestPlan14GenerationKernelRetirementRootRunner|TestPlan14GenerationParkedRootRunner|TestPlan14GenerationPayloadPhysicalRootRunner|TestPlan14GenerationPrivateJournalRootRunner|TestPlan14GenerationRelocationUIDRootRunner|TestPlan14GenerationRequiredArtifactsRootRunner|TestPlan14GenerationRunningTenantPinRootRunner|TestPlan14GuardExecEdgeRootRunner|TestPlan14GuardRecoveryLegacyPolicyRootRunner|TestPlan14InitialMigrationExclusionRootRunner|TestPlan14InstalledClaimLaunchRootRunner|TestPlan14LaunchVerifierRootRunner|TestPlan14LocalBackingStateRootRunner|TestPlan14MaterializedBoundRouteRootRunner|TestPlan14MigratedBoundRouteRootRunner|TestPlan14NamedClaimLaunchRootRunner|TestPlan14NamedStartupDefersReplacementRootRunner|TestPlan14NoCensusReleaseRootRunner|TestPlan14OrdinaryUnitAuthorizationRootRunner|TestPlan14PendingAcquisitionStartupRootRunner|TestPlan14PendingInventoryStartupRootRunner|TestPlan14RealConsoleRootRunner|TestPlan14RealWoodstarRootRunner|TestPlan14RebootStopDescriptorIdentity|TestPlan14RepairExecutorEndToEndRootRunner|TestPlan14RetiredInstallationStartupRootRunner|TestPlan14RetiredMigrationPayloadRootRunner|TestPlan14RootCapabilitiesBothPaths|TestPlan14RootFailureProductionOpener|TestPlan14RootFloorAdoptRefreshRootRunner|TestPlan14RootFloorBootSelectors|TestPlan14RootFloorCrashOrderingRootRunner|TestPlan14RootFloorHealthyAdoptRootRunner|TestPlan14RootFloorRecoveryRollbackRootRunner|TestPlan14RootFloorSignedTargetRootRunner|TestPlan14RootFloorSyscallCrashOrdering|TestPlan14RootFloorUnknownCrashRecoveryRootRunner|TestPlan14RootInventoryPersistentProducer|TestPlan14RootInventoryRejectsOldBootMaterialization|TestPlan14RootJournalNamespaceRootRunner|TestPlan14RootMigrationBootstrapRootRunner|TestPlan14RootMigrationFreshStopRootRunner|TestPlan14RootMigrationHistoricRootRunner|TestPlan14RootMigrationLedgerExclusionRootRunner|TestPlan14RootMigrationMaskRootRunner|TestPlan14RootMigrationRootRunner|TestPlan14RootMigrationTimerAdoptionRootRunner|TestPlan14RootMigrationUnarmedRootRunner|TestPlan14RootRetryHardenedController|TestPlan14RootRetryProjectionDelivery|TestPlan14RootRuntimeInstalledCompletion|TestPlan14RootRuntimeInstalledFailure|TestPlan14RootSeedsFloorRootRunner|TestPlan14RootSeedsFloorSyscallCrashOrdering|TestPlan14SwapFailureHostileRootRunner|TestPlan14SwapFailureNoCensusRootRunner|TestPlan14TimerIntentionRootRunner|TestPlan14TimerRetirementRootRunner|TestPlan14UnitBindingRootRunner|TestRootFilesSeedCombinedRecoveryAllowsUpdate|TestRootFilesSeedDelegatedController|TestRootFilesSeedDelegatedOneShot|TestRootFilesSeedInstanceControlConstruction|TestRootFilesSeedIsolatedTenant|TestRootFilesSeedIsolationNegativeControl|TestRootFilesSeedKilledControllerRerun|TestRootFilesSeedLiveTransport|TestRootFilesSeedRealOriginPreparation|TestRootFilesSeedRealOriginTenant|TestRootFilesSeedSocketCompleteExecutor|TestRootFilesSeedSocketDisconnectExecutor|TestRootFilesSeedSocketShutdownExecutor|TestRootFilesSeedTwoHostIsolation|TestRootFilesSeedWholeExecutorKilledRerun|TestRootServingHandoverDirectoryProof|TestRootServingNamespaceStartup|TestRootServingNamespaceUnrecordedStartup|TestRootServingParkAuthenticatedHandover|TestRootServingParkAuthenticatedPeer|TestRootServingParkColdHandoverBoundary|TestRootServingParkCollection|TestRootServingParkDisplacedRoot|TestRootServingParkInstalledUnit|TestRootServingParkNestedBindRestore|TestRootServingParkQuarantine|TestRootServingParkRetainedDoesNotBlockRetirement|TestRootServingParkSnapshotClone|TestRootServingParkStoppedWriterHandover|TestRootServingParkUnrecordedReplacement)$", "-test.v", "-test.timeout=25m")
out, err := cmd.CombinedOutput()
t.Log(string(out))
if err != nil {
@@ -286,3 +286,16 @@ func TestRootRebootPlan14FinalSuitePhase1(t *testing.T) {
func TestRootRebootPlan14FinalSuitePhase2(t *testing.T) {
plan14RebootGroups(t, "2", []string{"FilesSeedTwoHostRenumber"})
}
+
+func TestRootRebootPlan14R11FinalPhase1(t *testing.T) {
+ cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^(TestPlan14DeletedRecordRetirementRootRunner|TestPlan14SettingsSuccessorProductionRootRunner|TestPlan14ServingHandoverPinsRootRunner)$", "-test.v", "-test.timeout=3m")
+ out, err := cmd.CombinedOutput()
+ t.Log(string(out))
+ if err != nil {
+ t.Fatal(err)
+ }
+ plan14RebootGroups(t, "1", []string{"FilesSeedTwoHostRenumber"})
+}
+func TestRootRebootPlan14R11FinalPhase2(t *testing.T) {
+ plan14RebootGroups(t, "2", []string{"FilesSeedTwoHostRenumber"})
+}
diff --git a/internal/host/plan14_r11_handover_test.go b/internal/host/plan14_r11_handover_test.go
new file mode 100644
index 000000000..01c1381af
--- /dev/null
+++ b/internal/host/plan14_r11_handover_test.go
@@ -0,0 +1,194 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strconv"
+ "testing"
+)
+
+func TestPlan14ServingHandoverPins(t *testing.T) {
+ f := newServingRootFixture(t, true)
+ root := servingHandoverRoot(t, f)
+ f.mutateRoot(func(m map[string]any) {
+ m["Installation"].(map[string]any)["Artifacts"] = map[string]any{"Schema": 1, "StateDirectory": true}
+ })
+ if os.Getenv("PLAN14_R11_HANDOVER_ROOT") == "1" {
+ root.Reconcile.Retirement.JournalDir = claimRetireJournalDir("", "")
+ }
+ readHandovers := func() ([]hostbox.GenerationHandover, error) {
+ if os.Getenv("PLAN14_R11_HANDOVER_ROOT") == "1" {
+ return productionGenerationHandovers(t.Context())
+ }
+ return generationReadHandovers(t.Context(), &rollbackDisk{dir: filepath.Join(root.Reconcile.Retirement.JournalDir, "serving")})
+ }
+ next := hostbox.ColdSuccessor{Host: f.current.Host, Incarnation: f.current.Incarnation, Box: f.actor.Box, Enrollment: f.actor.Enrollment, DirectoryRevision: strconv.FormatUint(f.actor.Revision, 10), ETag: f.f.read().ETag, Generation: f.current.Generation, Stamp: f.current.Stamp, Session: f.current.Session}
+ if err := root.servingHandover().Run(t.Context(), f.receipt.Target, next); err != nil {
+ t.Fatal(err)
+ }
+ installs, err := f.f.guard.Root.GenerationInstallations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ payloads, err := f.f.journal.GenerationPayloads(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ snapshot := generationInventory{Installations: installs, Payloads: payloads}
+ snapshot.Handovers, err = readHandovers()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if _, err = generationTransfers(t.Context(), snapshot); err != nil {
+ t.Fatal("transfers", err)
+ }
+ if _, err = generationRegistryInventory(t.Context(), root.Reconcile.Registry, snapshot); err != nil {
+ t.Fatal("registry after actual serving handover", err)
+ }
+ physical, err := generationPhysicalPayloads(t.Context(), snapshot)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if physical[0].Inventory.Entries[0].Identity != physical[1].Inventory.Entries[0].Identity {
+ t.Fatal("predecessor did not follow verified persistent identity")
+ }
+ original, _ := json.Marshal(snapshot)
+ for _, attack := range []string{"missing journal", "prepared", "successor", "barrier", "receipt", "retired", "path", "row", "revision", "duplicate", "cycle", "old boot"} {
+ t.Run(attack, func(t *testing.T) {
+ var bad generationInventory
+ if err := json.Unmarshal(original, &bad); err != nil {
+ t.Fatal(err)
+ }
+ switch attack {
+ case "missing journal":
+ bad.Handovers = nil
+ case "prepared":
+ bad.Handovers[0].Phase = "prepared"
+ case "successor":
+ bad.Handovers[0].Successor.Stamp++
+ case "barrier":
+ bad.Handovers[0].Barrier.ControllerID = "other"
+ case "receipt":
+ bad.Handovers[0].Receipt.Bytes = []byte("other")
+ case "retired":
+ bad.Installations[0].Retired = nil
+ case "path":
+ bad.Payloads[1].Inventory.Entries[0].Inode++
+ case "row":
+ bad.Payloads[1].Inventory.Entries[1].RowIdentity = "other"
+ case "revision":
+ bad.Payloads[0].Revision++
+ case "duplicate":
+ bad.Handovers = append(bad.Handovers, bad.Handovers[0])
+ case "cycle":
+ bad.Handovers[0].Successor.Stamp = bad.Handovers[0].Prior.Stamp
+ case "old boot":
+ bad.Payloads[1].Inventory.Entries[0].Identity.Boot = "other-boot"
+ }
+ if _, err := generationRegistryInventory(t.Context(), root.Reconcile.Registry, bad); err == nil {
+ t.Fatal("accepted", attack)
+ }
+ })
+ }
+ before := f.f.read()
+ receipt := servingStop(t, f.f, before, f.receipt.Target.BootID)
+ admitted, err := hostclaim.Update(t.Context(), f.f.claims, before, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: before.Claim.Holder.Session, ObservedGeneration: before.Claim.Holder.Generation, Box: f.actor.Box, Region: f.actor.Region, PredecessorRegion: f.actor.Region, Mode: "cold", NewGeneration: "G3"}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ next.ETag, next.Generation, next.Stamp, next.Session = admitted.ETag, admitted.Claim.Holder.Generation, admitted.Claim.Holder.Stamp, admitted.Claim.Holder.Session
+ if err = root.servingHandover().Run(t.Context(), receipt.Target, next); err != nil {
+ t.Fatal("second transfer", err)
+ }
+ snapshot.Installations, err = f.f.guard.Root.GenerationInstallations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ snapshot.Payloads, err = f.f.journal.GenerationPayloads(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ snapshot.Handovers, err = readHandovers()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err = generationRegistryInventory(t.Context(), root.Reconcile.Registry, snapshot); err != nil {
+ t.Fatal("second transfer registry", err)
+ }
+ if len(snapshot.Handovers) != 2 || len(snapshot.Payloads) != 3 {
+ t.Fatal("transfer lost retained authority")
+ }
+ physical, err = generationPhysicalPayloads(t.Context(), snapshot)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for i := range physical {
+ if !reflect.DeepEqual(physical[i].Inventory.Entries[0].Identity, physical[2].Inventory.Entries[0].Identity) {
+ t.Fatal("chain identity", i)
+ }
+ }
+ // The initial installation and stop evidence are fixtures. Transfers, root
+ // retirement, row removal and directory parking below use their production adapters.
+ stopped := servingStop(t, f.f, f.f.read(), f.receipt.Target.BootID)
+ terminal := snapshot.Payloads[2].Inventory
+ inventory := hostbox.RetirementInventory{Host: terminal.Host, Captured: hostguard.Captured{Incarnation: terminal.Incarnation, Stamp: terminal.InstallationStamp}, Ownership: terminal.Entries[0].Ownership}
+ for _, e := range terminal.Entries {
+ inventory.Artifacts = append(inventory.Artifacts, hostbox.Artifact{Kind: hostbox.ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Identity: e.Identity, Dev: e.Device, Ino: e.Inode, Type: e.Type})
+ }
+ retirement, err := hostbox.NewRetirementJournal(stopped.Target.FenceID, inventory)
+ if err != nil {
+ t.Fatal(err)
+ }
+ retirement.Target, retirement.StopReceipt = &stopped.Target, stopped.Bytes
+ data := f.f.guard.DataDir
+ t.Setenv("CARLOS_DATA_DIR", data)
+ if err = f.f.claims.Delete(t.Context(), snapshot.Installations[0].Installation.Record); err != nil {
+ t.Fatal(err)
+ }
+ parker := hostbox.TypedParker{Roots: map[string]string{"data": data}, ParkedRoots: map[string]string{"data": data}, Incarnation: terminal.Incarnation, Host: terminal.Host, Retirement: retirement.ID}
+ runner := hostbox.TypedRetirement{JournalDir: root.Reconcile.Retirement.JournalDir, Authority: hostbox.InstallationCleanup{Guard: f.f.guard, Verify: verifyRetirementReceipt}, Filesystem: parker, Rows: hostbox.RegistryRows{Registry: root.Reconcile.Registry, Inventory: inventory}, Ack: func(context.Context, hostbox.RetirementJournal) error { return nil }}
+ if err = runner.Run(t.Context(), stopped.Target, retirement); err != nil {
+ t.Fatal("terminal retirement", err)
+ }
+ raw, err := os.ReadFile(filepath.Join(runner.JournalDir, retirement.ID+".json"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = json.Unmarshal(raw, &retirement); err != nil || !retirement.Complete {
+ t.Fatal(err)
+ }
+ snapshot.Retirements = []hostbox.RetirementJournal{retirement}
+ snapshot.Installations, err = f.f.guard.Root.GenerationInstallations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err = generationRequiredInventory(t.Context(), f.f.claims, snapshot, nil); err != nil {
+ t.Fatal("deleted record through transfer chain", err)
+ }
+ if _, err = generationRegistryInventory(t.Context(), root.Reconcile.Registry, snapshot); err != nil {
+ t.Fatal("retired transfer chain", err)
+ }
+ physical, err = generationPhysicalPayloads(t.Context(), snapshot)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, payload := range physical {
+ for _, e := range payload.Inventory.Entries {
+ if e.Kind == "path" {
+ if _, err = os.Stat(filepath.Join(data, e.RelativePath)); err != nil {
+ t.Fatal("retained predecessor location", err)
+ }
+ }
+ }
+ }
+
+}
diff --git a/internal/host/plan14_r11_process_test.go b/internal/host/plan14_r11_process_test.go
new file mode 100644
index 000000000..2b168aca8
--- /dev/null
+++ b/internal/host/plan14_r11_process_test.go
@@ -0,0 +1,96 @@
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func TestPlan14PrelaunchCancellationProcessDeath(t *testing.T) {
+ if dir := os.Getenv("PLAN14_R11_RESERVE_CRASH"); dir != "" {
+ f, disk, _ := generationLaunchFixture(t)
+ if err := os.Mkdir(filepath.Join(dir, "pins"), 0700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir(filepath.Join(dir, "fences"), 0700); err != nil {
+ t.Fatal(err)
+ }
+ views, err := f.root.Guard.Root.GenerationInstallations(t.Context())
+ if err != nil || len(views) != 1 {
+ t.Fatal(err)
+ }
+ in := views[0].Installation
+ raw, _ := json.Marshal(map[string]any{"Schema": 1, "Installation": in, "State": map[string]any{"v": 1, "host": in.Host, "incarnation": in.Incarnation, "active_stamp": in.Stamp}})
+ if err = os.WriteFile(filepath.Join(dir, "fences", in.Host+".json"), raw, 0600); err != nil {
+ t.Fatal(err)
+ }
+ launch, err := f.journal.Read(t.Context(), f.target)
+ if err != nil {
+ t.Fatal(err)
+ }
+ journal, err := hostbox.OpenLaunchJournal(filepath.Join(dir, "launch.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = journal.Prelaunch(t.Context(), launch); err != nil {
+ t.Fatal(err)
+ }
+ if err = journal.Close(); err != nil {
+ t.Fatal(err)
+ }
+ disk.dir = filepath.Join(dir, "pins")
+ disk.afterWrite = func(_ string, raw []byte) {
+ var r release.GenerationReservation
+ if json.Unmarshal(raw, &r) == nil && r.Revision == 1 {
+ os.Exit(73)
+ }
+ }
+ _, err = f.root.generations.reserve(t.Context(), f.root, f.peer, f.req)
+ t.Fatal("reservation crash not reached", err)
+ }
+ dir := t.TempDir()
+ cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestPlan14PrelaunchCancellationProcessDeath$", "-test.count=1")
+ cmd.Dir = dir
+ cmd.Env = append(os.Environ(), "PLAN14_R11_RESERVE_CRASH="+dir, "TMPDIR="+dir)
+ out, err := cmd.CombinedOutput()
+ var status *exec.ExitError
+ if !errors.As(err, &status) || status.ExitCode() != 73 {
+ t.Fatal("crash child", err, string(out))
+ }
+ disk := &generationDisk{rollbackDisk: &rollbackDisk{dir: filepath.Join(dir, "pins")}}
+ r := readGenerationLaunch(t, disk)
+ journal, err := hostbox.OpenLaunchJournal(filepath.Join(dir, "launch.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer journal.Close()
+ launches, err := journal.GenerationLaunches(t.Context())
+ if err != nil || len(launches) != 1 || len(launches[0].Reservations) != 0 {
+ t.Fatal("pre-intention boundary", err, launches)
+ }
+ verifier := generationRetirement{guard: hostguard.Guard{DataDir: dir, LockDir: t.TempDir(), Root: hostguard.NewRootAuthority(&rollbackDisk{dir: filepath.Join(dir, "fences")})}, journal: journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { return r.Binding.Digest, nil }}
+ ledger := &release.RootGenerations{Store: disk}
+ if err = verifier.retire(t.Context(), ledger, r); err != nil {
+ t.Fatal(err)
+ }
+ if got := readGenerationLaunch(t, disk); got != r {
+ t.Fatal("death substituted for writer closure")
+ }
+ if err = journal.CloseWriters(t.Context(), launches[0].Target); err != nil {
+ t.Fatal(err)
+ }
+ if err = verifier.retire(t.Context(), ledger, r); err != nil {
+ t.Fatal("cancellation recovery", err)
+ }
+ if got := readGenerationLaunch(t, disk); !got.Closed || got.Retirement == "" {
+ t.Fatal("recovered reservation pinned")
+ }
+}
diff --git a/internal/host/plan14_r11_root_linux_test.go b/internal/host/plan14_r11_root_linux_test.go
new file mode 100644
index 000000000..43c6eaa74
--- /dev/null
+++ b/internal/host/plan14_r11_root_linux_test.go
@@ -0,0 +1,272 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "crypto/ed25519"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net"
+ "net/http"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rollout"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func TestPlan14DeletedRecordRetirementRootRunner(t *testing.T) {
+ t.Setenv("PLAN14_R11_DELETION", "1")
+ TestPlan14GenerationParkedRootRunner(t)
+}
+
+func TestPlan14ServingHandoverPinsRootRunner(t *testing.T) {
+ if err := os.Mkdir("/var/lib/carlos-claims", 0700); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { os.RemoveAll("/var/lib/carlos-claims") })
+ t.Setenv("PLAN14_R11_HANDOVER_ROOT", "1")
+ TestPlan14ServingHandoverPins(t)
+}
+
+func r11DeletedRecordPins(t *testing.T, snapshot generationInventory, store release.Store) {
+ t.Helper()
+ ctx := t.Context()
+ open := func(path string) rootstate.Store {
+ s, err := rootstate.Open(path, true, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { s.Close() })
+ return s
+ }
+ write := func(s rootstate.Store, key string, v any) {
+ raw, err := json.Marshal(v)
+ if err == nil {
+ err = s.Write(key, raw)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ write(open(rootparticipation.Namespace), "census.json", snapshot.Census)
+ fences := open("/var/lib/carlos-claims/authority/fences")
+ in := snapshot.Installations[0].Installation
+ write(fences, in.Host+".json", map[string]any{"Schema": 1, "Installation": in, "Retired": []hostguard.Captured{{Incarnation: in.Incarnation, Stamp: in.Stamp}}, "State": map[string]any{"v": 1, "host": in.Host, "incarnation": in.Incarnation, "active_stamp": 0, "retired_stamps": []uint64{in.Stamp}, "fence_journal": snapshot.Installations[0].Receipts}})
+ journal, err := hostbox.OpenLaunchJournal("/var/lib/carlos-claims/journals.db")
+ if err != nil {
+ t.Fatal(err)
+ }
+ service, err := hostbox.NewJournalService(journal)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = service.Inventory.WriteStopInventory(ctx, snapshot.Payloads[0].Inventory); err != nil {
+ t.Fatal(err)
+ }
+ target := snapshot.Launches[0].Target
+ if err = journal.Prelaunch(ctx, hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "closed-fixture", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "fixture", Device: 1, Inode: 1}}}); err != nil {
+ t.Fatal(err)
+ }
+ if err = journal.CloseWriters(ctx, target); err != nil {
+ t.Fatal(err)
+ }
+ if err = journal.Close(); err != nil {
+ t.Fatal(err)
+ }
+ open(release.RootGenerationsDir)
+ snapshotRead, err := generationInventoryAt(ctx, rootparticipation.Namespace, "/var/lib/carlos-claims/authority/fences", "/var/lib/carlos-claims/journals.db")
+ if err != nil {
+ t.Fatal("read production inventory", err)
+ }
+ snapshotRead.Retirements, err = productionGenerationRetirements(ctx)
+ if err != nil {
+ t.Fatal("read retirements", err)
+ }
+ if _, err = generationRequiredInventory(ctx, store, snapshotRead, productionGenerationCreation); err != nil {
+ t.Fatal("retained obligations", err)
+ }
+ if _, err = productionGenerationRegistryInventory(ctx, snapshotRead); err != nil {
+ t.Fatal("registry obligations", err)
+ }
+ if _, err = generationPhysicalPayloads(ctx, snapshotRead); err != nil {
+ t.Fatal("physical obligations", err)
+ }
+ if _, _, err = productionGenerationKernelCensus(ctx); err != nil {
+ t.Fatal("kernel census", err)
+ }
+ source := productionRootGenerationPins()
+ lock := open(rootReleasePrivate)
+ if err = rootstate.WithRelease(ctx, lock, func() error {
+ pins, err := source.StagedPins(ctx, release.OptDir())
+ if err == nil && len(pins) != 0 {
+ t.Fatal("unexpected pins", pins)
+ }
+ return err
+ }); err != nil {
+ t.Fatal("production pins after record deletion", err)
+ }
+ pub, priv, err := ed25519.GenerateKey(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ body := []byte("signed sibling")
+ m := release.Manifest{App: "sibling", Version: "v9", Kind: "binary", Artifacts: map[string]release.Artifact{release.AdoptPlatform(): {}}}
+ if err = release.Ship(ctx, store, "bab", m, map[string][]byte{release.AdoptPlatform(): body}); err != nil {
+ t.Fatal(err)
+ }
+ if err = release.Promote(ctx, store, priv, "bab", "sibling", "stable", "v9"); err != nil {
+ t.Fatal(err)
+ }
+ if err = release.StageArtifact(release.OptDir(), "bab", "v9", "sibling", body); err != nil {
+ t.Fatal(err)
+ }
+ manifest := release.StagedManifestPath(release.OptDir(), "bab", "v9", "sibling")
+ if err = os.WriteFile(manifest, []byte("broken sibling manifest"), 0644); err != nil {
+ t.Fatal(err)
+ }
+ if err = rootstate.WithRelease(ctx, lock, func() error {
+ proof := release.NewRootProofGate(filepath.Join(t.TempDir(), "policy.json"), filepath.Join(t.TempDir(), "bakes"))
+ _, err := release.StageHeldVerifiedWithPins(ctx, store, pub, nil, release.OptDir(), registry.Route{Host: "sibling.example", Account: "bab", App: "sibling", Kind: "instance", Channel: "stable", Status: "landing", HoldOp: "repair"}, "v9", proof, source)
+ return err
+ }); err != nil {
+ t.Fatal("sibling repair after deleted host", err)
+ }
+ raw, err := os.ReadFile(manifest)
+ if err != nil || string(raw) == "broken sibling manifest" {
+ t.Fatal("sibling manifest not repaired", err)
+ }
+ if _, _, err = store.Get(ctx, provision.RecordKey(in.Account, in.App, in.Host)); !errors.Is(err, os.ErrNotExist) {
+ t.Fatal("record resurrected", err)
+ }
+}
+
+func TestPlan14SettingsSuccessorProductionRootRunner(t *testing.T) {
+ for _, path := range []string{"/var/lib/carlos-claims", "/var/lib/carlos-release", rootRetryProjection, "/run/carlos"} {
+ if err := os.Mkdir(path, 0755); err != nil {
+ t.Fatal("existing fixture", path, err)
+ }
+ t.Cleanup(func() { os.RemoveAll(path) })
+ }
+ open := func(path string) rootstate.Store {
+ s, err := rootstate.Open(path, true, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { s.Close() })
+ return s
+ }
+ c := rootparticipation.Census{Schema: 1, Box: "box-1", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}
+ raw, _ := json.Marshal(c)
+ if err := open(rootparticipation.Namespace).Write("census.json", raw); err != nil {
+ t.Fatal(err)
+ }
+ failures := open(rollout.RootFailuresDir)
+ if _, err := rollout.InitializeRootFailures(failures, rootUpdatesMigration(c), nil); err != nil {
+ t.Fatal(err)
+ }
+ testInventoryRegistrationWith(t, true, nil, open("/var/lib/carlos-claims/authority/fences"), func(t *testing.T, f *recoveryFixture, root ClaimRootHTTP, row registry.Route, actor hostbox.Actor) {
+ t.Setenv("CARLOS_DATA_DIR", root.Roots["data"])
+ t.Setenv("CARLOS_OPT_DIR", root.Roots["opt"])
+ pub, priv, err := ed25519.GenerateKey(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ m := release.Manifest{App: row.App, Version: "v1", Kind: "binary", Artifacts: map[string]release.Artifact{release.AdoptPlatform(): {}}}
+ body := []byte("signed settings witness")
+ if err = release.Ship(t.Context(), f.claims, row.Account, m, map[string][]byte{release.AdoptPlatform(): body}); err != nil {
+ t.Fatal(err)
+ }
+ if err = release.Promote(t.Context(), f.claims, priv, row.Account, row.App, "stable", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err = os.MkdirAll(filepath.Dir(row.Socket), 0755); err != nil {
+ t.Fatal(err)
+ }
+ listener, err := net.Listen("unix", row.Socket)
+ if err != nil {
+ t.Fatal(err)
+ }
+ server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ready")) })}
+ go server.Serve(listener)
+ defer server.Close()
+ source := productionRootGenerations(f.claims, root.Roots["opt"])
+ source.pub = func() (ed25519.PublicKey, error) { return pub, nil }
+ digest := sha256.Sum256(body)
+ g := root.Stop.Coordinator.Guard
+ capture, err := g.Capture(row.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ prior, err := g.Root.Installation(t.Context(), row.Host, capture)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for wake := 0; wake < 2; wake++ {
+ opened, err := productionRootUpdates(dataDir())(t.Context(), f.claims, pub)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resolved, err := opened.Resolve(t.Context(), row.Host)
+ if err != nil {
+ t.Fatal("settings wake", wake, err)
+ }
+ if wake == 1 && (resolved.Build.State != nil || resolved.Consumed != nil) {
+ t.Fatal("successor reused foreign completion")
+ }
+ if wake == 0 {
+ if err = resolved.Save(release.Consumed{Resolved: &release.ResolvedRequest{ID: 11, Target: "v1"}}); err != nil {
+ t.Fatal(err)
+ }
+ }
+ opened.Close()
+ b := release.GenerationBinding{Binding: release.HistoryBinding{Host: row.Host, Account: row.Account, App: row.App}, Version: "v1", Digest: hex.EncodeToString(digest[:]), Attempt: fmt.Sprintf("wake-%d", wake)}
+ finish, close, err := source.beginUpdate(t.Context(), source, b)
+ if err != nil {
+ t.Fatal("production begin", wake, err)
+ }
+ if err = finish(fmt.Sprintf("invocation-%d", wake), nil, func() error { return nil }); err != nil {
+ close()
+ t.Fatal(err)
+ }
+ close()
+ if wake == 0 {
+ observed := f.read()
+ admitted, err := hostclaim.Update(t.Context(), f.claims, observed, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: observed.Claim.Holder.Session, ObservedGeneration: observed.Claim.Holder.Generation, Box: actor.Box, Region: actor.Region, Mode: "cold", NewGeneration: "second-generation"}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ next := prior
+ next.Stamp = admitted.Claim.Holder.Stamp
+ if next.Stamp <= prior.Stamp {
+ t.Fatal("cold admission did not advance installation")
+ }
+ intent, _ := json.Marshal(admitted)
+ if err = g.RebindRootInventory(t.Context(), capture, prior.InventoryRevision, next, intent, func(claim *hostclaim.Claim) error {
+ if !hostclaim.SameWriterIdentity(admitted, hostclaim.Observed{Host: row.Host, Claim: claim}) {
+ return os.ErrPermission
+ }
+ return nil
+ }, func() error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ }
+ }
+ var saved rootUpdateRecord
+ raw, err := failures.Read(rootSettingsKey(row.Host, rootHistoryInstallation(prior)))
+ if err != nil || json.Unmarshal(raw, &saved) != nil || saved.Build.State == nil || saved.Build.State.Completed != 11 {
+ t.Fatal("predecessor settings not retained", err)
+ }
+ })
+}
diff --git a/internal/host/plan14_r11_test.go b/internal/host/plan14_r11_test.go
new file mode 100644
index 000000000..89f453b3a
--- /dev/null
+++ b/internal/host/plan14_r11_test.go
@@ -0,0 +1,331 @@
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rollout"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+)
+
+type r11RecordingStore struct {
+ release.Store
+ reads []string
+}
+
+func (s *r11RecordingStore) Get(ctx context.Context, key string) ([]byte, string, error) {
+ s.reads = append(s.reads, "get:"+key)
+ return s.Store.Get(ctx, key)
+}
+func (s *r11RecordingStore) List(ctx context.Context, prefix string) ([]string, error) {
+ s.reads = append(s.reads, "list:"+prefix)
+ return s.Store.List(ctx, prefix)
+}
+func TestPlan14NoCensusProofObservation(t *testing.T) {
+ for _, flag := range []string{"", "1"} {
+ t.Run(flag, func(t *testing.T) {
+ t.Setenv("CARLOS_HOST_CLAIM", flag)
+ _, public, err := release.GenerateKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Setenv("CARLOS_RELEASE_PUBKEY", public)
+ e, rt, rec, source := plan14ProofFixture(t)
+ store := &r11RecordingStore{Store: e.store}
+ production := productionProofBindingSource(store, source.sys, source.optDir)
+ if production.signedRecovery {
+ t.Fatal("no census enabled signed recovery")
+ }
+ source.store = store
+ source.signedRecovery = production.signedRecovery
+ if production.claim != nil {
+ rec.CreatedAt = time.Now().UTC()
+ plan14PutRecord(t, e.store, rec)
+ body, err := json.Marshal(rec)
+ if err != nil {
+ t.Fatal(err)
+ }
+ o, err := hostclaim.Read(t.Context(), e.store, rec.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ o, err = hostclaim.Update(t.Context(), e.store, o, hostclaim.BeginCreate(hostclaim.NewHolder{Family: "instance", Key: provision.RecordKey(rec.Account, rec.App, rec.Host), Host: rec.Host, Account: rec.Account, App: rec.App, Region: "ie", CreatedAt: rec.CreatedAt}, body, "create", "console", rec.CreatedAt))
+ if err != nil {
+ t.Fatal(err)
+ }
+ o, err = hostclaim.Update(t.Context(), e.store, o, hostclaim.CompleteCreate("create", "box"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err = hostclaim.Update(t.Context(), e.store, o, hostclaim.AdmitWriter(hostclaim.Admission{Box: "box", Region: "ie", Mode: "cold", NewGeneration: "first"})); err != nil {
+ t.Fatal(err)
+ }
+ source.claim = production.claim
+ source.box = func(context.Context) (string, error) { return "box", nil }
+ }
+ source.healthy = func(string, string, string, string) *release.HealthyBuild { return nil }
+ live, err := e.root.obs.observe(rt, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if live.binding.Historical || live.binding.Signed {
+ t.Fatal("fabricated recovery authority")
+ }
+ baseline := append([]string(nil), store.reads...)
+ store.reads = nil
+ if _, _, err = provision.FindRecordByHost(t.Context(), store, rec.Host); err != nil {
+ t.Fatal(err)
+ }
+ if source.claim != nil {
+ if _, err = source.claim(t.Context(), rec.Host); err != nil {
+ t.Fatal(err)
+ }
+ }
+ // Observation binds the unit and repeats that binding after probing it.
+ want := append(append([]string(nil), store.reads...), store.reads...)
+ if !reflect.DeepEqual(baseline, want) {
+ t.Fatalf("legacy observation read beyond its two canonical record/claim lookups: got %v, want %v", baseline, want)
+ }
+ store.reads = nil
+ source.signedRecovery = true
+ if _, err = e.root.obs.observe(rt, 0); err != nil {
+ t.Fatal(err)
+ }
+ if reflect.DeepEqual(baseline, store.reads) {
+ t.Fatal("negative control did not read authorization")
+ }
+ for _, key := range baseline {
+ if strings.Contains(key, "manifest") || strings.Contains(key, "promotion") {
+ t.Fatal("legacy authorization read", key)
+ }
+ }
+ })
+ }
+}
+
+func TestPlan14PrelaunchCancellation(t *testing.T) {
+ for _, mode := range []string{"reserved", "opener-failure", "open-writers", "wrong-controller", "kernel", "lost-close", "lost-retirement"} {
+ t.Run(mode, func(t *testing.T) {
+ f, disk, _ := generationLaunchFixture(t)
+ if mode == "opener-failure" {
+ f.root.generations.beginUpdate = func(context.Context, *rootGenerationSource, release.GenerationBinding) (func(string, error, func() error) error, func() error, error) {
+ return nil, nil, os.ErrPermission
+ }
+ }
+ permit, err := f.root.generations.reserve(t.Context(), f.root, f.peer, f.req)
+ if mode == "opener-failure" {
+ if err == nil {
+ t.Fatal("opener succeeded")
+ }
+ } else {
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer permit.ledger.Store.Close()
+ }
+ r := readGenerationLaunch(t, disk)
+ launches, err := f.journal.GenerationLaunches(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, l := range launches {
+ if len(l.Reservations) != 0 {
+ t.Fatal("unit intention preceded cancellation test")
+ }
+ }
+ if mode != "open-writers" {
+ if err = f.journal.CloseWriters(t.Context(), f.target); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if mode == "wrong-controller" {
+ r.Binding.ControllerPID++
+ }
+ checks := 0
+ v := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) {
+ checks++
+ if mode == "kernel" {
+ return "", os.ErrPermission
+ }
+ if mode == "lost-retirement" {
+ disk.fail = 1
+ }
+ return strings.Repeat("a", 64), nil
+ }}
+ if mode == "lost-close" {
+ disk.fail = 1
+ }
+ ledger := &release.RootGenerations{Store: disk}
+ err = v.retire(t.Context(), ledger, r)
+ got := readGenerationLaunch(t, disk)
+ switch mode {
+ case "open-writers":
+ if err != nil || got != r || checks != 0 {
+ t.Fatal("open reservation changed", err)
+ }
+ return
+ case "wrong-controller", "kernel":
+ if err == nil || got.Retirement != "" {
+ t.Fatal("uncertain cancellation accepted", err)
+ }
+ return
+ case "lost-close", "lost-retirement":
+ if err == nil {
+ t.Fatal("durability failure ignored")
+ }
+ v.kernel = func(context.Context, release.GenerationReservation) (string, error) {
+ return strings.Repeat("a", 64), nil
+ }
+ if err = v.retire(t.Context(), ledger, r); err != nil {
+ t.Fatal("recovery", err)
+ }
+ got = readGenerationLaunch(t, disk)
+ default:
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ if !got.Closed || got.Retirement == "" || got.Process != (release.GenerationProcess{}) {
+ t.Fatal("reservation not cancelled", got)
+ }
+ if err = f.journal.IntendUnit(t.Context(), *f.req.Reservation); err == nil {
+ t.Fatal("closed controller replayed intention")
+ }
+ if _, err = ledger.Reserve(r.Binding); err == nil {
+ t.Fatal("cancelled reservation replayed")
+ }
+ views, err := f.root.Guard.Root.GenerationInstallations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ launches, err = f.journal.GenerationLaunches(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ var exact []hostbox.LaunchInventory
+ for _, l := range launches {
+ if generationOwner(l.Target) == r.Binding.Target {
+ exact = append(exact, l)
+ }
+ }
+ snapshot := generationInventory{Installations: views, Launches: exact, Payloads: generationPayloadFixture(views)}
+ snapshot.Census.Schema = 1
+ snapshot.Census.Complete = true
+ snapshot.Census.Box = views[0].Installation.Box
+ snapshot.Census.Enrollment = views[0].Installation.Enrollment
+ snapshot.Census.Migration = 1
+ snapshot.Census.Revision = 1
+ snapshot.Census.LaunchRevision = 1
+ verifier := generationReconciler{read: func(context.Context) (generationInventory, error) { return snapshot, nil }, kernel: func(context.Context) (string, string, error) { return "boot", strings.Repeat("a", 64), nil }}
+ if _, err = verifier.reconcile(t.Context(), []release.GenerationReservation{got}); err != nil {
+ t.Fatal("cancelled pin failed census", err)
+ }
+ })
+ }
+}
+
+func TestPlan14RetiredRecordDeletion(t *testing.T) {
+ f, _, rec := generationLaunchFixture(t)
+ rec.Schema = 1
+ plan14PutRecord(t, f.root.generations.store, rec)
+ views, err := f.root.Guard.Root.GenerationInstallations(t.Context())
+ if err != nil {
+ t.Fatal(err)
+ }
+ in := views[0].Installation
+ views[0].Retired = []hostguard.Captured{{Incarnation: in.Incarnation, Stamp: in.Stamp}}
+ views[0].Installation.Empty = true
+ snapshot := generationInventory{Installations: views, Payloads: generationPayloadFixture(views)[:1]}
+ if err = f.root.generations.store.Delete(t.Context(), in.Record); err != nil {
+ t.Fatal(err)
+ }
+ if _, err = generationRequiredInventory(t.Context(), f.root.generations.store, snapshot, nil); err != nil {
+ t.Fatal("retired record required", err)
+ }
+ views[0].Retired = nil
+ if _, err = generationRequiredInventory(t.Context(), f.root.generations.store, snapshot, nil); err == nil {
+ t.Fatal("missing active record accepted")
+ }
+}
+
+func TestPlan14SettingsSuccessorPreservesDenials(t *testing.T) {
+ ctx := t.Context()
+ store := release.NewDirStore(t.TempDir())
+ rec := provision.Record{Schema: 1, Host: "settings.example", Account: "bab", App: "shop", Channel: "stable", CreatedAt: time.Now().UTC()}
+ if err := provision.CreateRecord(ctx, store, rec); err != nil {
+ t.Fatal(err)
+ }
+ inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ state := &rollbackMemory{files: map[string][]byte{}}
+ book, err := rollout.InitializeRootFailures(state, "box/en/1", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ p := rootUpdateSource{enforceFailures: true, state: state, store: store, book: book, binding: &proofBindingSource{}, data: t.TempDir()}
+ p.source.census = func() (rootparticipation.Census, error) {
+ return rootparticipation.Census{Schema: 1, Complete: true, Box: "box", Enrollment: "en", Migration: 1}, nil
+ }
+ in := hostguard.RootInstallation{Host: rec.Host, Incarnation: inc, Stamp: 1, Account: rec.Account, App: rec.App, Record: provision.RecordKey(rec.Account, rec.App, rec.Host), Box: "box", Enrollment: "en", Migration: 1, InventoryRevision: 1}
+ for _, stamp := range []uint64{1, 2} {
+ authority := &rollbackMemory{files: map[string][]byte{}}
+ in.Stamp = stamp
+ if err = hostguard.NewRootAuthority(authority).RetainMigrationDeny(ctx, in, stamp); err != nil {
+ t.Fatal(err)
+ }
+ p.source.state = authority
+ row, err := p.resolve(ctx, rec.Host)
+ if err != nil {
+ t.Fatal("successor settings", stamp, err)
+ }
+ if stamp == 1 {
+ if err = row.Save(release.Consumed{Completed: 17}); err != nil {
+ t.Fatal(err)
+ }
+ finish, err := row.Begin("v1", strings.Repeat("a", 64), "first")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = finish("", os.ErrPermission, nil); err != nil {
+ t.Fatal(err)
+ }
+ key := rootSettingsKey(rec.Host, rootHistoryInstallation(in))
+ raw, err := state.Read(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = state.Write("settings-"+rec.Host+".json", raw); err != nil {
+ t.Fatal(err)
+ }
+ if err = state.Remove(key); err != nil {
+ t.Fatal(err)
+ }
+ recovered, err := p.resolve(ctx, rec.Host)
+ if err != nil || recovered.Consumed == nil || recovered.Consumed.Completed != 17 {
+ t.Fatal("same-installation legacy settings lost", err)
+ }
+ } else {
+ if row.Consumed != nil || row.Build.State != nil {
+ t.Fatal("foreign completion reused")
+ }
+ if !row.Failed("v1") {
+ t.Fatal("successor lost required denial")
+ }
+ if row.Route.Socket != provision.NestedSocket(rec.Host) || row.Route.DBPath != provision.NestedDBPath(p.data, rec.Host) {
+ t.Fatal("claim readiness used legacy paths", row.Route)
+ }
+ }
+ }
+}
diff --git a/internal/host/plan14_recovery_without_health_test.go b/internal/host/plan14_recovery_without_health_test.go
index 8cfa40b12..65c53c1ec 100644
--- a/internal/host/plan14_recovery_without_health_test.go
+++ b/internal/host/plan14_recovery_without_health_test.go
@@ -27,7 +27,7 @@ func signedRecoveryFixture(t *testing.T) (*proofRootEnv, *plan14RestartUnits, ro
t.Fatal(err)
}
}
- source := &proofBindingSource{store: e.store, sys: u, optDir: u.opt, openFile: os.Open, unitDirs: []string{filepath.Dir(u.props.FragmentPath)}}
+ source := &proofBindingSource{signedRecovery: true, store: e.store, sys: u, optDir: u.opt, openFile: os.Open, unitDirs: []string{filepath.Dir(u.props.FragmentPath)}}
c.resolve, c.host, c.rollback, c.prepareRollback, c.recovered = source.resolve, source.forHost, source.rollback, source.prepareRollback, source.recovered
return e, u, c, source, pub
}
diff --git a/internal/host/plan14_root_failures_test.go b/internal/host/plan14_root_failures_test.go
index 515ebb16c..6bb5a5f49 100644
--- a/internal/host/plan14_root_failures_test.go
+++ b/internal/host/plan14_root_failures_test.go
@@ -211,7 +211,7 @@ func testRootSettingsPass(t *testing.T, book *rollout.RootFailures, attack strin
}
return
}
- raw, _ := state.Read("settings-" + rt.Host + ".json")
+ raw, _ := state.Read(rootSettingsKey(rt.Host, rootHistoryInstallation(in)))
var saved rootUpdateRecord
if json.Unmarshal(raw, &saved) != nil || saved.Consumed == nil || saved.Consumed.Desired != "" || saved.Consumed.Completed != 0 {
t.Fatal("forged completion", string(raw))
@@ -244,7 +244,7 @@ func testRootSettingsPass(t *testing.T, book *rollout.RootFailures, attack strin
if v, _ := release.LiveVersion(opt, rt.Host); v != "v1" {
t.Fatal("verified retry did not reach production adoption", v)
}
- raw, _ = state.Read("settings-" + rt.Host + ".json")
+ raw, _ = state.Read(rootSettingsKey(rt.Host, rootHistoryInstallation(in)))
var mirror rootUpdateRecord
if json.Unmarshal(raw, &mirror) != nil || mirror.RetryMirror == nil || mirror.RetryMirror.Book != legacy.ID() || mirror.RetryMirror.Epoch != legacy.PublishedEpoch(rec.Account, rec.App, "v1", rec.Host) {
t.Fatal("root did not bind agent mirror receipt", string(raw))
diff --git a/internal/host/proofbinding.go b/internal/host/proofbinding.go
index 71497c9fb..f5bfbeeae 100644
--- a/internal/host/proofbinding.go
+++ b/internal/host/proofbinding.go
@@ -48,23 +48,24 @@ type proofUnitFile struct {
type proofBindingResolver func(context.Context, registry.Route) (proofBinding, error)
type proofBindingSource struct {
- persistent func(*os.File) (rootstate.FileIdentity, error)
- indexMu sync.Mutex
- index *provision.RecordIndex
- indexErr error
- cacheIndex bool
- checkPointers func(string) error
- now func() time.Time
- namedPath string
- attempts func() (rootstate.Store, error)
- store release.Store
- claim func(context.Context, string) (hostclaim.Observed, error)
- box func(context.Context) (string, error)
- sys unitSystem
- optDir string
- openFile func(string) (*os.File, error)
- unitDirs []string
- healthy func(string, string, string, string) *release.HealthyBuild
+ signedRecovery bool
+ persistent func(*os.File) (rootstate.FileIdentity, error)
+ indexMu sync.Mutex
+ index *provision.RecordIndex
+ indexErr error
+ cacheIndex bool
+ checkPointers func(string) error
+ now func() time.Time
+ namedPath string
+ attempts func() (rootstate.Store, error)
+ store release.Store
+ claim func(context.Context, string) (hostclaim.Observed, error)
+ box func(context.Context) (string, error)
+ sys unitSystem
+ optDir string
+ openFile func(string) (*os.File, error)
+ unitDirs []string
+ healthy func(string, string, string, string) *release.HealthyBuild
}
func proofRouteReady(rt registry.Route, timeout time.Duration) func() error {
@@ -110,6 +111,7 @@ func productionProofBindingSource(store release.Store, sys unitSystem, optDir st
}}
if legacy, err := legacyRootRestart(); err == nil && !legacy {
source.persistent = rootstate.PersistentIdentity
+ source.signedRecovery = true
}
if hostwrite.Enabled(os.Getenv) {
source.claim = func(ctx context.Context, host string) (hostclaim.Observed, error) {
@@ -312,7 +314,7 @@ func (s *proofBindingSource) binding(ctx context.Context, host string, rollback
return proofBinding{}, os.ErrPermission
}
historical, history, signed = rollback.Historical, rollback.History, rollback.Signed
- } else if !historical {
+ } else if !historical && s.signedRecovery {
pub, keyErr := release.ParsePublicKey(os.Getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY")
if keyErr == nil {
m, authErr := release.AuthorizedManifest(ctx, s.store, pub, rec.Account, rec.App, version)
diff --git a/internal/host/rootgeneration_handover.go b/internal/host/rootgeneration_handover.go
new file mode 100644
index 000000000..93eafe904
--- /dev/null
+++ b/internal/host/rootgeneration_handover.go
@@ -0,0 +1,188 @@
+package host
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "os"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func productionGenerationHandovers(ctx context.Context) ([]hostbox.GenerationHandover, error) {
+ path := filepath.Join(claimRetireJournalDir("", ""), "serving")
+ s, err := rootstate.Open(path, false, false)
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ defer s.Close()
+ out, err := generationReadHandovers(ctx, s)
+ if err != nil {
+ return nil, err
+ }
+ directory, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer directory.Close()
+ if err = directory.Sync(); err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func generationReadHandovers(ctx context.Context, s rootstate.Store) ([]hostbox.GenerationHandover, error) {
+ names, err := s.Names()
+ if err != nil {
+ return nil, err
+ }
+ if len(names) > 8192 {
+ return nil, os.ErrPermission
+ }
+ sort.Strings(names)
+ var out []hostbox.GenerationHandover
+ for _, name := range names {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ if strings.HasPrefix(name, ".handover-") {
+ continue
+ }
+ raw, err := s.Read(name)
+ if err != nil {
+ return nil, err
+ }
+ j, err := hostbox.DecodeGenerationHandover(raw)
+ if err != nil || name != j.ID+".json" {
+ return nil, os.ErrPermission
+ }
+ out = append(out, j)
+ }
+ return out, nil
+}
+
+func generationTransfers(ctx context.Context, snapshot generationInventory) (map[int]int, error) {
+ out := map[int]int{}
+ for _, j := range snapshot.Handovers {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ raw, err := json.Marshal(j)
+ if err != nil {
+ return nil, err
+ }
+ if _, err = hostbox.DecodeGenerationHandover(raw); err != nil {
+ return nil, err
+ }
+ p, n := j.Prior, j.Successor
+ oldIndex, nextIndex := -1, -1
+ for i, payload := range snapshot.Payloads {
+ in := payload.Inventory
+ if in.Host != p.Host || in.Incarnation != p.Incarnation {
+ continue
+ }
+ if in.InstallationStamp == p.Stamp {
+ if oldIndex != -1 || j.ServingInstallation == nil || payload.Revision != j.ServingInstallation.InventoryRevision || !reflect.DeepEqual(in, j.Inventory) {
+ return nil, os.ErrPermission
+ }
+ oldIndex = i
+ }
+ if in.InstallationStamp == n.Stamp {
+ if nextIndex != -1 || in.Box != n.Box || in.Enrollment != n.Enrollment || in.DirectoryRevision != n.DirectoryRevision {
+ return nil, os.ErrPermission
+ }
+ nextIndex = i
+ }
+ }
+ if oldIndex < 0 || nextIndex < 0 {
+ return nil, os.ErrPermission
+ }
+ if _, exists := out[oldIndex]; exists {
+ return nil, os.ErrPermission
+ }
+ bound := false
+ for _, view := range snapshot.Installations {
+ if view.Host != p.Host {
+ continue
+ }
+ in, err := generationInstallation(view, generationOwner(p))
+ if err != nil || rootHistoryInstallation(in) != rootHistoryInstallation(*j.ServingInstallation) {
+ return nil, os.ErrPermission
+ }
+ retired := false
+ for _, old := range view.Retired {
+ if old == (hostguard.Captured{Incarnation: p.Incarnation, Stamp: p.Stamp}) {
+ retired = true
+ }
+ }
+ next := p
+ next.Stamp, next.Session = n.Stamp, n.Session
+ if _, err := generationInstallation(view, generationOwner(next)); err != nil || !retired {
+ return nil, os.ErrPermission
+ }
+ receipt := p.Session == 0 && reflect.DeepEqual(j.Receipt, hostguard.FenceReceipt{})
+ for _, r := range view.Receipts {
+ if r.Target == p && j.Receipt.Target == p && bytes.Equal(r.Bytes, j.Receipt.Bytes) && verifyRetirementReceipt(ctx, r) == nil {
+ receipt = true
+ }
+ }
+ if !receipt {
+ return nil, os.ErrPermission
+ }
+ bound = true
+ }
+ if !bound {
+ return nil, os.ErrPermission
+ }
+ next := snapshot.Payloads[nextIndex].Inventory
+ for _, old := range j.Inventory.Entries {
+ matches := 0
+ for _, entry := range next.Entries {
+ if entry.Kind != old.Kind || entry.ManagedRoot != old.ManagedRoot || entry.RelativePath != old.RelativePath || entry.ArtifactID != old.ArtifactID {
+ continue
+ }
+ if old.Materialization != "materialized" || entry.Materialization != "materialized" || old.Ownership != j.Before.Route.ClaimOwner || entry.Ownership != j.After.Route.ClaimOwner {
+ return nil, os.ErrPermission
+ }
+ switch old.Kind {
+ case "route":
+ if old.RowIdentity != j.Before.Route.ClaimRowID || entry.RowIdentity != j.After.Route.ClaimRowID {
+ return nil, os.ErrPermission
+ }
+ case "path":
+ if old.Type != entry.Type || old.Inode != entry.Inode || (old.Identity.Boot != "" && !old.Identity.Same(entry.Identity)) || (old.Identity.Boot == "" && (old.Device != entry.Device || entry.Identity.Boot != p.BootID || entry.Identity.Device != entry.Device || entry.Identity.Inode != entry.Inode)) {
+ return nil, os.ErrPermission
+ }
+ case "timer":
+ found := false
+ for i, timer := range j.Before.Timers {
+ if old.RelativePath == p.Host+"/"+timer.Name && old.RowIdentity == timer.ClaimRowID && entry.RowIdentity == j.After.Timers[i].ClaimRowID {
+ found = true
+ }
+ }
+ if !found {
+ return nil, os.ErrPermission
+ }
+ default:
+ return nil, os.ErrPermission
+ }
+ matches++
+ }
+ if matches != 1 {
+ return nil, os.ErrPermission
+ }
+ }
+ out[oldIndex] = nextIndex
+ }
+ return out, nil
+}
diff --git a/internal/host/rootgeneration_inventory.go b/internal/host/rootgeneration_inventory.go
index 71147c296..20d3edc6a 100644
--- a/internal/host/rootgeneration_inventory.go
+++ b/internal/host/rootgeneration_inventory.go
@@ -17,6 +17,7 @@ import (
)
type generationInventory struct {
+ Handovers []hostbox.GenerationHandover
Retirements []hostbox.RetirementJournal
Census rootparticipation.Census
Installations []hostguard.FenceView
@@ -149,15 +150,17 @@ func (v generationReconciler) reconcile(ctx context.Context, records []release.G
if matched[i] {
continue
}
- // Reserve precedes the launch intention. This crash window stays pinned;
- // it cannot become a retired or observed generation without its journal.
+ // A pre-intention reservation retains its exact controller journal.
in, err := generationInstallation(installs[r.Binding.Target.Host], r.Binding.Target)
- if err != nil || r.Closed || r.Process != (release.GenerationProcess{}) || r.Binding.Installation != rootHistoryInstallation(in) || r.Binding.Binding.Account != in.Account || r.Binding.Binding.App != in.App || r.Binding.Record != in.Record || !hosts[in.Host] {
+ if err != nil || r.Process != (release.GenerationProcess{}) || r.Binding.Installation != rootHistoryInstallation(in) || r.Binding.Binding.Account != in.Account || r.Binding.Binding.App != in.App || r.Binding.Record != in.Record || !hosts[in.Host] {
return zero, os.ErrPermission
}
pending := 0
for _, launch := range before.Launches {
if generationOwner(launch.Target) == r.Binding.Target && launch.ControllerPID == r.Binding.ControllerPID && launch.ProcessStart == r.Binding.ControllerStart {
+ if r.Closed && !launch.WritersClosed {
+ return zero, os.ErrPermission
+ }
pending++
}
}
@@ -191,7 +194,7 @@ func (v generationReconciler) reconcile(ctx context.Context, records []release.G
if err := generationPayloadFiles(ctx, dataDir(), physical); err != nil {
return zero, err
}
- return release.GenerationEvidence{Box: c.Box, Enrollment: c.Enrollment, Migration: c.Migration, Boot: boot, Installations: generationInventoryDigest([]any{before.Installations, before.Payloads, before.Records, before.Retirements}), Launches: generationInventoryDigest([]any{c.LaunchRevision, before.Launches}), Kernel: kernel}, nil
+ return release.GenerationEvidence{Box: c.Box, Enrollment: c.Enrollment, Migration: c.Migration, Boot: boot, Installations: generationInventoryDigest([]any{before.Installations, before.Payloads, before.Records, before.Retirements, before.Handovers}), Launches: generationInventoryDigest([]any{c.LaunchRevision, before.Launches}), Kernel: kernel}, nil
}
func generationPayloadBindings(payloads []hostbox.GenerationPayload, installs map[string]hostguard.FenceView) error {
@@ -252,6 +255,10 @@ func readProductionGenerationInventory(ctx context.Context, local bool) (generat
if err != nil {
return generationInventory{}, err
}
+ snapshot.Handovers, err = productionGenerationHandovers(ctx)
+ if err != nil {
+ return generationInventory{}, err
+ }
snapshot.Retirements, err = productionGenerationRetirements(ctx)
if err != nil {
return generationInventory{}, err
diff --git a/internal/host/rootgeneration_parked.go b/internal/host/rootgeneration_parked.go
index 23b9b9c8e..f989ccd90 100644
--- a/internal/host/rootgeneration_parked.go
+++ b/internal/host/rootgeneration_parked.go
@@ -19,6 +19,10 @@ import (
// A completed root retirement changes where an obligation is checked. It never
// deletes the payload, its requirements, or a generation reservation.
func generationRetirements(ctx context.Context, snapshot generationInventory) (map[int]string, error) {
+ transfers, err := generationTransfers(ctx, snapshot)
+ if err != nil {
+ return nil, err
+ }
retired := map[int]string{}
for _, j := range snapshot.Retirements {
if err := ctx.Err(); err != nil {
@@ -98,6 +102,25 @@ func generationRetirements(ctx context.Context, snapshot generationInventory) (m
}
retired[index] = j.ID
}
+ for prior := range transfers {
+ next := prior
+ for steps := 0; ; steps++ {
+ if steps > len(transfers) {
+ return nil, os.ErrPermission
+ }
+ successor, ok := transfers[next]
+ if !ok {
+ break
+ }
+ next = successor
+ }
+ if id := retired[next]; id != "" {
+ if retired[prior] != "" {
+ return nil, os.ErrPermission
+ }
+ retired[prior] = id
+ }
+ }
return retired, nil
}
@@ -117,6 +140,35 @@ func generationPhysicalPayloads(ctx context.Context, snapshot generationInventor
}
}
payloads := append([]hostbox.GenerationPayload(nil), snapshot.Payloads...)
+ transfers, err := generationTransfers(ctx, snapshot)
+ if err != nil {
+ return nil, err
+ }
+ for prior := range transfers {
+ next := transfers[prior]
+ for steps := 0; ; steps++ {
+ if steps > len(transfers) {
+ return nil, os.ErrPermission
+ }
+ successor, ok := transfers[next]
+ if !ok {
+ break
+ }
+ next = successor
+ }
+ in := &payloads[prior].Inventory
+ in.Entries = append([]hostbox.StopInventoryEntry(nil), in.Entries...)
+ for i, entry := range in.Entries {
+ if entry.Kind != "path" {
+ continue
+ }
+ for _, current := range snapshot.Payloads[next].Inventory.Entries {
+ if current.Kind == "path" && current.ManagedRoot == entry.ManagedRoot && current.RelativePath == entry.RelativePath {
+ in.Entries[i].Identity, in.Entries[i].Device = current.Identity, current.Device
+ }
+ }
+ }
+ }
for i, id := range retired {
in := &payloads[i].Inventory
in.Entries = append([]hostbox.StopInventoryEntry(nil), in.Entries...)
@@ -148,7 +200,7 @@ func generationRetirementRecords(ctx context.Context, store rootstate.Store) ([]
}
// Cold handover retains its own payload inventory. It is never parking
// authority here. Temporary writes cannot establish completed retirement.
- if name == "cold" || strings.HasPrefix(name, ".retire-") {
+ if name == "cold" || name == "serving" || strings.HasPrefix(name, ".retire-") {
continue
}
if !strings.HasSuffix(name, ".json") {
diff --git a/internal/host/rootgeneration_requirements.go b/internal/host/rootgeneration_requirements.go
index b1d3db1e4..2c87b24e4 100644
--- a/internal/host/rootgeneration_requirements.go
+++ b/internal/host/rootgeneration_requirements.go
@@ -22,6 +22,10 @@ func generationRequiredInventory(ctx context.Context, store release.Store, snaps
if store == nil {
return zero, os.ErrPermission
}
+ retirements, err := generationRetirements(ctx, snapshot)
+ if err != nil {
+ return zero, err
+ }
snapshot.Records = nil
for _, view := range snapshot.Installations {
if err := ctx.Err(); err != nil {
@@ -34,6 +38,20 @@ func generationRequiredInventory(ctx context.Context, store release.Store, snaps
if in.Record != provision.RecordKey(in.Account, in.App, in.Host) {
return zero, os.ErrPermission
}
+ retired := false
+ for _, old := range view.Retired {
+ if old.Incarnation == in.Incarnation && old.Stamp == in.Stamp {
+ retired = true
+ }
+ }
+ if retired {
+ for i, payload := range snapshot.Payloads {
+ if payload.Inventory.Host == in.Host && len(payload.Inventory.Entries) != 0 && retirements[i] == "" {
+ return zero, os.ErrPermission
+ }
+ }
+ continue
+ }
rec, etag, err := provision.GetRecordETag(ctx, store, in.Account, in.App, in.Host)
if err != nil {
return zero, err
diff --git a/internal/host/rootgeneration_retirement.go b/internal/host/rootgeneration_retirement.go
index 3ca90ac9e..59d3ee5ce 100644
--- a/internal/host/rootgeneration_retirement.go
+++ b/internal/host/rootgeneration_retirement.go
@@ -42,12 +42,14 @@ func (v generationRetirement) closure(ctx context.Context, r release.GenerationR
}
matches, controllers := 0, 0
closed := false
+ writersClosed := false
for _, launch := range launches {
target := launch.Target
if target.Host != t.Host || target.Incarnation != t.Incarnation || target.Stamp != t.Stamp || target.Session != t.Session || target.Box != t.Box || target.Enrollment != t.Enrollment || target.BootID != t.BootID || target.ControllerID != t.ControllerID {
continue
}
controllers++
+ writersClosed = launch.WritersClosed
if launch.ControllerPID != b.ControllerPID || launch.ProcessStart != b.ControllerStart {
return false, os.ErrPermission
}
@@ -66,9 +68,11 @@ func (v generationRetirement) closure(ctx context.Context, r release.GenerationR
closed = launch.WritersClosed && unit.State == hostbox.UnitSettled
}
}
- // Reserve precedes IntendUnit. Keep that exact prelaunch obligation pinned.
- if controllers == 1 && matches == 0 && !r.Closed && r.Process == (release.GenerationProcess{}) {
- return false, nil
+ if controllers == 1 && matches == 0 && r.Process == (release.GenerationProcess{}) {
+ if r.Closed && !writersClosed {
+ return false, os.ErrPermission
+ }
+ return writersClosed, nil
}
if controllers != 1 || matches != 1 || (r.Closed && !closed) {
return false, os.ErrPermission
diff --git a/internal/host/rootgeneration_rows.go b/internal/host/rootgeneration_rows.go
index 417fcfa53..0c1bc0a05 100644
--- a/internal/host/rootgeneration_rows.go
+++ b/internal/host/rootgeneration_rows.go
@@ -32,12 +32,19 @@ func generationRegistryInventory(ctx context.Context, reg *registry.Registry, sn
if err != nil {
return zero, err
}
+ transferred, err := generationTransfers(ctx, snapshot)
+ if err != nil {
+ return zero, err
+ }
type key struct{ kind, host, name string }
expected := map[key]registry.ClaimOwnership{}
hosts := map[string]bool{}
for index, p := range snapshot.Payloads {
in := p.Inventory
hosts[in.Host] = true
+ if _, ok := transferred[index]; ok {
+ continue
+ }
if retired[index] != "" {
continue
}
diff --git a/internal/host/rootinventory_test.go b/internal/host/rootinventory_test.go
index 7e1a9468f..ae17b7faf 100644
--- a/internal/host/rootinventory_test.go
+++ b/internal/host/rootinventory_test.go
@@ -100,9 +100,22 @@ func TestPlan14RootInventoryRejectsOldBootMaterialization(t *testing.T) {
}
func testInventoryRegistration(t *testing.T, persistent bool, oldBoot ...bool) {
- for _, stale := range []bool{false, true} {
+ testInventoryRegistrationWith(t, persistent, oldBoot, nil, nil)
+}
+func testInventoryRegistrationWith(t *testing.T, persistent bool, oldBoot []bool, authority rootstate.Store, after func(*testing.T, *recoveryFixture, ClaimRootHTTP, registry.Route, hostbox.Actor)) {
+ cases := []bool{false, true}
+ if after != nil {
+ cases = []bool{false}
+ }
+ for _, stale := range cases {
t.Run(fmt.Sprint(stale), func(t *testing.T) {
f := newRecoveryFixture(t)
+ if after != nil {
+ o := f.read()
+ if _, err := hostclaim.Update(t.Context(), f.claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: "box-1", Region: "ie", Mode: "cold", NewGeneration: "first-generation"})); err != nil {
+ t.Fatal(err)
+ }
+ }
obs := f.read()
holder := obs.Claim.Holder
rec := provision.Record{Schema: 1, Host: obs.Host, Account: holder.Account, App: holder.App, Channel: "stable", CreatedAt: f.now}
@@ -114,6 +127,9 @@ func testInventoryRegistration(t *testing.T, persistent bool, oldBoot ...bool) {
}
rootGuard := f.guard
rootGuard.Root = hostguard.NewRootAuthority(&rollbackDisk{dir: t.TempDir()})
+ if authority != nil {
+ rootGuard.Root = hostguard.NewRootAuthority(authority)
+ }
roots := map[string]string{"data": f.guard.DataDir, "opt": t.TempDir(), "units": t.TempDir()}
actor := hostbox.Actor{Box: "box-1", Enrollment: "en", Revision: 1, Region: "ie"}
census := &hostbox.EmptyInstallationCensus{Inventory: journals.Inventory, Registry: reg, Launches: f.journal, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "1", Roots: roots, SharedPaths: map[string][]string{"data": {"hostguard"}}}
@@ -223,6 +239,9 @@ func testInventoryRegistration(t *testing.T, persistent bool, oldBoot ...bool) {
if err != nil || revision != 5 {
t.Fatalf("state phases missing: %d %v", revision, err)
}
+ if after != nil {
+ after(t, f, root, row, actor)
+ }
})
}
diff --git a/internal/host/rootupdates.go b/internal/host/rootupdates.go
index aa87e2f70..b60136f51 100644
--- a/internal/host/rootupdates.go
+++ b/internal/host/rootupdates.go
@@ -3,6 +3,8 @@ package host
import (
"context"
"crypto/ed25519"
+ "crypto/sha256"
+ "encoding/hex"
"encoding/json"
"errors"
"fmt"
@@ -155,13 +157,27 @@ func (p rootUpdateSource) resolve(ctx context.Context, host string) (release.Roo
if err = provision.ValidatePolicy(policy); err != nil {
return out, err
}
- rt := provision.RouteFor(*rec, p.data, "", false, policy)
+ rt := provision.RouteFor(*rec, p.data, "", true, policy)
// A remote proposal cannot supply protected local lifecycle or served facts.
rt.GoodVersion, rt.GoodDigest, rt.GoodDemand, rt.ServingVersion, rt.BuildState = "", "", "", "", ""
installation := rootHistoryInstallation(in)
- key := "settings-" + host + ".json"
+ key := rootSettingsKey(host, installation)
var saved rootUpdateRecord
raw, err := p.state.Read(key)
+ if errors.Is(err, os.ErrNotExist) {
+ old, readErr := p.state.Read("settings-" + host + ".json")
+ if readErr == nil {
+ var previous rootUpdateRecord
+ if rootparticipation.Decode(old, &previous) != nil || previous.Schema != 1 {
+ return out, os.ErrPermission
+ }
+ if previous.Installation == installation {
+ raw, err = old, nil
+ }
+ } else if !errors.Is(readErr, os.ErrNotExist) {
+ return out, readErr
+ }
+ }
if err == nil {
if rootparticipation.Decode(raw, &saved) != nil || saved.Schema != 1 || saved.Installation != installation || saved.Route.Host != host || saved.Route.Account != rec.Account || saved.Route.App != rec.App {
return out, os.ErrPermission
@@ -286,3 +302,8 @@ func initializeMigrationFailures(dd string, execution rootstate.Store) error {
_, err = rollout.InitializeRootFailures(s, rootUpdatesMigration(c), raw)
return err
}
+
+func rootSettingsKey(host, installation string) string {
+ sum := sha256.Sum256([]byte(host + "\x00" + installation))
+ return "settings-" + hex.EncodeToString(sum[:]) + ".json"
+}
diff --git a/internal/hostbox/generationhandover.go b/internal/hostbox/generationhandover.go
new file mode 100644
index 000000000..0fd83992b
--- /dev/null
+++ b/internal/hostbox/generationhandover.go
@@ -0,0 +1,39 @@
+package hostbox
+
+import (
+ "reflect"
+
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+// GenerationHandover is retained transfer evidence, read from root's journal.
+// It cannot authorize a new handover or discard a launch reservation.
+type GenerationHandover coldJournal
+
+func DecodeGenerationHandover(raw []byte) (GenerationHandover, error) {
+ var j coldJournal
+ if decodeStrict(raw, &j) != nil || j.V != 1 || j.Phase != "complete" || j.IntendedAbsent || j.ServingInstallation == nil {
+ return GenerationHandover{}, ErrTypedJournal
+ }
+ p, n, in := j.Prior, j.Successor, j.Inventory
+ if p.Host == "" || p.Incarnation == "" || n.Host != p.Host || n.Incarnation != p.Incarnation || n.Box != p.Box || n.Enrollment != p.Enrollment || n.Stamp <= p.Stamp || n.Session != n.Stamp || n.Session <= p.Session || n.Generation == "" || n.ETag == "" || n.Op != "" || n.Phase != "" || p.Op != "" || p.Phase != "" || p.ObligationID != "" || j.ID != "serving-handover-"+ownershipFor(n.Host, n.Incarnation, n.Stamp, n.Box) || !in.Complete || in.Host != p.Host || in.Incarnation != p.Incarnation || in.InstallationStamp != p.Stamp || in.Box != p.Box || in.Enrollment != p.Enrollment || in.DirectoryRevision != p.DirectoryRevision {
+ return GenerationHandover{}, ErrTypedJournal
+ }
+ barrier := p
+ barrier.FenceID, barrier.Stamp, barrier.Session, barrier.ClaimETag, barrier.DirectoryRevision = j.ID, n.Stamp, n.Session, n.ETag, n.DirectoryRevision
+ if j.Barrier != barrier {
+ return GenerationHandover{}, ErrTypedJournal
+ }
+ after := j.Before
+ after.Timers = append([]registry.Timer(nil), j.Before.Timers...)
+ owner := ownershipFor(n.Host, n.Incarnation, n.Stamp, n.Box)
+ after.Route.ClaimOwnership = registry.ClaimOwnership{ClaimIncarnation: n.Incarnation, ClaimStamp: n.Stamp, ClaimOwner: owner, ClaimRowID: rowIDFor(owner, "route", n.Host)}
+ for i := range after.Timers {
+ after.Timers[i].ClaimOwnership = after.Route.ClaimOwnership
+ after.Timers[i].ClaimRowID = rowIDFor(owner, "timer", after.Timers[i].Name)
+ }
+ if !reflect.DeepEqual(after, j.After) {
+ return GenerationHandover{}, ErrTypedJournal
+ }
+ return GenerationHandover(j), nil
+}
diff --git a/internal/provision/plan14_p3_census_test.go b/internal/provision/plan14_p3_census_test.go
index 3c3b96ceb..710c429a1 100644
--- a/internal/provision/plan14_p3_census_test.go
+++ b/internal/provision/plan14_p3_census_test.go
@@ -35,15 +35,23 @@ func plan14P3WriteCensusInventory() []censusEntry {
(e.File == "internal/host/rootinventory.go" && e.Unit == "ClaimRootHTTP.commitRootInventoryPending") {
continue
}
+ if e.File == "internal/host/rootgeneration_inventory.go" && e.Unit == "generationReconciler.reconcile" {
+ continue
+ }
+ if e.File == "internal/host/rootgeneration_requirements.go" && e.Unit == "generationRequiredInventory" {
+ continue
+ }
kept = append(kept, e)
}
return append(kept,
- censusEntry{"internal/host/proofbinding.go", "proofBindingSource.binding", "key-site", "key:RecordKey", "4a47da1beac6c8d56047d2a928f05005cb2a63be02478ec93260a390b6162ab0"},
+ censusEntry{"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "23995980eb4aa9c02618ce649b583344fc80ee1ee193ff84fb9a46f49d33ce68"},
+ censusEntry{"internal/host/rootgeneration_requirements.go", "generationRequiredInventory", "key-site", "key:RecordKey", "3c0eb88abc5680c67191d1e848e93ba528c66e81bcd521577098ce761fb827e0"},
+ censusEntry{"internal/host/proofbinding.go", "proofBindingSource.binding", "key-site", "key:RecordKey", "20ed721cbcaccb6ab813a0d317542a43fe057544811920ba43ac6431c3e73000"},
censusEntry{"internal/host/proofbinding.go", "proofBindingSource.unitBinding", "key-site", "key:RecordKey", "82e214d1cf66d29d0900f5c1ce9cfe0a2f31c070da4bf1497ef3fd5273c09aad"},
censusEntry{"internal/host/proofnamed.go", "proofBindingSource.record", "key-site", "key:RecordKey", "97f1501b9543e36c6a231170425c5c79c7c22489500aadf119e064abc1abde93"},
censusEntry{"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "18fcea255a124e5c970c32d943c960836602dd12d9f00669583d8b4bd72fe7e6"},
censusEntry{"internal/host/rootgenerations_launch.go", "rootGenerationSource.resolve", "key-site", "key:RecordKey", "3cd7c8132e9c451c983392f4d1cc676ddc14b761c90dec3e631d54ae800f9951"},
- censusEntry{"internal/host/rootupdates.go", "rootUpdateSource.resolve", "key-site", "key:RecordKey", "e2247ec55dae1342cccc71cb84f8dd8dc901968ba8453bbc2af572c78c8505d3"},
+ censusEntry{"internal/host/rootupdates.go", "rootUpdateSource.resolve", "key-site", "key:RecordKey", "8b7160d5b5cc1510eea7bbb3d9febe310df695337cfbc2b5e2ada7e9857ea83f"},
censusEntry{"internal/host/rootmovesfloor.go", "rootMovesFloors.refresh", "key-site", "key:writeRecord", "cb70a1eefbb4f8f243090270f23141c9c9a773ee278af002c79a6c3ef6b7c3b7"},
censusEntry{"internal/host/rootmovesfloor.go", "rootMovesFloors.persist", "key-site", "key:writeRecord", "2c1e0843c5865a31c0fab011d7d4825963bbb5a301727f37f3b1b6c3519eba5e"},
censusEntry{"internal/host/rootmovesfloor.go", "rootMovesFloors.writeRecord", "key-site", "key:writeRecord", "a8a51bdd73f8ab1447edf2241d19a41ba1bcb0084ef045d370e5282aca7e9e66"},
diff --git a/internal/provision/testdata/plan14-root-reads-p3.tsv b/internal/provision/testdata/plan14-root-reads-p3.tsv
index 534ccc61d..4d2af6612 100644
--- a/internal/provision/testdata/plan14-root-reads-p3.tsv
+++ b/internal/provision/testdata/plan14-root-reads-p3.tsv
@@ -201,12 +201,12 @@ internal/hostguard/rootmigration.go RootAuthority.CompleteMigrationInventory aut
@coverage internal/activator 0a01bc096c64f1886f810f4856d60f9a2642074a8b2ac0c56cbed77d122e76c8
@coverage internal/console 6f4998c39e6d66cd73ea98500000cbecac251dd21dafd58f51a85c2291e0c533
@coverage internal/edgesfile 7b48368150a427e4620f1c9ded49942b688c9e6c9cd983b7da43d3f2c0810b72
-@coverage internal/host d5a2aeca145f5881f0543a7bdb8998d3338ebb16e703ba330d58d98ec7da18bf
+@coverage internal/host a51ef433e83138e17482d44181a32650db1980f68283492cf7e53ac2cdce39b1
@coverage internal/host/units bf6ae516012701e0ca41908167e4efd60dd3c2c24e9af1555dec077e8be44461
-@coverage internal/hostbox f877b123ea1c5c361a3046f5962ea5ef8e1a3b8a534a29aa2c15aac507b37081
+@coverage internal/hostbox e1115353432aca616ea15aaa1a08ddba92d793dfa6552f4a02e72ad02fd3a656
@coverage internal/hostfloor 696e5ea4fa37b924b68e8e262235b99298ee19342d875213ba8209e5287dd3c6
@coverage internal/hostguard d115a22e2a8410adef697af3e73f420b663337dc827fcca82e181e15fdeb8834
-@coverage internal/release 4bf58e7d369ab2ceca1a6605ff71eabf453af7c1ae4b30565c72d6833bcea369
+@coverage internal/release 1c219907db7ac4ab5d7f5e416c37cd623045e5b5ef759f121ab327effa4017c3
@coverage internal/rootparticipation ffa18f535e706793976bb235426fa5852e65baba211be5077666c1dc738e7f0b
@coverage internal/rootstate 2cedbf9f0230a1e6e2ed41afe421a33796a58cdf50a37f0f709dabea24c9088b
@coverage internal/safefile 7030f312f7a60487381f0dbae2cdecaaa83625b639b6af15104f912592550816
@@ -217,10 +217,10 @@ internal/host/rootgenerations_launch.go type rootGenerationSource authority E2,E
internal/host/rootupdates.go initializeMigrationFailures authority G,E3 rev3=PROPOSED-p3;run=8;identity=offline journal-migrate uid 0;basis=requires complete root census before creating migration-bound root failure book; missing legacy history supplies no known refusal file:Open@254 5f7f652988f3def733e1b525260a52fac3568242960468e5457975ced1cd41d8
internal/host/rootupdates.go productionRootUpdates authority E3 rev3=PROPOSED-p3;run=10;identity=root claim controller and adopt uid 0;basis=keeps canonical root inputs and retry; leaves sticky failure enforcement disabled; legacy bytes only identify an optional exact retry mirror and cannot block opening or inject fresh denials file:Open@1124,file:Open@765,file:Read@1447,whole:Route@1760,whole:Route@2081 ce81dce48e37f4055efeb3fd8d3562057e504ec5cd74ccbef42c0b8fcd6f20f0
internal/host/rootupdates.go rootFailureLegacy authority E3 rev3=PROPOSED-p3;run=10;identity=root adoption and offline migration uid 0;basis=bounded nonblocking regular-file read supplies optional exact retry mirror identity or one-time migration deny evidence; refuses symlinks and special files file:OpenFile@65,file:ReadAll@265 efae62d1b996f14baa592e39e0cc22e0f5c8cde5cd94f429498d41039f5a70e4
-internal/host/rootupdates.go rootUpdateSource.resolve authority E3,J1,J3 rev3=PROPOSED-p3;run=10;identity=root claim controller and adopt uid 0;basis=binds canonical settings and retries; gates sticky root failure veto and projection delivery errors off while failure lifecycle acceptance is incomplete file:Read@1368,registry:Account@1543,registry:Account@2428,registry:App@1581,registry:App@2473,registry:BuildState@1210,registry:GoodDemand@1176,registry:GoodDigest@1161,registry:GoodVersion@1145,registry:Host@1515,registry:Host@2393,registry:ServingVersion@1191,registry:Unit@2124,whole:Route@1521,whole:Route@1549,whole:Route@1587,whole:Route@1756,whole:Route@1763,whole:Route@2183,whole:Route@2406,whole:Route@2441,whole:Route@2486,whole:Route@3254,whole:Route@3262,whole:Route@4890,whole:Route@4897 e2247ec55dae1342cccc71cb84f8dd8dc901968ba8453bbc2af572c78c8505d3
+internal/host/rootupdates.go rootUpdateSource.resolve authority E3,J1,J3 rev3=PROPOSED-p3;run=16;identity=root adopt/update/control uid 0;basis=canonical deployment settings stored per exact root installation with nested claim paths for readiness; legacy settings reused only for the same identity; successor completion state is empty while the shared root failure book retains denials file:Read@1374,file:Read@1447,registry:Account@1975,registry:Account@2860,registry:App@2013,registry:App@2905,registry:BuildState@1209,registry:GoodDemand@1175,registry:GoodDigest@1160,registry:GoodVersion@1144,registry:Host@1947,registry:Host@2825,registry:ServingVersion@1190,registry:Unit@2556,whole:Route@1953,whole:Route@1981,whole:Route@2019,whole:Route@2188,whole:Route@2195,whole:Route@2615,whole:Route@2838,whole:Route@2873,whole:Route@2918,whole:Route@3686,whole:Route@3694,whole:Route@5322,whole:Route@5329 8b7160d5b5cc1510eea7bbb3d9febe310df695337cfbc2b5e2ada7e9857ea83f
internal/host/updatesroot.go updatesPassOption authority E3 rev3=PROPOSED-p3;run=8;identity=root adopt uid 0; unchanged agent opener uid carlos;basis=installs census-only root input provider while preserving the legacy failure-book opener for nonparticipating passes 4f66774059952bda531435d68487fa032a95b8d1c8ca33ec8b71b6639fa117e3
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.;rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0; legacy agent pass uid carlos;basis=resolves protected route inputs before filters, recovery and dispatch on census boxes and bypasses service-owned failure opener only there registry:App@26795,registry:App@29252,registry:App@29744,registry:App@29981,registry:App@31388,registry:App@6590,registry:App@9615,registry:App@9756,registry:Hibernate@29627,registry:Host@1061,registry:Host@1426,registry:Host@29797,registry:Host@30713,registry:Host@31441,registry:Host@3263,registry:Host@3330,registry:Host@3426,registry:Host@3461,registry:UpdateSettings@9493,whole:Route@1046,whole:Route@1164,whole:Route@1188,whole:Route@1461,whole:Route@1622,whole:Route@26790,whole:Route@2726,whole:Route@2768,whole:Route@2877,whole:Route@2901,whole:Route@29247,whole:Route@2932,whole:Route@29505,whole:Route@29527,whole:Route@29574,whole:Route@29592,whole:Route@29602,whole:Route@29645,whole:Route@29666,whole:Route@29679,whole:Route@29739,whole:Route@29765,whole:Route@29839,whole:Route@29878,whole:Route@2989,whole:Route@29894,whole:Route@29922,whole:Route@29976,whole:Route@30002,whole:Route@3075,whole:Route@30755,whole:Route@30809,whole:Route@30825,whole:Route@3097,whole:Route@31332,whole:Route@31383,whole:Route@31409,whole:Route@31483,whole:Route@31518,whole:Route@31534,whole:Route@3197,whole:Route@4280,whole:Route@4927,whole:Route@6512,whole:Route@6534,whole:Route@6585,whole:Route@6611,whole:Route@761,whole:Route@803,whole:Route@9444,whole:Route@950,whole:Route@9558,whole:Route@9603,whole:Route@9751,whole:Route@988 270763804170539d06afd58252d86e1a8ff692ab4d9355be3a2127e9efc9d4a2
-internal/release/adopt.go adoptHost authority B3,E1,E2,E3,E4,J1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant.;rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0; legacy agent pass uid carlos;basis=applies root settings to canonically resolved routes including named units while legacy settings eligibility remains unchanged registry:App@10936,registry:App@11096,registry:App@12125,registry:App@13051,registry:App@14633,registry:App@19792,registry:App@25268,registry:App@3144,registry:App@33027,registry:App@6758,registry:App@8059,registry:App@8253,registry:Hibernate@33888,registry:Host@10816,registry:Host@10849,registry:Host@10949,registry:Host@11109,registry:Host@11211,registry:Host@11243,registry:Host@11308,registry:Host@11758,registry:Host@11891,registry:Host@12338,registry:Host@12447,registry:Host@12649,registry:Host@13141,registry:Host@13927,registry:Host@14405,registry:Host@14619,registry:Host@14961,registry:Host@15551,registry:Host@19282,registry:Host@19390,registry:Host@19760,registry:Host@20580,registry:Host@20738,registry:Host@2105,registry:Host@21200,registry:Host@21252,registry:Host@21399,registry:Host@21486,registry:Host@22592,registry:Host@22836,registry:Host@2294,registry:Host@23496,registry:Host@2375,registry:Host@23881,registry:Host@24022,registry:Host@25236,registry:Host@25363,registry:Host@26705,registry:Host@27355,registry:Host@30588,registry:Host@31206,registry:Host@31531,registry:Host@3157,registry:Host@31605,registry:Host@31852,registry:Host@32003,registry:Host@33199,registry:Host@33366,registry:Host@33473,registry:Host@33701,registry:Host@33844,registry:Host@34589,registry:Host@34715,registry:Host@34803,registry:Host@4469,registry:Host@4583,registry:Host@5546,registry:Host@5651,registry:Host@6870,registry:Host@6918,registry:Host@7035,registry:Host@7067,registry:Host@7945,registry:Host@8113,registry:Host@8239,registry:Host@8358,registry:Host@8836,registry:Host@9223,registry:Host@9449,registry:Host@9545,registry:Host@9636,registry:Kind@554,registry:Unit@31751,registry:Unit@579,whole:Route@10073,whole:Route@10931,whole:Route@11091,whole:Route@12115,whole:Route@13041,whole:Route@14601,whole:Route@15166,whole:Route@15527,whole:Route@16750,whole:Route@17053,whole:Route@17209,whole:Route@19539,whole:Route@19683,whole:Route@19782,whole:Route@2206,whole:Route@22321,whole:Route@22720,whole:Route@25258,whole:Route@30159,whole:Route@3139,whole:Route@33175,whole:Route@3595,whole:Route@4347,whole:Route@6753 a9672d71a3d70ab0bcc2b257daebb974da0f0bf67731303f51221b216f06eff6
+internal/release/adopt.go adoptHost authority B3,E1,E2,E3,E4,J1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. E22: root generation pins guard existing-tree artifact and manifest writes in adoption and moves-held staging/publication. Held snapshot is read under release exclusion before the leaf held-stage lock; failures remain restrictive for writes. Claims-off source returns no root pins. Fresh wholly absent version trees retain signed staging. Fixtures: TestPlan14GenerationSiblingWrites, TestPlan14GenerationVerifiedSiblingWrites, TestPlan14GenerationHeldStage (production source assembly, both flag values without census, lock order, pin/error/fresh cases). Production completeness remains unavailable; no new completeness grant.;rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0; legacy agent pass uid carlos;basis=applies root settings to canonically resolved routes including named units while legacy settings eligibility remains unchanged;rev3=PROPOSED-p3;run=16;basis=P3-R11 approved punctuation edit only; authority and reads unchanged registry:App@10936,registry:App@11096,registry:App@12125,registry:App@13051,registry:App@14630,registry:App@19789,registry:App@25265,registry:App@3144,registry:App@33024,registry:App@6758,registry:App@8059,registry:App@8253,registry:Hibernate@33885,registry:Host@10816,registry:Host@10849,registry:Host@10949,registry:Host@11109,registry:Host@11211,registry:Host@11243,registry:Host@11308,registry:Host@11758,registry:Host@11891,registry:Host@12338,registry:Host@12447,registry:Host@12649,registry:Host@13141,registry:Host@13927,registry:Host@14402,registry:Host@14616,registry:Host@14958,registry:Host@15548,registry:Host@19279,registry:Host@19387,registry:Host@19757,registry:Host@20577,registry:Host@20735,registry:Host@2105,registry:Host@21197,registry:Host@21249,registry:Host@21396,registry:Host@21483,registry:Host@22589,registry:Host@22833,registry:Host@2294,registry:Host@23493,registry:Host@2375,registry:Host@23878,registry:Host@24019,registry:Host@25233,registry:Host@25360,registry:Host@26702,registry:Host@27352,registry:Host@30585,registry:Host@31203,registry:Host@31528,registry:Host@3157,registry:Host@31602,registry:Host@31849,registry:Host@32000,registry:Host@33196,registry:Host@33363,registry:Host@33470,registry:Host@33698,registry:Host@33841,registry:Host@34586,registry:Host@34712,registry:Host@34800,registry:Host@4469,registry:Host@4583,registry:Host@5546,registry:Host@5651,registry:Host@6870,registry:Host@6918,registry:Host@7035,registry:Host@7067,registry:Host@7945,registry:Host@8113,registry:Host@8239,registry:Host@8358,registry:Host@8836,registry:Host@9223,registry:Host@9449,registry:Host@9545,registry:Host@9636,registry:Kind@554,registry:Unit@31748,registry:Unit@579,whole:Route@10073,whole:Route@10931,whole:Route@11091,whole:Route@12115,whole:Route@13041,whole:Route@14598,whole:Route@15163,whole:Route@15524,whole:Route@16747,whole:Route@17050,whole:Route@17206,whole:Route@19536,whole:Route@19680,whole:Route@19779,whole:Route@2206,whole:Route@22318,whole:Route@22717,whole:Route@25255,whole:Route@30156,whole:Route@3139,whole:Route@33172,whole:Route@3595,whole:Route@4347,whole:Route@6753 2b2c351c0ecc567e5eaf4146cf036ee073b47e2d6ae123198f4d970d0e4ccb96
internal/release/adopt.go settingsTarget authority E3 Settings target rejects foreign consumed decisions and forces root-owned settings rebind to the canonical tenant.;rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0; legacy agent pass uid carlos;basis=census branch delegates target selection to root inputs before any legacy consumed, good-version or build-state read registry:App@2977,registry:App@3077,registry:App@3362,registry:BuildState@2680,registry:GoodVersion@3387,registry:Host@1313,registry:Host@1723,registry:Host@2241,registry:Host@2488,registry:Host@2777,registry:Host@3373,registry:Host@3785,registry:Host@429,registry:Host@4440,registry:Host@4628,registry:Host@702,registry:UpdateSettings@556,whole:Route@1187,whole:Route@2277,whole:Route@2284,whole:Route@2842,whole:Route@2849,whole:Route@301,whole:Route@495,whole:Route@513,whole:Route@658 420982c2cbde642ae3008b61f93dc514defb527c05b20203864de8d052621599
internal/release/adopt.go type passConfig authority E2,E3 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.;rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0; legacy agent pass uid carlos;basis=stores the typed census-only root update provider and per-pass resolved input authority 7a405d8b9a24ccb4c9f09f91ca468f74a1e9b4a8b960b430dc3a6290a6b310cd
internal/release/rootgenerationpins.go RootGenerationPins.StagedPins authority E2 rev3=PROPOSED-p3;identity=root release retention and adoption uid 0;basis=checks the recorded boot before any legacy file identity comparison; cross-boot release pins require persistent executable and tree evidence;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures 0bcf3e831097fd3d1e2f3a58fd1e0707f98e04dc058fbea1f923b073c3691d50
@@ -235,7 +235,7 @@ internal/rollout/rootfailures.go RootFailures.ImportLegacy authority E3 rev3=PRO
internal/rollout/rootfailures.go RootFailures.VerifiedRetry authority E3 rev3=PROPOSED-p3;run=8;identity=root adoption uid 0;basis=verifies deployment-root signature, tuple, freshness and exact root book epoch before existing durable retry transition f4ab9edee069dedf8748055ee612a6ef8905b9770718c82378cfc15b0316810e
internal/rollout/rootfailures.go rootFailureWriter authority E3 rev3=PROPOSED-p3;run=8;identity=root adoption and migration uid 0;basis=writes migration identity, failure epochs and import fingerprints atomically through checked private root store e32ffb3459fe2b13e0ecbe7b364bd405ef25325ed8cbd685b9ac1e076a8c197a
@coverage internal/rollout 56efcb8c1d7231a7f43d0464d97345930c83c2baf864762c1faa7a004286e984
-internal/host/agent.go runAdoptUnderRootRelease authority B3,E1,E2,E3 rev3=PROPOSED-p3;run=13;identity=root adopt uid 0;basis=selects the complete legacy proof/history path without a census; census participation gates history creation, program recording, refusals and mint policy together whole:Route@11104,whole:Route@18293,whole:Route@18329,whole:Route@8406 a25835004cca39d1c624fc47e59b7bcac47eb175b55c1db583fac920eaa33068
+internal/host/agent.go runAdoptUnderRootRelease authority B3,E1,E2,E3 rev3=PROPOSED-p3;run=16;identity=root adopt/update/control uid 0;basis=no census selects legacy proof/history and update effects; proof observation cannot fetch signed recovery manifests on that path; participating adoption retains root release exclusion and independent generation pins whole:Route@11104,whole:Route@18293,whole:Route@18329,whole:Route@8406 a25835004cca39d1c624fc47e59b7bcac47eb175b55c1db583fac920eaa33068
internal/host/agent.go runAdoptWithEnsureSystemUnits authority B3,C,D,E1,E2,E3,E4,F,J1 PROPOSED E45 / R-E45-3: root checks census presence before opening release exclusion; absent census uses main adoption with no release directory or lock. Retained census remains protected with either flag value. PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending.;rev3=PROPOSED-p3;run=8;identity=carlos adopt uid 0;basis=passes the same census source to release exclusion and root settings to preserve the no-census branch;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=passes both the census probe and P3 production update opener under the common root release exclusion file:Open@176 13c055021f5e0453138bf773673521b86851eb16d567f6ccf968f5b7bc4f9199
internal/host/claimassembly.go NewProductionRootClaimStop authority C,E2 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=installs root filesystem identity capture in the production inventory handler;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures a55fcaa85155e2376d5f680ca8f86de6b579072e6a5c3ad47e1329b354311a00
internal/host/claimlaunch_root.go ClaimLaunchRoot.startRecorded authority E1,E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=persists the root failure intent before launch and independently revalidates readiness and the exact unit invocation before served/request completion;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=retains P3 runtime settlement and E host-aware readiness validation under the generation reservation and host guard file:Read@4103 b19cb431a122d33703a83b39cb9a19786b7f509eba22961ec1e86d95f618e909
@@ -244,15 +244,15 @@ internal/host/claimretire_transport.go type ClaimRootHTTP authority E1 rev3=PROP
internal/host/claimroleassembly_linux.go ClaimRoleAssembly.Launch authority C,J1 rev3=PROPOSED-p3;identity=carlos agent claim lifecycle uid carlos; final authorization remains in root;basis=checks retained state directory identity using persistent root evidence across boots before preparing the claimed launch;rev3=PROPOSED-p3;run=11;identity=carlos agent uid carlos; root validates final inventory and launch;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Command@5889,file:EvalSymlinks@5438,file:Fstat@3521,file:Lstat@2269,file:OpenFile@3111,file:OpenFile@3298,file:Read@711 ff3c65c83908a6803f53b0f61eedf9840eba50c109429bbb2c3620e9154bf689
internal/host/claimroleassembly_linux.go ClaimRoleAssembly.PrepareState authority C,J1 rev3=PROPOSED-p3;identity=carlos agent claim lifecycle uid carlos; final authorization remains in root;basis=reuses state directories using root-captured persistent identity when present; legacy local inventories retain the prior same-device comparison;rev3=PROPOSED-p3;run=11;identity=carlos agent uid carlos; root validates final inventory and launch;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Fstatat@2113,file:Fstatat@3196,file:Open@1597,file:Openat@1884 c610385f0932f8aa5e1b79ad34ad7b10f654e03016d8cae453ebe0aa25769f65
internal/host/claimstop_writers_linux.go installationCoverage authority C,G,J4 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=validates stopped-installation data roots against persistent filesystem identity before treating descriptors as covered;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures file:Lstat@820,file:ReadDir@1171 5b17c0581b7010785b8ee5f1b591c41863eebf19defde576ea7b8dc11ab429c6
-internal/host/proofbinding.go productionProofBindingSource authority B3 rev3=PROPOSED-p3;identity=root adoption and rollback proof binder uid 0;basis=enables persistent unit-file identity only for the census-required root proof path;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=uses the strict root-owned persistent identity capture for census-bound unit proofs; no-census hashes remain unchanged file:Open@209,file:Open@379,file:Read@1076 3b5fb803e7338161fe5d1550472904a925d5a7c2dcc419768823fdfbae8f74a0
-internal/host/proofbinding.go proofBindingSource.binding authority B3,J1 rev3=PROPOSED-p3;identity=root adoption and rollback proof binder uid 0;basis=compares retained rollback files by persistent evidence rather than old-boot device numbers file:EvalSymlinks@1220,file:EvalSymlinks@1529,file:EvalSymlinks@562,file:EvalSymlinks@801,registry:Unit@3205,whole:Route@265,whole:Route@3702,whole:Route@3709 4a47da1beac6c8d56047d2a928f05005cb2a63be02478ec93260a390b6162ab0
+internal/host/proofbinding.go productionProofBindingSource authority B3 rev3=PROPOSED-p3;run=16;identity=root adopt/update/control uid 0;basis=enables signed recovery lookups and persistent executable identity only after a successful participating census check; absent census keeps legacy observation store reads file:Open@209,file:Open@379,file:Read@1107 6312660ec9d139e25a408543b56372b428144ea6538add20963a6ccc10f91fbf
+internal/host/proofbinding.go proofBindingSource.binding authority B3,J1 rev3=PROPOSED-p3;run=16;identity=root adopt/update/control uid 0;basis=binds canonical unit, executable and healthy history; signed manifest recovery lookup runs only for participating sources, never absent-census observation file:EvalSymlinks@1220,file:EvalSymlinks@1529,file:EvalSymlinks@562,file:EvalSymlinks@801,registry:Unit@3225,whole:Route@265,whole:Route@3722,whole:Route@3729 20ed721cbcaccb6ab813a0d317542a43fe057544811920ba43ac6431c3e73000
internal/host/proofbinding.go proofBindingSource.unitBinding authority B3,J1,E1 rev3=PROPOSED-p3;identity=root adoption and rollback proof binder uid 0;basis=hashes persistent file identity into the canonical unit binding while preserving exact legacy source hashes without census registry:Unit@508,registry:Unit@609,registry:Unit@800,whole:Route@2878,whole:Route@2885 82e214d1cf66d29d0900f5c1ce9cfe0a2f31c070da4bf1497ef3fd5273c09aad
internal/host/prooffile.go proofFileSource authority B3 rev3=PROPOSED-p3;identity=root adoption and rollback proof binder uid 0;basis=excludes only boot-local numbers from persistent proof hashes; filesystem, handle, ownership and mount attachment remain bound;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=keeps filesystem handle, inode, ownership and mount attachment in canonical proof while excluding boot, device, mount number and mount namespace from cross-boot hashes f3ce69cdd8a0fba648dcf173904ba278ef4856a00280fa0991846743b68ec28f
internal/host/prooffile.go sameProofFiles authority B3 rev3=PROPOSED-p3;identity=root adoption and rollback proof binder uid 0;basis=compares retained and current proof files using persistent evidence, refusing an implicit upgrade of old unproven files e531e1eac189f957d9cc0151319093e0b2ab0fcd80412abc0d2f3fefcfc3421a
internal/host/proofnamed.go proofBindingSource.record authority B3 rev3=PROPOSED-p3;identity=root adoption and rollback proof binder uid 0;basis=binds named-unit provenance to boot-stable file evidence and preserves the original no-census hash shape 97f1501b9543e36c6a231170425c5c79c7c22489500aadf119e064abc1abde93
internal/host/proofnamed.go proofBindingSource.trustedBytes authority B3,J1 rev3=PROPOSED-p3;identity=root adoption and rollback proof binder uid 0;basis=captures persistent evidence from the checked root-owned unit file descriptor alongside its bytes file:ReadAll@422 aa6e98efc7a57b01ed38cd051642d8d2f0b6abf2d527d5f70533e7365746096e
internal/host/rootgeneration_kernel_linux.go generationGroupGone authority E2 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=refuses a present cgroup placement from another boot; an absent old placement carries no process identity to compare;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=requires a known boot before interpreting a present cgroup; same-boot exact scan and E reboot population scan use separate authority rules;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=requires a known boot before interpreting a present cgroup; same-boot exact scan and E reboot population scan use separate authority rules file:Lstat@194 1ed5a4aadf5334f37027e04b258d7fb4a6413356d2d21257ae5ef9c626655376
-internal/host/rootgeneration_parked.go generationRetirements authority E2 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=carries root-captured persistent artifact identity through generation retirement inventory;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures bae5991fb380bc83c9cdf49e1dd2e388a1c07aa66a989c59019221f4b6cc3bbf
+internal/host/rootgeneration_parked.go generationRetirements 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 e7fb5fc50073e49b5e8f1c0d9a09e9e3cc8da8f96902229d2af4d2c7df8fb20e
internal/host/rootgeneration_payload_linux.go generationPayloadFiles authority E2 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=verifies materialized payload entries by persistent filesystem evidence before admitting their files into the absence scan;rev3=PROPOSED-p3;run=11;identity=root claim, migration or adoption controller uid 0;basis=merges E FileIdentity callers and P3 authority effects; retains the recorded family checks and both tracks behavioral fixtures 2c899f743f9953ce2c8d9223d15f00914e9a09c7675226260d1c4d4c1fe1fd5d
internal/host/rootgenerations_launch.go rootGenerationSource.reserve authority E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=writes root runtime intent after the generation reservation and before launch under release and host exclusion b09d7390279bcb302d83c282a517680f43c44013e07fc490f37bc798fd1703ba
internal/host/rootgenerations_launch.go type generationLaunchPermit authority E2,E3 rev3=PROPOSED-p3;identity=root claim/adoption controller uid 0;basis=retains the root runtime completion and close callbacks for the reserved launch 964b8f1ea86c1a85f8a60d5d87168d71375e9c835e2c64859267dc822265c135
@@ -329,7 +329,18 @@ internal/host/rootmovesfloor_boot.go sameRootFloorImage authority F rev3=PROPOSE
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/claimassembly.go ClaimAssembly.Build authority C,G rev3=PROPOSED-p3;run=15;identity=agent uid carlos; root control/adopt/recovery uid 0;basis=supplies root registration client, guard, enrollment and typed inventory; local root obligations and exact intention bind inventory revision; service SQL contributes refusals only file:Prepare@3854,file:Prepare@3871,file:Prepare@4583,file:Read@4513,whole:Timer@2044 b77068fb6ba255c31031e3328b6a7ae976369cd5098c51602b0c235effbed253
-internal/host/rootgeneration_inventory.go readProductionGenerationInventory authority E2 rev3=PROPOSED-p3;run=15;identity=root generation/update/recovery uid 0;basis=local=true reads retained census, fences, obligations and journals without deployment store; local=false opens deployment store for canonical records; SQL remains refusal-only 0a9180acd9e78efc5905fe37df25d877c7343fcec037786f699b03182acaedc3
+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
internal/host/systemparticipation.go systemApplier.checkUnarmedParticipationRecovery authority G,F rev3=PROPOSED-p3;run=15;identity=root system update/recovery uid 0;basis=pending acquisition triggers production controller settlement even without an update journal; normal ledger admission stays refused until verified recovery clears the marker 4360d1c0c50c554afc06b3a9ad216acc646728f97750e00ca1fe038b1bccce51
internal/hostguard/generationinventory.go RootAuthority.generationInstallation authority E2 rev3=PROPOSED-p3;run=15;identity=root generation verification uid 0;basis=enumerates using preserved installation stamp including exact retired tuples; resyncs retained authority without admitting inactive installations or dropping payload obligations fc5ef57b638ee0eba8ae49b724ef9397b2a3ae097d1e892cab888880480eacd2
+internal/host/rootgeneration_inventory.go generationReconciler.reconcile 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 23995980eb4aa9c02618ce649b583344fc80ee1ee193ff84fb9a46f49d33ce68
+internal/host/rootgeneration_requirements.go generationRequiredInventory authority E2 rev3=PROPOSED-p3;run=16;identity=root adopt/update/control uid 0;basis=active installations require canonical deployment records; retired exact root tuples use retained artifacts and verified completed retirement journals without requiring deleted remote records; local obligations and pins remain 3c0eb88abc5680c67191d1e848e93ba528c66e81bcd521577098ce761fb827e0
+internal/host/rootgeneration_retirement.go generationRetirement.closure authority E2 rev3=PROPOSED-p3;run=16;identity=root adopt/update/control uid 0;basis=root installation and exact controller identity bind retirement; pre-intention cancellation requires no process and irreversible writer closure, followed by the existing independent kernel verifier and durable close/retirement writes 1035a55fba521c7c81631d14084684e2dd4cb94703a7604d982be880bcea0cdf
+internal/host/rootgeneration_inventory.go type generationInventory 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 42a3affbf3d0da492f0326545305a3d996841cd36c0e3f4373d057abc2e7374f
+internal/host/rootgeneration_parked.go generationPhysicalPayloads 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 6ea8bab62e2bd9fc33e64193a47149e809423967162604f824b4a6b18bd573f1
+internal/host/rootgeneration_parked.go generationRetirementRecords 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 file:Read@728 65cc967484b693f71e4f78c418b85dcef801e82d0eed1ffd2cdda21c8ba163ef
+internal/host/rootgeneration_rows.go generationRegistryInventory 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 registry:ClaimOwnership@2394,registry:ClaimOwnership@2773,registry:ClaimRowID@2826,registry:Host@2372,registry:Host@2751,registry:Name@2692,registry:Registration@2804,whole:ClaimOwnership@1625,whole:ClaimOwnership@1734,whole:ClaimOwnership@1748,whole:ClaimOwnership@1762,whole:ClaimOwnership@2271,whole:ClaimOwnership@2400,whole:ClaimOwnership@2418,whole:ClaimOwnership@2473,whole:ClaimOwnership@2722,whole:ClaimOwnership@2779,whole:ClaimOwnership@2797,whole:ClaimOwnership@2892,whole:ClaimOwnership@2920,whole:Route@2289,whole:Timer@2578,whole:Timer@2656 4d4e526c596c7a7fe36a071ca2452acc63f4941b225d389971fb718f4c39dd33
+internal/host/rootgeneration_handover.go generationReadHandovers 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 file:Read@453 226a4fbdb227eed6993a4d94c380e8aef8f681c6ad4b56abed2a30acee10e12a
+internal/host/rootgeneration_handover.go generationTransfers 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 registry:ClaimOwner@2754,registry:ClaimOwner@2802,registry:ClaimRowID@2936,registry:ClaimRowID@2986,registry:ClaimRowID@3574,registry:ClaimRowID@3615,registry:Name@3541,registry:Route@2754,registry:Route@2802,registry:Route@2936,registry:Route@2986,registry:Timers@3483,registry:Timers@3615,whole:ColdRows@2756,whole:ColdRows@2804,whole:ColdRows@2938,whole:ColdRows@2988,whole:ColdRows@3485,whole:ColdRows@3617,whole:Route@2763,whole:Route@2810,whole:Route@2945,whole:Route@2994,whole:Timer@3492,whole:Timer@3623 c0c0d2d05d6945560ee7170e6953ad3e603b5054b8f256ee49d93e36a1ad1fe9
+internal/host/rootgeneration_handover.go productionGenerationHandovers 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 file:Open@172,file:Open@422 a5f5ce3ccf3f0a068b049e29bc804ced7c942d25080038942041f399e86be743
+internal/hostbox/generationhandover.go DecodeGenerationHandover 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 registry:ClaimOwnership@1383,registry:ClaimOwnership@1588,registry:ClaimOwnership@1621,registry:ClaimRowID@1650,registry:Name@1704,registry:Route@1383,registry:Route@1621,registry:Timers@1255,registry:Timers@1300,registry:Timers@1571,registry:Timers@1588,registry:Timers@1650,registry:Timers@1704,whole:ClaimOwnership@1395,whole:ClaimOwnership@1604,whole:ClaimOwnership@1633,whole:ColdRows@1247,whole:ColdRows@1302,whole:ColdRows@1752,whole:ColdRows@1761,whole:Route@1389,whole:Route@1627,whole:Timer@1261,whole:Timer@1309,whole:Timer@1577,whole:Timer@1594,whole:Timer@1656,whole:Timer@1710 3992c3f45d4688db1d1ad88106f535ed3d510046fbd93aa80eccf6b82fd1063c
diff --git a/internal/release/adopt.go b/internal/release/adopt.go
index 6aa7eda01..da914a81c 100644
--- a/internal/release/adopt.go
+++ b/internal/release/adopt.go
@@ -1574,7 +1574,7 @@ func adoptHost(ctx context.Context, work hostwrite.Work, reg *registry.Registry,
programChange = programChange && ptr.DigestOf(ver) != ""
}
if (len(hist) > 0 || cfg.requireRootHistory && !fresh) && !slices.Contains(hist, newProg) && !programChange {
- fmt.Printf(" %s: %s is %s, but this route has only run %s — not pointed live (ship a new version with carlos ship --program-change and promote it, if the change is intended)\n",
+ fmt.Printf(" %s: %s is %s, but this route has only run %s. not pointed live (ship a new version with carlos ship --program-change and promote it, if the change is intended)\n",
rt.Host, ver, newProg, strings.Join(hist, ", "))
sha := m.Artifacts[adoptPlatform].SHA256
// Nothing was restarted, so the box is still on its old build.