carlos / platform Public

WIP files seed checkpoint 4 (not for merge): root generation authorization and successor recovery

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev c4f25c7e2d47d7f38964668d390cc8b3df465ac0 parent 95783ae
6 files changed, +1058 −27
  • internal/host/claimservingpark_records.go +132 −15
  • internal/host/claimservingparkroot_linux.go +286 −0
  • internal/host/claimservingparkroot_linux_test.go +551 −0
  • internal/host/claimservingparksource_linux.go +1 −1
  • internal/hostguard/servingretirement.go +67 −0
  • internal/provision/testdata/plan14-root-reads-seed.tsv +21 −11

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	360.940s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.119s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.197s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.710s
ok  	github.com/carlosframework/platform/internal/activator	235.487s
ok  	github.com/carlosframework/platform/internal/agentsock	0.015s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.339s
ok  	github.com/carlosframework/platform/internal/apperr	0.073s
ok  	github.com/carlosframework/platform/internal/applog	0.132s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.503s
?   	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.011s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.702s
ok  	github.com/carlosframework/platform/internal/changelog	0.300s
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	368.958s
?   	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	5.788s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.005s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.002s
ok  	github.com/carlosframework/platform/internal/docsite	0.004s
ok  	github.com/carlosframework/platform/internal/domains	2.106s
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.237s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.007s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.124s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.017s
ok  	github.com/carlosframework/platform/internal/geoip	0.029s
ok  	github.com/carlosframework/platform/internal/host	129.215s
ok  	github.com/carlosframework/platform/internal/hostactors	0.577s
ok  	github.com/carlosframework/platform/internal/hostbox	2.207s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.510s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.005s
ok  	github.com/carlosframework/platform/internal/hostexec	4.844s
ok  	github.com/carlosframework/platform/internal/hostfence	0.029s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.503s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.013s
ok  	github.com/carlosframework/platform/internal/hostguard	1.011s
ok  	github.com/carlosframework/platform/internal/hostops	1.476s
?   	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.467s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.137s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.003s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	1.815s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.705s
ok  	github.com/carlosframework/platform/internal/instancefs	0.005s
ok  	github.com/carlosframework/platform/internal/instancekey	0.016s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.006s
ok  	github.com/carlosframework/platform/internal/layout	1.447s
ok  	github.com/carlosframework/platform/internal/lease	9.452s
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.167s
ok  	github.com/carlosframework/platform/internal/maintenance	0.087s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.508s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.073s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	20.995s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.760s
ok  	github.com/carlosframework/platform/internal/objstore	0.170s
ok  	github.com/carlosframework/platform/internal/ops	0.168s
ok  	github.com/carlosframework/platform/internal/park	0.212s
ok  	github.com/carlosframework/platform/internal/privatefile	0.005s
ok  	github.com/carlosframework/platform/internal/provision	12.778s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.009s
ok  	github.com/carlosframework/platform/internal/relay	7.238s
ok  	github.com/carlosframework/platform/internal/release	19.927s
ok  	github.com/carlosframework/platform/internal/rollout	3.694s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.009s
ok  	github.com/carlosframework/platform/internal/rootstate	0.025s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.281s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.590s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.225s
ok  	github.com/carlosframework/platform/internal/status	0.040s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.238s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.005s
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.001s
ok  	github.com/carlosframework/platform/internal/vet	1.560s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.024s
=== RUN   TestT4StorageProbeMinio
2026/10/14 07:29:15 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE54D0425B0CD9, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.021s
minio contract: PASS

Intent

Human

<task-notification> <task-id>b54plz3ih</task-id> <tool-use-id>toolu_01JAj6xdmGr9oZbQenFFDbuk</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-carlos-platform/6387394d-0b76-4de7-bbb4-d725fe24b08c/tasks/b54plz3ih.output</output-file> <status>completed</status> <summary>Background command "Wait for seed build run 4" completed (exit code 0)</summary> </task-notification>

diff --git a/internal/host/claimservingpark_records.go b/internal/host/claimservingpark_records.go
index dd09c056a..aa451e1c5 100644
--- a/internal/host/claimservingpark_records.go
+++ b/internal/host/claimservingpark_records.go
@@ -41,6 +41,7 @@ type servingParkSource struct {
}
type servingParkRecord struct {
+ Continuations []servingParkContinuation `json:",omitempty"`
Version int
Authorization servingParkAuthorization
Source servingParkSource
@@ -52,14 +53,26 @@ type servingParkRecord struct {
Cause string
}
+type servingParkContinuation struct {
+ Authorization servingParkAuthorization
+ Receipts []hostguard.FenceReceipt
+}
+
+func (r servingParkRecord) activeAuthorization() servingParkAuthorization {
+ if len(r.Continuations) != 0 {
+ return r.Continuations[len(r.Continuations)-1].Authorization
+ }
+ return r.Authorization
+}
+
type servingLegacyConsumption struct {
Authorization servingParkAuthorization
Source servingParkSource
}
type servingParkSlot struct {
- Version int
- Box, Host, ID, Pending string
+ Version int
+ Box, Host, ID, Pending, Last string
}
type servingParkFilesystem interface {
@@ -200,6 +213,29 @@ func validateServingParkRecord(r servingParkRecord) error {
if r.Complete && r.Disposition != servingParkRetain && !r.Closed {
return hostbox.ErrChanged
}
+ previous := a
+ if len(r.Continuations) > 32 {
+ return hostbox.ErrChanged
+ }
+ for _, continuation := range r.Continuations {
+ next := continuation.Authorization
+ want := previous
+ want.Successor = next.Successor
+ want.TagsFrom = next.TagsFrom
+ identity := previous.Successor
+ identity.Stamp, identity.Session, identity.Generation = next.Successor.Stamp, next.Successor.Session, next.Successor.Generation
+ if next != want || next.Successor != identity || identity.Stamp <= previous.Successor.Stamp || identity.Session != identity.Stamp || identity.Generation == "" || identity.Generation == previous.Successor.Generation || (next.TagsFrom != previous.TagsFrom && next.TagsFrom != previous.Successor.Stamp && next.TagsFrom != next.Successor.Stamp) || len(continuation.Receipts) == 0 || !r.Parked {
+ return hostbox.ErrChanged
+ }
+ for _, receipt := range continuation.Receipts {
+ t := receipt.Target
+ p := previous.Successor
+ if t.Host != p.Host || t.Box != p.Box || t.Region != p.Region || t.Incarnation != p.Incarnation || t.Stamp != p.Stamp || t.Session != p.Session || t.Op != "" || t.Phase != "" || t.ObligationID != "" || verifyRetirementReceipt(context.Background(), receipt) != nil {
+ return hostbox.ErrChanged
+ }
+ }
+ previous = next
+ }
return nil
}
@@ -235,6 +271,12 @@ func (l *servingParkLedger) slot(a servingParkAuthorization) (servingParkSlot, e
return s, hostbox.ErrChanged
}
}
+ if saved.Last != "" {
+ r, err := l.read(saved.Last)
+ if err != nil || r.Authorization.Successor.Box != s.Box || r.Authorization.Successor.Host != s.Host {
+ return s, hostbox.ErrChanged
+ }
+ }
return saved, nil
}
@@ -275,6 +317,11 @@ func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthori
if slot.Pending != "" && slot.Pending != a.ID {
return zero, hostbox.ErrChanged
}
+ unlockRecord, err := l.state.Lock(ctx, "retirement-"+a.ID+".lock")
+ if err != nil {
+ return zero, err
+ }
+ defer unlockRecord()
r, err := l.read(a.ID)
if errors.Is(err, os.ErrNotExist) {
source, e := l.fs.Capture(a)
@@ -330,6 +377,7 @@ func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthori
}
if slot.Pending == "" {
slot.Pending = a.ID
+ slot.Last = a.ID
if err = l.write("slot-"+servingParkKey(slot.Box, slot.Host)+".json", slot); err != nil {
return zero, err
}
@@ -353,18 +401,8 @@ func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthori
return zero, hostbox.ErrChanged
}
if slot.ID != "" {
- old, e := l.read(slot.ID)
- if e != nil || !old.Complete {
- return zero, hostbox.ErrChanged
- }
- if !old.Closed {
- cause := "replacement"
- if old.Deleting {
- cause = old.Cause
- }
- if e = l.deleteLocked(ctx, &old, cause); e != nil {
- return zero, e
- }
+ if e = l.replaceLocked(ctx, slot.ID); e != nil {
+ return zero, e
}
}
slot.ID = a.ID
@@ -404,13 +442,23 @@ func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthori
// completeLocked follows a fresh root check of the successor's paired tags.
// Its caller holds namespace and host authority, never an agent Work lock.
func (l *servingParkLedger) completeLocked(ctx context.Context, id string, a servingArtifactIdentity) error {
+ unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(a.Box, a.Host)+".lock")
+ if err != nil {
+ return err
+ }
+ defer unlock()
r, err := l.read(id)
if err != nil {
return err
}
- if r.Authorization.Successor != a || !r.Parked {
+ if r.activeAuthorization().Successor != a || !r.Parked {
return hostbox.ErrChanged
}
+ unlockRecord, err := l.state.Lock(ctx, "retirement-"+id+".lock")
+ if err != nil {
+ return err
+ }
+ defer unlockRecord()
slot, err := l.slot(r.Authorization)
if err != nil {
return err
@@ -444,6 +492,45 @@ func (l *servingParkLedger) completeLocked(ctx context.Context, id string, a ser
return nil
}
+// continueLocked records permission from a later stopped successor without
+// changing the original retirement, source identities or retention deadline.
+// Root has already rechecked the original and every continuation stop.
+func (l *servingParkLedger) continueLocked(ctx context.Context, id string, a servingParkAuthorization, receipts []hostguard.FenceReceipt) (servingParkRecord, error) {
+ var zero servingParkRecord
+ unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(a.Successor.Box, a.Successor.Host)+".lock")
+ if err != nil {
+ return zero, err
+ }
+ defer unlock()
+ unlockRecord, err := l.state.Lock(ctx, "retirement-"+id+".lock")
+ if err != nil {
+ return zero, err
+ }
+ defer unlockRecord()
+ r, err := l.read(id)
+ if err != nil {
+ return zero, err
+ }
+ if r.activeAuthorization() == a {
+ return r, nil
+ }
+ slot, err := l.slot(a)
+ if err != nil || slot.Pending != id || r.Complete {
+ return zero, hostbox.ErrChanged
+ }
+ if err = l.fs.CheckParked(&r); err != nil {
+ return zero, err
+ }
+ r.Continuations = append(r.Continuations, servingParkContinuation{Authorization: a, Receipts: receipts})
+ if err = ctx.Err(); err != nil {
+ return zero, err
+ }
+ if err = l.save(r); err != nil {
+ return zero, err
+ }
+ return r, nil
+}
+
func (l *servingParkLedger) deleteLocked(ctx context.Context, r *servingParkRecord, cause string) error {
if r.Closed {
return nil
@@ -481,6 +568,16 @@ func (l *servingParkLedger) reapLocked(ctx context.Context, id string) error {
if err != nil {
return err
}
+ unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(r.Authorization.Successor.Box, r.Authorization.Successor.Host)+".lock")
+ if err != nil {
+ return err
+ }
+ defer unlock()
+ unlockRecord, err := l.state.Lock(ctx, "retirement-"+id+".lock")
+ if err != nil {
+ return err
+ }
+ defer unlockRecord()
if r.Closed {
return nil
}
@@ -489,3 +586,23 @@ func (l *servingParkLedger) reapLocked(ctx context.Context, id string) error {
}
return l.deleteLocked(ctx, &r, "expiry")
}
+
+func (l *servingParkLedger) replaceLocked(ctx context.Context, id string) error {
+ unlock, err := l.state.Lock(ctx, "retirement-"+id+".lock")
+ if err != nil {
+ return err
+ }
+ defer unlock()
+ old, err := l.read(id)
+ if err != nil || !old.Complete {
+ return hostbox.ErrChanged
+ }
+ if old.Closed {
+ return nil
+ }
+ cause := "replacement"
+ if old.Deleting {
+ cause = old.Cause
+ }
+ return l.deleteLocked(ctx, &old, cause)
+}
diff --git a/internal/host/claimservingparkroot_linux.go b/internal/host/claimservingparkroot_linux.go
new file mode 100644
index 000000000..35dd848a5
--- /dev/null
+++ b/internal/host/claimservingparkroot_linux.go
@@ -0,0 +1,286 @@
+//go:build linux
+
+package host
+
+import (
+ "bytes"
+ "context"
+ "net"
+ "path/filepath"
+ "sort"
+ "strconv"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/provision"
+)
+
+// servingParkRoot composes local root authority with the filesystem ledger.
+// Production construction remains disabled until legacy authorization, journal
+// transport and startup recovery are implemented. Requests supply identities
+// only; installation paths and receipts are read under root's host lock.
+type servingParkRoot struct {
+ root ClaimRootHTTP
+ ledger *servingParkLedger
+}
+
+func (p *servingParkRoot) withAuthority(ctx context.Context, conn net.Conn, current servingArtifactIdentity, fn func(hostclaim.Observed, hostguard.FenceView, hostbox.Actor, servingArtifactDirectory, uint64) error) error {
+ if ctx == nil || conn == nil || p == nil || p.ledger == nil || p.root.Stop.Authenticate == nil || p.root.RequireNewWork == nil || p.root.InventoryActor == nil || p.root.Journals == nil || p.root.Journals.Inventory == nil || p.root.Journals.Inventory.Remote() || p.root.Stop.Coordinator.Guard.Root == nil || fn == nil {
+ return hostbox.ErrUnsupported
+ }
+ data := p.root.Roots["data"]
+ if !filepath.IsAbs(data) || data != p.root.Stop.Coordinator.Guard.DataDir {
+ return hostbox.ErrChanged
+ }
+ return p.ledger.withNamespace(ctx, func() error {
+ return p.root.Stop.Coordinator.Guard.WithServingRetirement(ctx, current.Host, hostguard.Captured{Incarnation: current.Incarnation, Stamp: current.Stamp}, func(o hostclaim.Observed, view hostguard.FenceView) error {
+ // Reauthenticate after waiting for locks: an earlier connection check
+ // cannot establish that the edge main process is still this caller.
+ peer, err := p.root.Stop.Authenticate(ctx, conn)
+ if err != nil {
+ return err
+ }
+ if peer.UID == 0 || peer.PID <= 0 || peer.ProcessStart == "" || peer.BootID == "" || peer.ControllerID == "" {
+ return ErrClaimStopIdentity
+ }
+ if err = p.root.RequireNewWork(ctx); err != nil {
+ return err
+ }
+ actor, err := p.root.InventoryActor(ctx)
+ if err != nil {
+ return err
+ }
+ if !actor.Eligible || actor.Quarantined || actor.Enrollment == "" || actor.Revision == 0 {
+ return hostbox.ErrChanged
+ }
+ actual, err := servingWriterIdentity(o, actor.Box, actor.Region)
+ if err != nil || actual != current || o.ETag == "" {
+ return hostbox.ErrChanged
+ }
+ in, revision, err := p.root.Journals.Inventory.Snapshot(ctx, current.Host, view.Captured)
+ if err != nil {
+ return err
+ }
+ dir, err := servingParkStateDirectory(data, o, view.Installation, in, revision, actor)
+ if err != nil {
+ return err
+ }
+ db, err := filepath.Rel(data, provision.NestedDBPath(data, current.Host))
+ if err != nil {
+ return err
+ }
+ tags, err := servingInventoryPair(o.Claim.Inventory[actor.Box], filepath.ToSlash(db))
+ if err != nil || tags > current.Stamp {
+ return hostbox.ErrChanged
+ }
+ if err = ctx.Err(); err != nil {
+ return err
+ }
+ fresh, err := p.root.Stop.Coordinator.Guard.Claims.Read(ctx, current.Host)
+ if err != nil {
+ return err
+ }
+ if fresh.ETag != o.ETag || fresh.Absent {
+ return hostbox.ErrChanged
+ }
+ return fn(o, view, actor, dir, tags)
+ })
+ })
+}
+
+func servingRootStops(ctx context.Context, view hostguard.FenceView, prior servingArtifactIdentity, actor hostbox.Actor) ([]hostguard.FenceReceipt, error) {
+ if prior.Host != view.Host || prior.Incarnation != view.Installation.Incarnation || prior.Box != actor.Box || prior.Region != actor.Region || prior.Stamp == 0 || prior.Session != prior.Stamp {
+ return nil, hostbox.ErrChanged
+ }
+ known := view.Captured == (hostguard.Captured{Incarnation: prior.Incarnation, Stamp: prior.Stamp})
+ for _, retired := range view.Retired {
+ known = known || retired == (hostguard.Captured{Incarnation: prior.Incarnation, Stamp: prior.Stamp})
+ }
+ if !known {
+ return nil, hostguard.ErrNoFenceReceipt
+ }
+ var out []hostguard.FenceReceipt
+ seen := make(map[string]bool)
+ for _, receipt := range view.Receipts {
+ t := receipt.Target
+ if t.Host != prior.Host || t.Incarnation != prior.Incarnation || t.Stamp != prior.Stamp || t.Box != prior.Box || t.Op != "" || t.Phase != "" || t.ObligationID != "" {
+ continue
+ }
+ if t.Session != prior.Session || t.Region != prior.Region || t.Enrollment != actor.Enrollment || t.DirectoryRevision != strconv.FormatUint(actor.Revision, 10) || t.FenceID == "" || seen[t.FenceID] || verifyRetirementReceipt(ctx, receipt) != nil {
+ return nil, ErrClaimStopReceipt
+ }
+ seen[t.FenceID] = true
+ out = append(out, receipt)
+ }
+ if len(out) == 0 {
+ return nil, hostguard.ErrNoFenceReceipt
+ }
+ sort.Slice(out, func(i, j int) bool { return out[i].Target.FenceID < out[j].Target.FenceID })
+ return out, nil
+}
+
+func sameServingRootStops(a, b []hostguard.FenceReceipt) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := range a {
+ if a[i].Target != b[i].Target || !a[i].EvidenceAt.Equal(b[i].EvidenceAt) || !bytes.Equal(a[i].Bytes, b[i].Bytes) {
+ return false
+ }
+ }
+ return true
+}
+
+func (p *servingParkRoot) parkServingDatabase(ctx context.Context, conn net.Conn, current, requestedPrior servingArtifactIdentity) (servingParkAuthorization, error) {
+ var out servingParkAuthorization
+ err := p.withAuthority(ctx, conn, current, func(o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, dir servingArtifactDirectory, tags uint64) error {
+ var a servingParkAuthorization
+ var saved *servingParkRecord
+ var continuation []hostguard.FenceReceipt
+ var next servingParkAuthorization
+ slot, err := p.ledger.slot(servingParkAuthorization{Successor: current})
+ if err != nil {
+ return err
+ }
+ id := slot.Pending
+ if id == "" && tags == current.Stamp {
+ id = slot.Last
+ }
+ if id != "" {
+ r, err := p.ledger.read(id)
+ if err != nil {
+ return err
+ }
+ a, saved = r.Authorization, &r
+ if a.Legacy || a.Directory != dir {
+ return hostbox.ErrChanged
+ }
+ active := r.activeAuthorization()
+ if err = validateServingRootHistory(ctx, o, view, actor, r); err != nil {
+ return err
+ }
+ if requestedPrior != (servingArtifactIdentity{}) {
+ prior, e := servingPredecessor(o.Claim, current, tags, requestedPrior.Generation)
+ if e != nil || prior != requestedPrior {
+ return hostbox.ErrChanged
+ }
+ }
+ if tags != active.TagsFrom && tags != active.Successor.Stamp && tags != current.Stamp {
+ return hostbox.ErrChanged
+ }
+ if active.Successor != current {
+ if r.Complete || active.Successor.Stamp >= current.Stamp {
+ return hostbox.ErrChanged
+ }
+ prior, e := servingPredecessor(o.Claim, current, active.Successor.Stamp, active.Successor.Generation)
+ if e != nil || prior != active.Successor {
+ return hostbox.ErrChanged
+ }
+ continuation, err = servingRootStops(ctx, view, prior, actor)
+ if err != nil {
+ return err
+ }
+ next = active
+ next.Successor, next.TagsFrom = current, tags
+ }
+ } else {
+ if requestedPrior == (servingArtifactIdentity{}) {
+ return hostbox.ErrChanged
+ }
+ prior, err := servingPredecessor(o.Claim, current, tags, requestedPrior.Generation)
+ if err != nil || prior != requestedPrior {
+ return hostbox.ErrChanged
+ }
+ a = servingParkAuthorization{TagsFrom: tags, Predecessor: prior, OriginalSuccessor: current, Successor: current, Directory: dir}
+ a.ID = servingRetirementID(prior, current, false)
+ }
+ prior, err := servingPredecessor(o.Claim, current, a.Predecessor.Stamp, a.Predecessor.Generation)
+ if err != nil || prior != a.Predecessor {
+ return hostbox.ErrChanged
+ }
+ receipts, err := servingRootStops(ctx, view, prior, actor)
+ if err != nil {
+ return err
+ }
+ if saved != nil && !sameServingRootStops(saved.Receipts, receipts) {
+ return ErrClaimStopReceipt
+ }
+ r, err := p.ledger.parkLocked(ctx, a, receipts)
+ if err != nil {
+ return err
+ }
+ if err = p.ledger.fs.CheckParked(&r); err != nil {
+ return err
+ }
+ if next.ID != "" {
+ r, err = p.ledger.continueLocked(ctx, a.ID, next, continuation)
+ if err != nil {
+ return err
+ }
+ }
+ out = r.activeAuthorization()
+ return nil
+ })
+ return out, err
+}
+
+func (p *servingParkRoot) completeServingDatabasePark(ctx context.Context, conn net.Conn, id string, current servingArtifactIdentity) error {
+ return p.withAuthority(ctx, conn, current, func(o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, dir servingArtifactDirectory, tags uint64) error {
+ r, err := p.ledger.read(id)
+ if err != nil {
+ return err
+ }
+ a := r.activeAuthorization()
+ if err = validateServingRootHistory(ctx, o, view, actor, r); err != nil {
+ return err
+ }
+ if a.Legacy || a.Successor != current || a.Directory != dir || tags != current.Stamp {
+ return hostbox.ErrChanged
+ }
+ prior, err := servingPredecessor(o.Claim, current, a.Predecessor.Stamp, a.Predecessor.Generation)
+ if err != nil || prior != a.Predecessor {
+ return hostbox.ErrChanged
+ }
+ receipts, err := servingRootStops(ctx, view, prior, actor)
+ if err != nil {
+ return err
+ }
+ if !sameServingRootStops(r.Receipts, receipts) {
+ return ErrClaimStopReceipt
+ }
+ return p.ledger.completeLocked(ctx, id, current)
+ })
+}
+
+// Every recorded successor must still be a retained local admission; each
+// transition carries the root stop of the successor it superseded.
+func validateServingRootHistory(ctx context.Context, o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, r servingParkRecord) error {
+ current, err := servingWriterIdentity(o, actor.Box, actor.Region)
+ if err != nil {
+ return err
+ }
+ previous := r.Authorization.Successor
+ for _, continuation := range r.Continuations {
+ prior, err := servingPredecessor(o.Claim, current, previous.Stamp, previous.Generation)
+ if err != nil || prior != previous {
+ return hostbox.ErrChanged
+ }
+ receipts, err := servingRootStops(ctx, view, previous, actor)
+ if err != nil {
+ return err
+ }
+ if !sameServingRootStops(receipts, continuation.Receipts) {
+ return ErrClaimStopReceipt
+ }
+ previous = continuation.Authorization.Successor
+ }
+ if previous == current {
+ return nil
+ }
+ prior, err := servingPredecessor(o.Claim, current, previous.Stamp, previous.Generation)
+ if err != nil || prior != previous {
+ return hostbox.ErrChanged
+ }
+ return nil
+}
diff --git a/internal/host/claimservingparkroot_linux_test.go b/internal/host/claimservingparkroot_linux_test.go
new file mode 100644
index 000000000..1ac5b5b0c
--- /dev/null
+++ b/internal/host/claimservingparkroot_linux_test.go
@@ -0,0 +1,551 @@
+//go:build linux
+
+package host
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "sync"
+ "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/hostwrite"
+ "github.com/carlosframework/platform/internal/provision"
+ "golang.org/x/sys/unix"
+)
+
+// Disk persistence and lock exclusion are real. Root ownership, mount policy,
+// roster and authenticated process observations remain explicit test seams.
+type servingAuthorityDisk struct {
+ *rollbackDisk
+ mu sync.Mutex
+ locks map[string]chan struct{}
+ trace *[]string
+ prefix string
+}
+
+func (s *servingAuthorityDisk) Lock(ctx context.Context, name string) (func(), error) {
+ s.mu.Lock()
+ if s.locks == nil {
+ s.locks = make(map[string]chan struct{})
+ }
+ lock := s.locks[name]
+ if lock == nil {
+ lock = make(chan struct{}, 1)
+ s.locks[name] = lock
+ }
+ s.mu.Unlock()
+ select {
+ case lock <- struct{}{}:
+ if s.trace != nil {
+ *s.trace = append(*s.trace, s.prefix+name)
+ }
+ return func() { <-lock }, nil
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
+}
+
+type servingRootFixture struct {
+ t *testing.T
+ f *recoveryFixture
+ p *servingParkRoot
+ disk *servingAuthorityDisk
+ rootDisk *servingAuthorityDisk
+ conn net.Conn
+ actor hostbox.Actor
+ current, prior servingArtifactIdentity
+ admitted hostclaim.Observed
+ receipt hostguard.FenceReceipt
+ db string
+ trace []string
+}
+
+func newServingRootFixture(t *testing.T, launched bool) *servingRootFixture {
+ t.Helper()
+ f, _, _, in := servingRetirementFixture(t)
+ first := servingAdmit(t, f, &in, "G")
+ disk := &servingAuthorityDisk{rollbackDisk: &rollbackDisk{dir: t.TempDir()}, prefix: "park:"}
+ roots := &servingAuthorityDisk{rollbackDisk: &rollbackDisk{dir: t.TempDir()}, prefix: "host:"}
+ f.guard.Root = hostguard.NewRootAuthority(roots)
+ journals, err := hostbox.NewJournalService(f.journal)
+ if err != nil {
+ t.Fatal(err)
+ }
+ fx := &servingRootFixture{t: t, f: f, disk: disk, rootDisk: roots, db: in.DBPath, actor: hostbox.Actor{Box: "box-1", Region: "ie", Enrollment: "enrollment", Revision: 7, Eligible: true}}
+ fx.prior, err = servingWriterIdentity(first, "box-1", "ie")
+ if err != nil {
+ t.Fatal(err)
+ }
+ fs := newServingParkFS(f.guard.DataDir, disk)
+ fs.owner = uint32(os.Geteuid())
+ fs.mount = func(string) (bool, error) { return true, nil }
+ fx.p = &servingParkRoot{ledger: &servingParkLedger{state: disk, fs: fs, journal: f.journal, now: func() time.Time { return f.now }}}
+ fx.p.root = ClaimRootHTTP{Roots: map[string]string{"data": f.guard.DataDir}, Journals: journals,
+ Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: f.guard, Evidence: f.claims}, Authenticate: func(context.Context, net.Conn) (StopPeer, error) {
+ return StopPeer{UID: 1001, PID: 77, ProcessStart: "edge-start", BootID: "boot", ControllerID: "controller"}, nil
+ }},
+ InventoryActor: func(context.Context) (hostbox.Actor, error) { return fx.actor, nil }, RequireNewWork: func(context.Context) error { return nil }}
+ fx.install(first)
+ fx.tags(fx.prior.Stamp)
+ fx.receipt = servingStop(t, f, f.read())
+ target := fx.receipt.Target
+ if err := f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "edge-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/parent", Device: 1, Inode: 2}}}); err != nil {
+ t.Fatal(err)
+ }
+ if launched {
+ res := hostbox.LaunchReservation{ID: "attempt:main", Role: "main", InvocationID: "attempt", Inputs: "digest", Target: target, Group: hostbox.LaunchGroup{RelativePath: "system.slice/carlos-app@shop.service"}, Executable: hostbox.LaunchFile{Device: 1, Inode: 3}}
+ if err := f.journal.IntendUnit(t.Context(), res); err != nil {
+ t.Fatal(err)
+ }
+ if err := f.journal.StartedUnit(t.Context(), res, hostbox.LaunchGroup{RelativePath: res.Group.RelativePath, Device: 1, Inode: 4, InvocationID: "unit"}, 99, "tenant-start"); err != nil {
+ t.Fatal(err)
+ }
+ if err := f.journal.SettleUnit(t.Context(), target, res.ID); err != nil {
+ t.Fatal(err)
+ }
+ }
+ o := f.read()
+ fx.admitted, 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", PredecessorRegion: "ie", Mode: "cold", NewGeneration: "G2"}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ fx.current, err = servingWriterIdentity(fx.admitted, "box-1", "ie")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for suffix, body := range map[string]string{"": "served bytes", ".gen": "G", "-wal": "WAL", "-shm": "SHM"} {
+ if err := os.WriteFile(fx.db+suffix, []byte(body), 0600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ left, right := net.Pipe()
+ fx.conn = left
+ t.Cleanup(func() { left.Close(); right.Close() })
+ disk.trace, roots.trace = &fx.trace, &fx.trace
+ return fx
+}
+func (f *servingRootFixture) install(o hostclaim.Observed) {
+ f.t.Helper()
+ h := o.Claim.Holder
+ d, err := os.Open(filepath.Dir(f.db))
+ if err != nil {
+ f.t.Fatal(err)
+ }
+ id, err := servingParkDirID(d)
+ d.Close()
+ if err != nil {
+ f.t.Fatal(err)
+ }
+ owner := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", o.Host, h.Incarnation, h.Stamp, f.actor.Box)))
+ in := hostbox.StopInventory{Complete: true, Host: o.Host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: f.actor.Box, Enrollment: f.actor.Enrollment, DirectoryRevision: "7", Entries: []hostbox.StopInventoryEntry{{Kind: "path", ManagedRoot: "data", RelativePath: "instances/" + provision.InstanceID(o.Host), ArtifactID: "state", Ownership: hex.EncodeToString(owner[:]), Materialization: "materialized", Device: id.Device, Inode: id.Inode, Type: unix.S_IFDIR}}}
+ inventory := f.p.root.Journals.Inventory
+ if err := inventory.WriteStopInventory(f.t.Context(), in); err != nil {
+ f.t.Fatal(err)
+ }
+ _, revision, err := inventory.Snapshot(f.t.Context(), o.Host, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp})
+ if err != nil {
+ f.t.Fatal(err)
+ }
+ root := hostguard.RootInstallation{Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Migration: 1, InventoryRevision: revision, Account: h.Account, App: h.App, Record: h.Key, Box: f.actor.Box, Enrollment: f.actor.Enrollment}
+ if err := f.f.guard.EstablishRoot(f.t.Context(), root, func(*hostclaim.Claim) error { return nil }); err != nil {
+ f.t.Fatal(err)
+ }
+}
+func (f *servingRootFixture) tags(stamp uint64) {
+ f.t.Helper()
+ _, err := hostclaim.Update(f.t.Context(), f.f.claims, f.f.read(), func(c *hostclaim.Claim) error {
+ rel, _ := filepath.Rel(f.f.guard.DataDir, f.db)
+ c.Inventory = map[string]hostclaim.InventoryEntry{f.actor.Box: {Layout: "nested", Paths: map[string]uint64{rel: stamp, rel + ".gen": stamp}}}
+ return nil
+ })
+ if err != nil {
+ f.t.Fatal(err)
+ }
+}
+func (f *servingRootFixture) park() (servingParkAuthorization, error) {
+ return f.p.parkServingDatabase(f.t.Context(), f.conn, f.current, f.prior)
+}
+func (f *servingRootFixture) mutateRoot(edit func(map[string]any)) {
+ f.t.Helper()
+ name := f.current.Host + ".json"
+ b, err := f.rootDisk.Read(name)
+ if err != nil {
+ f.t.Fatal(err)
+ }
+ var m map[string]any
+ if err = json.Unmarshal(b, &m); err != nil {
+ f.t.Fatal(err)
+ }
+ edit(m)
+ b, err = json.Marshal(m)
+ if err != nil {
+ f.t.Fatal(err)
+ }
+ if err = f.rootDisk.Write(name, b); err != nil {
+ f.t.Fatal(err)
+ }
+}
+func TestServingParkRootGenerationAuthority(t *testing.T) {
+ for _, launched := range []bool{false, true} {
+ t.Run(fmt.Sprint(launched), func(t *testing.T) {
+ f := newServingRootFixture(t, launched)
+ a, err := f.park()
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := []string{"park:namespace.lock", "host:" + f.current.Host + ".lock", "park:slot-" + servingParkKey(f.current.Box, f.current.Host) + ".lock", "park:retirement-" + a.ID + ".lock"}
+ if !reflect.DeepEqual(f.trace, want) {
+ t.Fatalf("lock order: %v", f.trace)
+ }
+ if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err == nil {
+ t.Fatal("completed before retag")
+ }
+ f.tags(f.current.Stamp)
+ if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err != nil {
+ t.Fatal(err)
+ }
+ r, err := f.p.ledger.read(a.ID)
+ if err != nil || !r.Complete {
+ t.Fatalf("record: %+v %v", r, err)
+ }
+ if (r.Disposition == servingParkRetain) != launched {
+ t.Fatalf("wrong disposition: %v", r.Disposition)
+ }
+ again, err := f.p.parkServingDatabase(t.Context(), f.conn, f.current, servingArtifactIdentity{})
+ if err != nil || again != a {
+ t.Fatalf("lost completion reply: %+v %v", again, err)
+ }
+ if err = os.WriteFile(f.db, []byte("replacement"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ if _, err = f.p.parkServingDatabase(t.Context(), f.conn, f.current, servingArtifactIdentity{}); err == nil {
+ t.Fatal("accepted replacement source")
+ }
+ })
+ }
+}
+func TestServingParkRootRefusesBeforeEffects(t *testing.T) {
+ for _, attack := range []string{"peer", "peer-root", "census", "roster", "quarantine", "enrollment", "revision", "claim", "prior", "tags", "half-tags", "source", "inventory-revision", "receipt", "pending", "root-quarantine", "root-pending", "barrier", "canceled"} {
+ t.Run(attack, func(t *testing.T) {
+ f := newServingRootFixture(t, true)
+ ctx := t.Context()
+ switch attack {
+ case "peer":
+ f.p.root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return StopPeer{}, ErrClaimStopIdentity }
+ case "peer-root":
+ f.p.root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) {
+ return StopPeer{PID: 77, ProcessStart: "start", BootID: "boot", ControllerID: "controller"}, nil
+ }
+ case "census":
+ f.p.root.RequireNewWork = func(context.Context) error { return hostbox.ErrChanged }
+ case "roster":
+ f.actor.Eligible = false
+ case "quarantine":
+ f.actor.Quarantined = true
+ case "enrollment":
+ f.actor.Enrollment = "other"
+ case "revision":
+ f.actor.Revision++
+ case "claim":
+ f.current.Generation = "foreign"
+ case "prior":
+ f.prior.Generation = "foreign"
+ case "tags":
+ f.tags(f.current.Stamp)
+ case "half-tags":
+ _, err := hostclaim.Update(t.Context(), f.f.claims, f.f.read(), func(c *hostclaim.Claim) error {
+ rel, _ := filepath.Rel(f.f.guard.DataDir, f.db)
+ delete(c.Inventory[f.actor.Box].Paths, rel+".gen")
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ case "source":
+ if err := os.Rename(filepath.Dir(f.db), filepath.Dir(f.db)+".saved"); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir(filepath.Dir(f.db), 0700); err != nil {
+ t.Fatal(err)
+ }
+ case "inventory-revision":
+ in, _, err := f.p.root.Journals.Inventory.Snapshot(t.Context(), f.prior.Host, hostguard.Captured{Incarnation: f.prior.Incarnation, Stamp: f.prior.Stamp})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = f.p.root.Journals.Inventory.WriteStopInventory(t.Context(), in); err != nil {
+ t.Fatal(err)
+ }
+ case "receipt":
+ f.mutateRoot(func(m map[string]any) {
+ s := m["State"].(map[string]any)
+ s["fence_journal"].([]any)[0].(map[string]any)["bytes"] = "Zm9yZ2Vk"
+ })
+ case "pending":
+ f.mutateRoot(func(m map[string]any) { m["State"].(map[string]any)["fence_journal"] = []any{} })
+ case "root-quarantine":
+ f.mutateRoot(func(m map[string]any) { m["Quarantined"] = true })
+ case "root-pending":
+ f.mutateRoot(func(m map[string]any) {
+ m["PendingInstallation"] = m["Installation"]
+ m["PendingInventory"] = "cGVuZGluZw=="
+ })
+ case "barrier":
+ f.mutateRoot(func(m map[string]any) { m["Barrier"] = m["State"].(map[string]any)["active_fence"] })
+ case "canceled":
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithCancel(ctx)
+ cancel()
+ }
+ if _, err := f.p.parkServingDatabase(ctx, f.conn, f.current, f.prior); err == nil {
+ t.Fatal("unproved authority accepted")
+ }
+ names, err := f.disk.Names()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(names) != 0 {
+ t.Fatalf("refusal wrote private state: %v", names)
+ }
+ db := f.db
+ if attack == "source" {
+ db = filepath.Join(filepath.Dir(db)+".saved", filepath.Base(db))
+ }
+ if b, err := os.ReadFile(db); err != nil || string(b) != "served bytes" {
+ t.Fatalf("source changed: %q %v", b, err)
+ }
+ })
+ }
+}
+
+type servingRootClientFixture struct{ f *servingRootFixture }
+
+func (c servingRootClientFixture) Park(ctx context.Context, current, prior servingArtifactIdentity) (servingParkAuthorization, error) {
+ return c.f.p.parkServingDatabase(ctx, c.f.conn, current, prior)
+}
+func (c servingRootClientFixture) ParkLegacy(context.Context, servingArtifactIdentity, servingLegacyPredecessor) (servingParkAuthorization, error) {
+ return servingParkAuthorization{}, hostbox.ErrUnsupported
+}
+func (c servingRootClientFixture) Complete(ctx context.Context, id string, current servingArtifactIdentity) error {
+ return c.f.p.completeServingDatabasePark(ctx, c.f.conn, id, current)
+}
+
+type servingRootWork struct {
+ g hostguard.Guard
+ o hostclaim.Observed
+}
+
+func (w servingRootWork) With(ctx context.Context, _ hostwrite.Identity, fn func() error) error {
+ return w.g.With(ctx, w.o.Host, hostguard.Captured{Incarnation: w.o.Claim.Holder.Incarnation, Stamp: w.o.Claim.Holder.Stamp}, func(c *hostclaim.Claim) error {
+ if !claimSameWriter(w.o, hostclaim.Observed{Host: w.o.Host, Claim: c}, nil) {
+ return hostbox.ErrChanged
+ }
+ return nil
+ }, fn)
+}
+func TestServingParkRootProductionRestoreSuccessor(t *testing.T) {
+ f := newServingRootFixture(t, true)
+ plan := plan12RestoreBaseline(t, f.f)
+ f.install(f.admitted)
+ r := &claimServingArtifactRetirement{Claims: f.f.claims, Guard: f.f.guard, DataDir: f.f.guard.DataDir, Box: f.actor.Box, Region: f.actor.Region, Root: servingRootClientFixture{f}}
+ in := ClaimLaunchInputs{DBPath: f.db}
+ work := servingRootWork{f.f.guard, f.admitted}
+ restore := hostbox.ColdRestore{Reader: f.f.data, Bucket: f.f.bucket, Generation: f.current.Generation, Work: work,
+ BeforeLegacyInstall: func(ctx context.Context, _, _ string) error { return r.BeforeLegacyInstall(ctx, f.admitted, in, work) },
+ BeforeInstallLocked: func(ctx context.Context, _, _ string) error { return r.EnrollLocked(ctx, f.admitted, in) },
+ RetireArtifacts: func(ctx context.Context, generation string) error { return r.Retire(ctx, f.admitted, in, generation) }}
+ if err := r.Recover(t.Context(), f.admitted, in); err != nil {
+ t.Fatal(err)
+ }
+ if err := restore.Install(t.Context(), f.db, plan); err != nil {
+ t.Fatal(err)
+ }
+ if got := mwReadDB(t, f.db); got != "selected recovery baseline" {
+ t.Fatal(got)
+ }
+ marker, err := hostbox.ReadGenerationMarker(f.db + ".gen")
+ if err != nil || marker != "G2" {
+ t.Fatalf("marker %q %v", marker, err)
+ }
+ names, err := f.disk.Names()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, name := range names {
+ if strings.HasPrefix(name, "retirement-") && strings.HasSuffix(name, ".json") {
+ rec, err := f.p.ledger.read(strings.TrimSuffix(strings.TrimPrefix(name, "retirement-"), ".json"))
+ if err != nil || !rec.Complete {
+ t.Fatalf("incomplete: %v", err)
+ }
+ b, err := os.ReadFile(filepath.Join(f.f.guard.DataDir, "instances", ".serving-parked", rec.Authorization.ID, filepath.Base(f.db)))
+ if err != nil || string(b) != "served bytes" {
+ t.Fatalf("parked predecessor: %q %v", b, err)
+ }
+ }
+ }
+}
+func TestServingParkRootWaitReauthenticates(t *testing.T) {
+ f := newServingRootFixture(t, true)
+ unlock, err := f.disk.Lock(t.Context(), "namespace.lock")
+ if err != nil {
+ t.Fatal(err)
+ }
+ ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
+ defer cancel()
+ if _, err = f.p.parkServingDatabase(ctx, f.conn, f.current, f.prior); !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("lock bypass: %v", err)
+ }
+ unlock()
+ f.p.root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return StopPeer{}, ErrClaimStopIdentity }
+ if _, err = f.park(); !errors.Is(err, ErrClaimStopIdentity) {
+ t.Fatalf("stale authentication: %v", err)
+ }
+}
+
+func TestServingParkRootContinuesOriginalRetirement(t *testing.T) {
+ for _, point := range []string{"rename", "parked", "retag", "lost-continuation"} {
+ t.Run(point, func(t *testing.T) {
+ f := newServingRootFixture(t, true)
+ original := f.current
+ fs := f.p.ledger.fs.(*servingParkFS)
+ rename := fs.rename
+ if point == "rename" {
+ once := false
+ fs.rename = func(a int, b string, c int, d string, e uint) error {
+ err := rename(a, b, c, d, e)
+ if err == nil && !once {
+ once = true
+ return errors.New("lost rename reply")
+ }
+ return err
+ }
+ }
+ a, err := f.park()
+ if point == "rename" {
+ if err == nil {
+ t.Fatal("fault missed")
+ }
+ fs.rename = rename
+ a = servingParkAuthorization{ID: servingRetirementID(f.prior, original, false)}
+ } else if err != nil {
+ t.Fatal(err)
+ }
+ before, err := f.p.ledger.read(a.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if point == "retag" || point == "lost-continuation" {
+ f.tags(original.Stamp)
+ }
+ f.install(f.f.read())
+ servingStop(t, f.f, f.f.read())
+ o := f.f.read()
+ later, err := hostclaim.Update(t.Context(), f.f.claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: "box-1", Region: "ie", PredecessorRegion: "ie", Mode: "cold", NewGeneration: "G3"}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.current, err = servingWriterIdentity(later, "box-1", "ie")
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.f.now = f.f.now.Add(2 * time.Hour)
+ requested := f.prior
+ if point == "retag" || point == "lost-continuation" {
+ requested = original
+ }
+ if point == "lost-continuation" {
+ disk := &servingParkFaultStore{Store: f.disk, failAt: 1}
+ f.p.ledger.state = disk
+ if _, err = f.p.parkServingDatabase(t.Context(), f.conn, f.current, requested); err == nil {
+ t.Fatal("lost continuation reply missed")
+ }
+ f.p.ledger.state = f.disk
+ }
+ continued, err := f.p.parkServingDatabase(t.Context(), f.conn, f.current, requested)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if continued.ID != a.ID || continued.OriginalSuccessor != original || continued.Successor != f.current || continued.Predecessor != f.prior {
+ t.Fatalf("relabelled retirement: %+v", continued)
+ }
+ f.tags(f.current.Stamp)
+ if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err != nil {
+ t.Fatal(err)
+ }
+ after, err := f.p.ledger.read(a.ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if after.Authorization != before.Authorization || len(after.Continuations) != 1 || !after.Complete {
+ t.Fatalf("history: %+v", after)
+ }
+ if !before.ParkedAt.IsZero() && (!before.ParkedAt.Equal(after.ParkedAt) || !before.ReapAfter.Equal(after.ReapAfter)) {
+ t.Fatal("continuation reset retention")
+ }
+ if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, original); err == nil {
+ t.Fatal("old successor completed continuation")
+ }
+ again, err := f.p.parkServingDatabase(t.Context(), f.conn, f.current, servingArtifactIdentity{})
+ if err != nil || again != continued {
+ t.Fatalf("replay: %+v %v", again, err)
+ }
+ })
+ }
+}
+
+func TestServingParkRootRechecksClaimAfterInputs(t *testing.T) {
+ f := newServingRootFixture(t, true)
+ original := f.p.root.Stop.Authenticate
+ f.p.root.Stop.Authenticate = func(ctx context.Context, conn net.Conn) (StopPeer, error) {
+ _, err := hostclaim.Update(ctx, f.f.claims, f.f.read(), func(c *hostclaim.Claim) error { c.Inventory = nil; return nil })
+ if err != nil {
+ return StopPeer{}, err
+ }
+ return original(ctx, conn)
+ }
+ if _, err := f.park(); err == nil {
+ t.Fatal("used claim captured before peer and inventory checks")
+ }
+ names, err := f.disk.Names()
+ if err != nil || len(names) != 0 {
+ t.Fatalf("effects: %v %v", names, err)
+ }
+}
+func TestServingParkRootContinuationNeedsStop(t *testing.T) {
+ f := newServingRootFixture(t, true)
+ a, err := f.park()
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.install(f.admitted)
+ original := f.current
+ o := f.f.read()
+ next, err := hostclaim.Update(t.Context(), f.f.claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: "box-1", Region: "ie", PredecessorRegion: "ie", Mode: "cold", NewGeneration: "G3"}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.current, err = servingWriterIdentity(next, "box-1", "ie")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err = f.park(); err == nil {
+ t.Fatal("continued without original successor stop")
+ }
+ rec, err := f.p.ledger.read(a.ID)
+ if err != nil || rec.activeAuthorization().Successor != original || len(rec.Continuations) != 0 {
+ t.Fatalf("changed record: %+v %v", rec, err)
+ }
+}
diff --git a/internal/host/claimservingparksource_linux.go b/internal/host/claimservingparksource_linux.go
index 3d8f89555..462d42c4c 100644
--- a/internal/host/claimservingparksource_linux.go
+++ b/internal/host/claimservingparksource_linux.go
@@ -18,7 +18,7 @@ import (
)
// servingParkStateDirectory accepts only root installation and root inventory
-// snapshots. The future root handler must read both locally under host authority;
+// snapshots. The root handler reads both locally under host authority;
// this function is not a transport decoder and accepts no registry row.
func servingParkStateDirectory(data string, o hostclaim.Observed, installation hostguard.RootInstallation, in hostbox.StopInventory, revision uint64, actor hostbox.Actor) (servingArtifactDirectory, error) {
var zero servingArtifactDirectory
diff --git a/internal/hostguard/servingretirement.go b/internal/hostguard/servingretirement.go
new file mode 100644
index 000000000..7672d0ee3
--- /dev/null
+++ b/internal/hostguard/servingretirement.go
@@ -0,0 +1,67 @@
+package hostguard
+
+import (
+ "context"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+)
+
+// WithServingRetirement holds both root and shared host locks. Unlike Work,
+// retirement may precede installation of the newly admitted stamp. The callback
+// must authenticate the older installation and stop receipts before effects.
+// It must not call a lock-taking Guard or RootAuthority method.
+func (g Guard) WithServingRetirement(ctx context.Context, host string, current Captured, fn func(hostclaim.Observed, FenceView) error) error {
+ if ctx == nil || g.Root == nil || g.FenceView != nil || current.Incarnation == "" || current.Stamp == 0 || fn == nil {
+ return ErrInvalidInput
+ }
+ return g.locked(ctx, host, func(d *recordDir) error {
+ o, err := g.freshObserved(ctx, host)
+ if err != nil {
+ return err
+ }
+ c, h := o.Claim, o.Claim.Holder
+ if h.Incarnation != current.Incarnation || h.Stamp != current.Stamp || h.Session != current.Stamp || c.Op.Kind != hostclaim.KindServing || c.Op.ID != "" || c.Op.Phase != "" {
+ return ErrStampMismatch
+ }
+ r, err := d.read(host)
+ if err != nil {
+ return err
+ }
+ if r.quarantined || r.pendingInstallation != nil || r.barrier != nil {
+ return ErrFenced
+ }
+ if r.ActiveFence != nil {
+ completed := false
+ for _, receipt := range r.FenceJournal {
+ completed = completed || receipt.Target == *r.ActiveFence
+ }
+ if !completed || r.ActiveFence.Incarnation != current.Incarnation || r.ActiveFence.Stamp >= current.Stamp {
+ return ErrFenced
+ }
+ }
+ if r.Incarnation != current.Incarnation || r.ActiveStamp == 0 || r.ActiveStamp > current.Stamp {
+ return ErrStampMismatch
+ }
+ for _, retired := range r.rootRetired {
+ if retired == current {
+ return ErrRetiredStamp
+ }
+ }
+ for _, receipt := range r.FenceJournal {
+ if receipt.Target.matchesSession(c, current) {
+ return ErrFenced
+ }
+ }
+ view, err := g.Root.viewLocked(host, Captured{Incarnation: r.Incarnation, Stamp: r.ActiveStamp})
+ if err != nil {
+ return err
+ }
+ if err = view.validate(host, view.Captured); err != nil {
+ return err
+ }
+ if err = ctx.Err(); err != nil {
+ return err
+ }
+ return fn(o, view)
+ })
+}
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv
index e5864bd32..70e5eb676 100644
--- a/internal/provision/testdata/plan14-root-reads-seed.tsv
+++ b/internal/provision/testdata/plan14-root-reads-seed.tsv
@@ -1,5 +1,5 @@
-# Files seed checkpoint 3 overlay; pending Paul’s census ruling.
-# Root filesystem and retirement-ledger mechanisms have test callers only; production binding is refused.
+# Files seed checkpoint 4 overlay; pending Paul’s census ruling.
+# Root generation authorization and continuation have test callers only; production binding is refused.
# Load after zddr4 and its explicit seal check. No legacy fingerprints are added or removed.
# Every classification below is rev3=PROPOSED-seed, not an approved disposition.
internal/host/claimconstruction.go OpenProductionClaimAssembly authority C,G rev3=PROPOSED-seed; box process identity and provider bindings; serving dispatch independent of moves; serving enrollment required before a runtime can be built file:Read@4150,file:Read@9550,file:ReadFile@3694,registry:Host@9577,whole:Route@9665 088f34c7fe1b4f9bb7a3187d40d60bff0fd811ebf1e1888ce3d4781dc4de80d7
@@ -54,18 +54,28 @@ internal/host/claimservingpark_other.go servingParkFS.Move authority C,J4 rev3=P
internal/host/claimservingpark_other.go servingParkFS.Prepare authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority file:Prepare@22 645770372357707f635351a377e943e78f1c160222385e41f42b80d57adedc29
internal/host/claimservingpark_records.go servingParkDecode authority C,J4 rev3=PROPOSED-seed; strict private-record JSON with unknown-field and trailing-value refusal 189c7944ea9f5d269d39ed6a869b6bd739e3033195f06cc1f2c9f23a23180184
internal/host/claimservingpark_records.go servingParkKey authority C,J4 rev3=PROPOSED-seed; box/host identity used for stable per-host namespace and slot keys 67518302550380dbf9b741a73758e5c94307eff70ff8a09cc56763a8aa794d86
-internal/host/claimservingpark_records.go servingParkLedger.completeLocked authority C,J4 rev3=PROPOSED-seed; root retirement and pending-slot records plus fresh filesystem absence/identity checks; completion requires outer root claim-tag authority which remains unbound 246eb0689f2f36a968c60d9241f52d2f3cf7ebf76b025092e1658f1b7b495236
+internal/host/claimservingpark_records.go servingParkLedger.completeLocked authority C,J4 rev3=PROPOSED-seed; root retirement and pending-slot records plus fresh filesystem absence/identity checks; completion requires outer root claim-tag authority which remains unbound 00f9c10b8130c134a41dea74848aa1651ae3331c8440a81e1b3f59f54f6a7218
internal/host/claimservingpark_records.go servingParkLedger.deleteLocked authority C,J4 rev3=PROPOSED-seed; complete retained record or proved discard disposition and persisted cause/progress before filesystem deletion 1545950c564d51d9c29b0d76daca1ddec5a159481698b11accd4dd7411157b37
-internal/host/claimservingpark_records.go servingParkLedger.parkLocked authority C,J4 rev3=PROPOSED-seed; immutable source capture, root-local launch journal, exact stop bytes, legacy consumption, pending work and incumbent slot; only started databases replace slots; outer root authorization remains unbound file:Prepare@3312,file:Read@1564 d248f233de019cf9e28cf89bbf1e41f8e9c23089556ca709d18a9b076a04c09c
+internal/host/claimservingpark_records.go servingParkLedger.parkLocked authority C,J4 rev3=PROPOSED-seed; immutable source capture, root-local launch journal, exact stop bytes, legacy consumption, pending work and incumbent slot; only started databases replace slots; outer root authorization remains unbound file:Prepare@3247,file:Read@1693 c46ddfb8aaf8339eb6d5dc414724ead938338552477825efe6d1e6f677897b32
internal/host/claimservingpark_records.go servingParkLedger.read authority C,J4 rev3=PROPOSED-seed; private retirement record by validated deterministic ID and strict record invariants file:Read@249 868c8c3f5682dc36738d6960d57f2f1fcc6512374c8f8e0790187bf69cf3de64
-internal/host/claimservingpark_records.go servingParkLedger.reapLocked authority C,J4 rev3=PROPOSED-seed; root completion, retention disposition and fixed seven-day deadline; rollback or pending work refuses expiry 2352daae92630f8e33bd7c9bdd70f382de9da9be559de8522982eb73cf935c09
+internal/host/claimservingpark_records.go servingParkLedger.reapLocked authority C,J4 rev3=PROPOSED-seed; root completion, retention disposition and fixed seven-day deadline; rollback or pending work refuses expiry aec4f50b45d1d955fac9814fe9e0d403f5f3f7f939c71f8f6b8ac15e9173b846
internal/host/claimservingpark_records.go servingParkLedger.save authority C,J4 rev3=PROPOSED-seed; validated private retirement record before durable rootstate write b6f940b1e21cf1beb67638effea958d2b1119695494a562e22bbbe0469d8af49
-internal/host/claimservingpark_records.go servingParkLedger.slot authority C,J4 rev3=PROPOSED-seed; private box/host slot and pending retirement identities across incarnation changes file:Read@179 5c0d897b1bc9f0733d1dad18a90ce27900e371aaae2940e49cded65888d1c084
-internal/host/claimservingpark_records.go servingParkLedger.withNamespace authority C,J4 rev3=PROPOSED-seed; private rootstate namespace lock and cancellation before invoking root authority; production host-guard composition remains unbound b7cfae44fdb7de9ef8130f5581710ae3a1a526bb059b4f616650172490f5c8ac
+internal/host/claimservingpark_records.go servingParkLedger.slot authority C,J4 rev3=PROPOSED-seed; private box/host slot and pending retirement identities across incarnation changes; last-retirement identity supports discarded/artifact-free completion replay file:Read@179 239b806b2e2bfdd568f5d6b749d299f0bc7b118d675e872429432591a89b7b68
+internal/host/claimservingpark_records.go servingParkLedger.withNamespace authority C,J4 rev3=PROPOSED-seed; private rootstate namespace lock and cancellation before invoking root authority; composed with root host authority in generation tests; production remains unbound b7cfae44fdb7de9ef8130f5581710ae3a1a526bb059b4f616650172490f5c8ac
internal/host/claimservingpark_records.go servingParkLedger.write authority C,J4 rev3=PROPOSED-seed; serialized private record sent only to the supplied rootstate store 0635d2c27871e67079119a0f2e3034e817b765d0fe3a47e7d9bdc66fb2779571
internal/host/claimservingpark_records.go type servingParkFilesystem authority C,J4 rev3=PROPOSED-seed; private filesystem effect interface over root-recorded retirement identities; no wire or public request fields file:Prepare@180 b93f0e2af16b66aee1ff71294e215eeba9e661fe696c917c982d9ebc2dc682c7
-internal/host/claimservingpark_records.go validateServingParkRecord authority C,J4 rev3=PROPOSED-seed; version, immutable tuples, exact serving receipt bytes, source identities, per-file progress, disposition, completion and retention deadline invariants 0ab78151f2243224751d32fb309db9e36eed27487935c97f9531a72adf42fc62
-internal/host/claimservingparksource_linux.go servingParkStateDirectory authority C,J4 rev3=PROPOSED-seed; root installation/inventory snapshots and authoritative serving observation; exact account/app/record, revision/enrollment, canonical nested path, ownership and descriptor identity; snapshot acquisition remains unbound ec4a121330c5c8f27343f21af0bbe458cced885d4754e96c0d87b12d9b4a8f35
-@coverage internal/host cc1a23e90ccf77b923b6c02649b3823cfbb0454368c06571e8e5292315becfc6
+internal/host/claimservingpark_records.go validateServingParkRecord authority C,J4 rev3=PROPOSED-seed; version, immutable tuples, exact serving receipt bytes, source identities, per-file progress, disposition, completion and retention deadline invariants; bounded immutable continuation history and exact predecessor stops db48025925e4b5acaf3231fdbf4d19900f406017b402efdf5ba1cb2550d00bc6
+internal/host/claimservingparksource_linux.go servingParkStateDirectory authority C,J4 rev3=PROPOSED-seed; root installation/inventory snapshots and authoritative serving observation; exact account/app/record, revision/enrollment, canonical nested path, ownership and descriptor identity; root acquisition under host lock is exercised by generation tests; production remains unbound ec4a121330c5c8f27343f21af0bbe458cced885d4754e96c0d87b12d9b4a8f35
+internal/hostguard/servingretirement.go Guard.WithServingRetirement authority C,J4 rev3=PROPOSED-seed; fresh serving claim and root installation/fence snapshot under root and shared host locks; refuses pending, quarantined, foreign, retired or already stopped successors; permits an older completed predecessor 3d105c50be7e613777055f561321efadf77fe823d8dc3263a60f2c1211b71108
+internal/host/claimservingparkroot_linux.go servingParkRoot.withAuthority authority C,J4 rev3=PROPOSED-seed; independent active-edge peer authentication, census gate, roster, local root inventory and fresh claim recheck under namespace and root host authority; no registry path or caller receipt file:Read@2432 6d0c84a5a9c5403f166db4f200504a3ccd75ebcd7ddac1736e3ac046237a2882
+internal/host/claimservingparkroot_linux.go servingRootStops authority C,J4 rev3=PROPOSED-seed; root fence snapshot and known installation stamps; exact local serving receipt bytes, enrollment, revision and unique fence identities d16eb077adfb8f0f8c9e63d1609250dc3470ec424bdde63b57c47d09500b2bce
+internal/host/claimservingparkroot_linux.go sameServingRootStops authority C,J4 rev3=PROPOSED-seed; byte-exact comparison of persisted and freshly selected root receipt sets, including every target field and evidence time c9e0803635546eecc3a7d07760cb69fc915182f96b0d1581ed655de3382edd88
+internal/host/claimservingparkroot_linux.go servingParkRoot.parkServingDatabase authority C,J4 rev3=PROPOSED-seed; fresh paired claim tags, root slot and retirement records, retained predecessor and successor admissions, root stop history and source identity before park or continuation 802f4b8f5a2e49c3d6d4b0530c8cc484d29abf0a25ae852d1e7f39509d0e986e
+internal/host/claimservingparkroot_linux.go servingParkRoot.completeServingDatabasePark authority C,J4 rev3=PROPOSED-seed; fresh paired enrollment, same current writer, root source identity and complete original/continuation stop history before durable completion f120d235529b61ce93167f4c364a962b7dc4197ba0611719547015182cba96e1
+internal/host/claimservingparkroot_linux.go validateServingRootHistory authority C,J4 rev3=PROPOSED-seed; retained local successor admissions and byte-exact root stops for every persisted continuation; rejects relabelled or missing history c85a1be96de5fa61acd48c4adf9393d8e837880f5133a2f965f1104037e02329
+internal/host/claimservingpark_records.go servingParkRecord.activeAuthorization authority C,J4 rev3=PROPOSED-seed; last persisted continuation authorization or immutable original authorization; no source or deadline changes fe168fe52317e45ce70c3e97a9bc2c52d2409328ab4844a8d2a34fc4babba36d
+internal/host/claimservingpark_records.go servingParkLedger.continueLocked authority C,J4 rev3=PROPOSED-seed; namespace/host-protected slot and record locks; pending original retirement, absent source and retained parked identities before durable bounded continuation 87f297a924f7f153624304233218d8b5548a016fc7dbf0f7cb8dedb789324009
+internal/host/claimservingpark_records.go servingParkLedger.replaceLocked authority C,J4 rev3=PROPOSED-seed; completed incumbent record under its record lock; resumes its recorded deletion cause before slot replacement 3e67316e959c9a901c32a2a61fda4e6ea5e92fe51bdb7fdb5f5c53d9d5960aad
+@coverage internal/host 9900e8ad9fe9d49c4359c0d7d11ff5a804a5b7b54328a50bcf6c3dfde91f771e
@coverage internal/hostbox 72c9b640d98c779a044579f70a103a68cd9819c794a0bb3e53f378d39ad11419
-@coverage internal/hostguard 51408b42f01be94a232e395494e5d154cc57736e32a6519ac6c03e9ff3d091e6
+@coverage internal/hostguard 94912485e1e5fad4bad56985dd48ede776cfcb5fe18e0f4a59953584ddab06c0